Dec 042010
 

An ACE (Application Control Engine) Appliance is commonly referred to as simply being a load balancer. An ACE appliance provides many more features that are commonly employed. One of the most desirable features is SSL off-loading. When properly configured for offloading, the ACE will serve as a load balancer, and also take on the responsibility of processing all SSL requests made to a web server farm. In this configuration web farm server’s IIS does not need to worry about SSL at all. SSL traffic ends at the ACE, and only unencrypted HTTP traffic exists between the ACE and IIS. This is desirable because it provides a hardware solution to SSL processing and alleviates some load and configuration on the web servers and IIS.

While these ACE features are wonderful, it creates a little headache for WCF servers hosted on IIS behind the ACE. The problem shows itself as one of the following error messages as seen on the client end:

System.ArgumentException: The provided URI scheme ‘http’ is invalid; expected ‘https’. Parameter name: via

System.ServiceModel.ServiceActivationException: The requested service, ‘https://sub.mydomain.com/MyService.svc’ could not be activated. See the server’s diagnostic trace logs for more information.

System.ServiceModel.ProtocolException: The content type text/html; charset=utf-8 of the response message does not match the content type of the binding (application/soap+msbin1). If using a custom encoder, be sure that the IsContentTypeSupported method is implemented properly.

System.Net.WebException: The remote server returned an error: (500) Internal Server Error.

The clients have to call the service with a URL similar to https://sub.mydomain.com/MyService.svc. The ACE handles the SSL, converts the request to an unencrypted HTTP request and passes it onto the web server. When the request reaches IIS and the WCF dispatcher, a service cannot be found that matches the HTTPS To address in the header because only HTTP service endpoints are being exposed. To make it work, you need to trick WCF into thinking its HTTP services can support transport security without actual transport or message protection. You also have to configure the service to listen on one address (HTTP) and respond to requests from another (HTTPS).

Enter the professionals. Michele Leroux Bustamante, of IDesign, wrote a few important articles in DevProConnections in late 2008 (links at the bottom of this post) that discuss this in detail and provide a solution. I have modified her solution slightly to provide a slightly easier configuration experience.

AssertEncryptionHttpTransportElement.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.ServiceModel.Configuration;
using System.ServiceModel.Channels;

namespace WcfExtensions
{
    public class AssertEncryptionHttpTransportElement : HttpTransportElement
    {
        private bool m_RequireClientCertificate = false;
        ConfigurationPropertyCollection m_PropCol = null;

        public override Type BindingElementType
        {
            get
            {
                return typeof(AssertEncryptionHttpTransportBindingElement);
            }
        }

        protected override TransportBindingElement CreateDefaultBindingElement()
        {
            AssertEncryptionHttpTransportBindingElement retval =
                new AssertEncryptionHttpTransportBindingElement(m_RequireClientCertificate);
            return retval;
        }

        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                if (m_PropCol == null)
                {
                    m_PropCol = base.Properties;
                    m_PropCol.Add(
			new ConfigurationProperty("requireClientCertificate",
				typeof(bool), false, null, null, ConfigurationPropertyOptions.None));
                }
                return m_PropCol;
            }
        }

        protected override BindingElement CreateBindingElement()
        {
            return new AssertEncryptionHttpTransportBindingElement(this.RequireClientCertificate);
        }

        [ConfigurationProperty("requireClientCertificate", DefaultValue = false)]
        public bool RequireClientCertificate
        {
            get
            {
                bool retval = (bool)base["requireClientCertificate"];
                return retval;
            }
        }
    }
}

AssertEncryptionHttpTransportBindingElement.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Xml;
using System.Net.Security;

namespace WcfExtensions
{
    public class AssertEncryptionHttpTransportBindingElement : HttpTransportBindingElement, ITransportTokenAssertionProvider, IPolicyExportExtension
    {
        private bool m_RequireClientCertificate = false;

