The key to building scalable WCF services is to eliminate binding configurations that could result in server affinity. For this reason you should avoid bindings that establish a session with the service, such as NetTcpBinding or WsHttpBinding with secure conversation enabled. Both BasicHttpBinding and WebHttpBinding, however, are sessionless and allow you to call a service multiple times without concern for which physical server responds to the call. This means the load can be more efficiently distributed across multiple servers.
Download the code for this blog post here.
Nevertheless, there is one wrinkle: by default the WCF HTTP bindings enable Keep-Alive, which can result in server affinity and thereby impede scalability in a load-balanced environment. Here is a snapshot of 5 calls to a service with HTTP Keep-Alive enabled over SSL. Notice the tunneling only takes place once because a consistent connection is maintained with the server. This is fine and dandy when clients are talking to the same back-end server and the SSL handshake only takes place when the connection is established.
Here is a snapshot of the same five calls but with Keep-Alive disabled. Notice how the handshake takes place each time the service is called.
To disable Keep-Alive, you need to create a custom binding, because this setting cannot be configured for the standard HTTP bindings. Here is a web.config file with custom SOAP and REST bindings and Keep-Alive disabled.
<configuration> <system.serviceModel> <services> <service name="SecureTransportDemo.Service.GreetingService"> <endpoint address="Soap" binding="customBinding" bindingConfiguration="soap-secure-nokeepalive" contract="SecureTransportDemo.Service.IGreetingService" name="soap-nokeepalive"/> <endpoint address="Rest" binding="customBinding" bindingConfiguration="rest-secure-nokeepalive" behaviorConfiguration="web" contract="SecureTransportDemo.Service.IGreetingService" name="rest-nokeepalive"/> </service> </services> <bindings> <customBinding> <binding name="soap-secure-nokeepalive"> <textMessageEncoding /> <httpsTransport allowCookies="false" keepAliveEnabled="false"/> </binding> <binding name="rest-secure-nokeepalive"> <webMessageEncoding /> <httpsTransport manualAddressing="true" allowCookies="false" keepAliveEnabled="false"/> </binding> </customBinding> </bindings> <behaviors> <endpointBehaviors> <behavior name="web"> <webHttp automaticFormatSelectionEnabled="true" faultExceptionEnabled="true" helpEnabled="true"/> </behavior> </endpointBehaviors> </behaviors> </system.serviceModel> </configuration>
There’s a problem however – we’re still not getting optimal efficiency for a load-balanced environment. The ideal solution would be a load balancer in the DMZ that would accept requests from clients over SSL with Keep-Alive enabled, but would turn around and communicate with backend servers over a non-secure channel with Keep-Alive disabled. For that we need a load balancer capable of SSL pass-through, such as F5’s BIG-IP. (Note that you’ll still want to secure the communication between the load balancer and the service using a mechanism such as IPSec, so that privacy is also maintained inside the corporate firewall.)
The problem here is that WCF will not allow you to pass credentials, such as username and password, over a non-secure channel. And for good reason: credentials sent in the clear could be intercepted. However, presuming it is safe to send credentials from the load balancer to the backend service from behind a firewall, this is exactly what we want to do. The trick is to fool WCF into thinking we are using a secure channel when in fact we are not.
Michele Leroux Bustamante has written an excellent article showing precisely how to do this. It entails creating a custom HttpTransportBindingElement that can assert security capabilities indicating encryption and signing at the transport level when in fact this is not the case. Essentially the custom binding element has a GetProperty method which returns an implementation of ISecurityCapabilities asserting that a protection level of EncryptAndSign is supported.
public class NonSslAuthHttpTransportBindingElement : HttpTransportBindingElement { public override BindingElement Clone() { return new NonSslAuthHttpTransportBindingElement { AuthenticationScheme = AuthenticationScheme, ManualAddressing = ManualAddressing }; } public override T GetProperty<T>(BindingContext context) { if (typeof(T) == typeof(ISecurityCapabilities)) { return (T)(object)new NonSslAuthSecurityCapabilities(); } return base.GetProperty<T>(context); } }
public class NonSslAuthSecurityCapabilities : ISecurityCapabilities { public ProtectionLevel SupportedRequestProtectionLevel { get { return ProtectionLevel.EncryptAndSign; } } public ProtectionLevel SupportedResponseProtectionLevel { get { return ProtectionLevel.EncryptAndSign; } } public bool SupportsClientAuthentication { get { return false; } } public bool SupportsClientWindowsIdentity { get { return false; } } public bool SupportsServerAuthentication { get { return true; } } }
The next step would be to create a class extending HttpTransportElement, so that you can specify the transport element for the custom binding in the config file of the service host.
public class NonSslAuthTransportElement : HttpTransportElement { public override Type BindingElementType { get { return typeof(NonSslAuthHttpTransportBindingElement); } } protected override TransportBindingElement CreateDefaultBindingElement() { return new NonSslAuthHttpTransportBindingElement(); } }
For this you’ll need to define a binding element extension in the config file that transport element type. Here is what the service config file looks like with the extension defined and used in a custom binding.
<extensions> <bindingElementExtensions> <add name="nonSslAuthTransport" type="NonSslAuthSecurity.NonSslAuthTransportElement, NonSslAuthSecurity"/> </bindingElementExtensions> </extensions> <bindings> <customBinding> <binding name="nonSslAuthBinding"> <binaryMessageEncoding /> <security authenticationMode="UserNameOverTransport"/> <nonSslAuthTransport authenticationScheme="Anonymous" allowCookies="false" keepAliveEnabled="false"/> </binding> <binding name="webNonSslAuthBinding"> <webMessageEncoding /> <nonSslAuthTransport authenticationScheme="Basic" manualAddressing="true" allowCookies="false" keepAliveEnabled="false"/> </binding> </customBinding> </bindings>
In this example I’m also specifying binary message encoding for the SOAP endpoint because I’m certain that a .NET client will be calling the service, and this will result in a more compact wire representation of the message. (This is a very good practice in an all-.NET environment, but it is not taken advantage of as often as it should be.)
If you download the code for this post, you’ll see that it contains a FakeLoadBalancer project with a routing service that simulates a load balancer with SSL pass-through capability. With a real load balancer, you could also get performance benefits from hardware-based acceleration for SSL. The sample project contains the necessary binding configurations for both SOAP and REST style endpoints. Enjoy.
Pingback: Distributed Weekly 160 — Scott Banwart's Blog
Image with Keep-Alive disabled is not visible for me.
Are you running Fiddler? If so, make sure you are using the machine name, instead of localhost, in the request url.
Please ignore, the image in your post is visible now
Pingback: Building Scalable and Secure WCF Services - .NET Code Geeks
Very helpful..
Pingback: Hello world! | Ramesh's Blog
Pingback: Experiencies NLB, WSDL, SSL, HTTPS « El TecnoBaúl de Kiquenet
Hi, very helpful blogpost. I was wondering if the sourcecode is still available for download somewhere?
Thanks!
I’m working on restoring all my download links. I should have them back up in the next day or two.
That would be great! Thanks.
Hi Tony;
When i try to this approach, i got an error message like this.
The ‘CustomBinding’.’http://tempuri.org/’ binding for the ‘ISdlService’.’http://tempuri.org/’ contract is configured with an authentication mode that requires transport level integrity and confidentiality. However the transport cannot provide integrity and confidentiality.
My Endpoint configuration:
Do you have any comment.
Have you configured the endpoint in app or web config to use the custom binding?
Thank you for this post. I had the exact problem when migrating asmx to wcf using Username Password validation.
I’ve implemented your solution and it works fine, but I have one issue.
The Service EndPoint Address in the WSDL is generated as http instead of https.
When I add Service Referernce, I manually change the endpoint configuration to https to get it working.
Is there a way to configure in such a way that an https endpoint address is generated?
Thanks for your question. The way to fix the problem is by adding a
useRequestHeadersForMetadataAddress
service behavior, as described in this SO answer: http://stackoverflow.com/a/4003143/825288.Your solution i very well but when try get WSDL from my solution i get this error:
Policy export for such a binding is Error: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider. Policy export for such a binding is not supported. Make sure the transport binding element in the binding implements the ITransportTokenAssertionProvider interface. —-> System.InvalidOperationException: Security policy export failed. The binding contains a TransportSecurityBindingElement but no transport binding element that implements ITransportTokenAssertionProvider. Policy export for such a binding is not supported. Make sure the transport binding element in the binding implements the ITransportTokenAssertionProvider interface.
at System.ServiceModel.Channels.TransportSecurityBindingElement.System.ServiceModel.Description.IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext policyContext)
There is any solution to generate WSDL ?
Yes, you may need to expose the WSDL from another endpoint.
I resolve implement IPolicyExportExtension in your class 🙂 🙂
how set maxReceivedMessageSize I test to config in web.config
but in trace I have every time this error
The maximum message size quota for incoming messages (65536) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element.
Any Idea how can change limit of MaxReceivedMessageSize ???
DO Y HAVE SKYPE ?
Yes, please see this article on how to increase maxReceivedMessageSize: https://weblogs.asp.net/hajan/transferring-large-data-when-using-web-services
Hi Toney or Antonio, can you please share what you have to do to get around this error ?
Yes I know and i just do it, but if I add MaxReceivedMessageSize in element “nonSslAuthTransport” on web.inf is not use I must had to constructor class of public NonSslAuthHttpTransportBindingElement (){
MaxReceivedMessageSize = 2097152;
MaxBufferPoolSize = 2097152;
}
😦 😦
Sure, does that work?
SURE !!!! work only after I had create constructor and initialize value to 2097152, but in this case I cant define into web.info.
You probably need to create a custom binding extension. See https://blogs.msdn.microsoft.com/carlosfigueira/2011/07/25/wcf-extensibility-binding-and-binding-element-configuration-extensions/. Admittedly this may not be very easy to implement.
I solved it by editing your code as follows :
protected override TransportBindingElement CreateDefaultBindingElement()
{
NonSslAuthHttpTransportBindingElement nnSSL= new NonSslAuthHttpTransportBindingElement(this.KeepAliveEnabled,
this.MaxReceivedMessageSize
, this.MaxBufferPoolSize, this.MaxBufferSize);
return nnSSL;
}
Tony, I ran into this problem a long time ago: https://serverfault.com/questions/80719/howto-wcf-service-https-binding-and-endpoint-configuration-in-iis-with-load-bala
I’d like to update that Stack Overflow question with the correct answer. Another user answered with a link to your post. Since links don’t age well on the internet and that question does get a lot of views would you mind taking a some time and answering there? It would be awesome if you could also post your source to GitHub with a README.md!
Thanks so much for this great info!
Mike G.
I am facing the same issue. Can you please share the github link of this solution as majority of link seems not working.
hi ,
sorry but link download it is broken can y upload again source code.
tks a lot
Yes sorry. I will see if I can locate.