We have a service-based application which has both internal (intranet) and external (internet) customers. We want to expose a secure WCF service for this application using appropriate and effective protocols based on the kind of connection.
Originally, the “obvious” answer appeared to be use a WCF routing service. A WCF router can be placed on an externally-facing web server and proxy requests to the internal server which actually serves the requests. However, upon further investigation, it turns out that the WCF router is extremely limited in the default implementation when security is in use:
It turns out that the WCF team did not provide a very good solution here. The only use case that supports security context forwarding is message security with Windows credentials. … Other use cases such as username password , X.509 or federated credentials does not allow security context forwarding. It means that the router can be configured to enforce message security but the service must be configured to disable security and it cannot access the security context such as the user’s Identity. (Source)
Well that’s a total non-starter. Since we have external users, we support custom username/password authentication. The server’s behavior is tightly integrating with the user’s identity, which must be known. Furthermore, the idea of transporting messages completely in the open between the router and primary server was also not appealing. We could enable this message transport by re-attaching the client’s credentials at the router, however, this would require loading the primary server’s private key certificate on the router server (which is a DMZ server). Neither secure nor appealing.
The solution was to use our NetScaler DMZ device to accept HTTP connections and forward them to an HTTP WCF endpoint on the primary server. Encryption is delegated to the message level, using a certificate, with authentication handled by username/password also at the message level. The client (whether internal or external) encrypts and signs the message with the server’s public key certificate. The private key only exists on the server. Thus, the communication is secure at all points.
In order to ensure no lapses in security implementation occur, we can set the “EncryptAndSign” protection level on the service interface. This ensures that the service will reject any circumstance in which the message is not encrypted.
[ServiceContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)] public interface IWsbisService { ... }
Since the requests will be forwarded from the NetScaler, the URL that the client requests will not match the actual answering host. By default, WCF will reject connections of this type. It is necessary to modify the service behavior to disable address filtering.
[ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)] public class WsbisService : IWsbisService { ... }
In order to implement this technique, we relieve the WCF service of any transport-level security. Although external transport level security (HTTPS) can be provided by the NetScaler, this interferes with WCF (it wants to either encrypt the message, or transport, but not both; since the HTTPS is stripped off, that won’t work). All WCF security (encryption and signing) will be provided at the message level, which can then be passed without inspection by the NetScaler.
We then configure the two server endpoints (HTTP, which will come from the NetScaler and external connections, and Net.TCP, which will be faster for internal connections) to both require message security. Although message security is slower than transport security, WCF cannot provided consistent transport security in these scenarios given that we don’t wish to expose the private key on an externally facing server, which would be necessary to perform the encryption at the transport level.
<netTcpBinding> <binding name="netTcpBindingConfig"> <security mode="Message"> <message clientCredentialType="UserName"/> </security> </binding> </netTcpBinding> <wsHttpBinding> <binding name="wsHttpBindingConfig" messageEncoding="Mtom"> <security mode="Message"> <message clientCredentialType="UserName"/> </security> </binding> </wsHttpBinding>
Even though “UserName” is the credential type, a certificate will still be required for encryption. You will need to generate and apply a certificate. We load the certificate in code, and use a custom certificate and certificate validator to ensure the thumbprint matches.
On the server:
X509Certificate2 certificate = new X509Certificate2(certFile); host.Credentials.ServiceCertificate.Certificate = certificate;
On the client:
client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.Custom; client.ClientCredentials.ServiceCertificate.Authentication.CustomCertificateValidator = Certificate;
Where “Certificate” is an instance of a certificate validator class (X509CertificateValidator) which loads the public key of the certificate and compares the thumbprint of the presented certificate to the local certificate. This prohibits “man in the middle” type attacks by forcing one specific certificate (whose private key exists only on the primary server) to be used for encryption. Together with a client username/password, mutual authentication is assured. The server is thus authenticated to the client with this certificate, and the client is authenticated to the server with a username/password.
This architecture grants end-to-end security for both intranet and external customers without exposure of keys or dangerous “en route” decryption/re-encryption.
(Note: the diagram shows HTTPS but it’s just HTTP, with the message content being encrypted)