        public AssertEncryptionHttpTransportBindingElement() : base() { }

        public AssertEncryptionHttpTransportBindingElement(bool requireClientCertificate)
            : base()
        {
            m_RequireClientCertificate = requireClientCertificate;
        }

        public override BindingElement Clone()
        {
            AssertEncryptionHttpTransportBindingElement retval = new AssertEncryptionHttpTransportBindingElement(m_RequireClientCertificate);
            return retval;
        }

        public override T GetProperty<T>(BindingContext context)
        {
            if (typeof(T) == typeof(ISecurityCapabilities))
            {
                AssertEncryptionSecurityCapabilities retval = new AssertEncryptionSecurityCapabilities();
                return (T)(object)retval;
            }
            else
                return base.GetProperty<T>(context);
        }

        #region ITransportTokenAssertionProvider Members

        public XmlElement GetTransportTokenAssertion()
        {
            string secpolNS0 = "http://schemas.xmlsoap.org/ws/2005/07/securitypolicy";
            //string secpolNS = "http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702";
            XmlDocument xmldoc = new XmlDocument();
            XmlElement TransTokAssert = xmldoc.CreateElement("sp", "HttpsToken", secpolNS0);
            TransTokAssert.SetAttribute("RequireClientCertificate", m_RequireClientCertificate.ToString().ToLower());

            return TransTokAssert;
        }

        #endregion

        public bool RequireClientCertificate
        {
            get
            {
                return this.m_RequireClientCertificate;
            }
            set
            {
                this.m_RequireClientCertificate = value;
            }
        }

        #region IPolicyExportExtension Members

        void IPolicyExportExtension.ExportPolicy(MetadataExporter exporter, PolicyConversionContext context)
        {
            HttpsTransportBindingElement httpsTBE = new HttpsTransportBindingElement();
            httpsTBE.RequireClientCertificate = m_RequireClientCertificate;
            ((IPolicyExportExtension)httpsTBE).ExportPolicy(exporter, context);
        }

        #endregion
    }
}

AssertEncryptionSecurityCapabilities.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Channels;
using System.Net.Security;

namespace WcfExtensions
{
    internal class AssertEncryptionSecurityCapabilities : ISecurityCapabilities
    {
        #region ISecurityCapabilities Members

        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; }
        }

        #endregion
    }
}

Hosting application configuration file:

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />

    <extensions>
      <bindingElementExtensions>
        <add name="assertEncryptionHttpTransportElement"
                type="WcfExtensions.AssertEncryptionHttpTransportElement, WcfExtensions" />
      </bindingElementExtensions>
    </extensions>

    <bindings>
      <customBinding>
        <binding name="MyService.customBinding0">
            <binaryMessageEncoding/>

            <!-- Replaces httpsTransport -->
            <assertEncryptionHttpTransportElement/>
        </binding>
      </customBinding>
    </bindings>

    <services>
      <service name="MyServices.MyService">
        <endpoint address="https://sub.mydomain.com/MyService.svc"
                  listenUri="http://myserver.mydomain.local/MyService.svc"
                  binding="customBinding"
                  bindingConfiguration="MyService.customBinding0"
                  contract="MyServices.IMyService" />
      </service>
    </services>
</system.serviceModel>

 

The server name in the “listenUri” must appear exactly as it does on the “You have created a service” page. This is typically the internal name of the server. The “address” must appear as the clients call the service (at the load balancer). The result of this configuration change is WCF responding to an unencrypted request sent to an HTTPS address while listening on an HTTP address. You can read about the internals of this solution in more detail in Michele’s articles referenced below.

You can download the project that compiles to give you the WcfExtensions DLL referenced above.

Resources:

  One Response to “WCF Service Behind a SSL-Offloading ACE Load Balancer”

  1. Thanks so much this is very useful. Is there any code to change schemalocation from Http to Https in the WSDL?

 Leave a Reply

(required)

(required)


8 − four =

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>