Expose SSL service to multi domains from the same cloud service

When designing and deploying web services to Azure with Azure cloud service, it is very common to apply SSL to secure web access and protect the data transfer, regarding how to enable SSL in cloud service and make custom domain work instead of default cloudapp.net, here are some good tutorials:

       1. https://azure.microsoft.com/en-us/documentation/articles/cloud-services-configure-ssl-certificate/

       2. https://blogs.msdn.com/b/avkashchauhan/archive/2011/03/28/complete-solution-adding-ssl-certificate-with-windows-azure-application.aspx

in this implementation, if your certificate is not in pfx format but in others like p7b, you can follow this article https://stackoverflow.com/questions/6708223/convert-p7b-to-pfx-for-azure to get the right pfx formated certificate to move with https on Azure.

if you certificate is pfx, but after you upload it to the portal, you notice 2 or more certificates added there, you can check the solution here to fix: https://blog.michaelchi.net/2014/12/cloud-servicechained-certificatessl.html

in this post, i am summarying a resolved issue in my job, which was to enable multi SSL in the same cloud service. (a special thanks to John Ellis, whom I worked on this topic.  )

for exampel, the cloud service is https://test.cloudapp.net and is listening at port 80 and 443, now the goal is to set the cloud service to be accessible at https://www.test1.com and https://www.test2.com

 

>Analysis:

In lower version of IIS, such as IIS 6 and IIS 7, it is not supported to enable multi SSL bindings for a website.

Until IIS 7.5, the major limitation of IIS is that IIS will allow you to bind only one site for one IP: Port combination using an SSL certificate. If you try to bind a second site on the IP address to the same certificate, IIS will give you an error when starting the site up stating that there is a conflict.

To know why it doesn’t work in HTTPS lets understand the SSL handshake briefly.

1. Client - > (SSL Handshake) - > “ Hello, I support XYZ algorithm for encryption”

2. Server -> (SSL Handshake) -> “Hi there, Okay so here is my public certificate. Let’s use algorithm X”

3. Client -> (SSL Handshake)-> “Great we can use that”

4. Client -> (In Encrypted format)-> “HTTP Request”

5. Server -> (In Encrypted format)-> “HTTP Response”

<And now cycle continues>

Now let’s say hypothetically, you have set two sites on same IP-address and port and different host headers and you set two different certificates on both of them.

Look at the steps in SSL Handshake, Client sends the HTTP Request only in Step 4. That means Server doesn’t know what host header HTTP request is referring to until step 4. So at Step 2, Server has only IP-Address and Port information with it, so how can server figure out which certificate it needs to send to the Client as you have bind two certificates to same IP-Address and port.

When a request comes to HTTP.SYS layer, the HTTP.SYS reads the site configuration, including the certificate used to encrypt/decrypt the data. The host name is encrypted in SSL Blob that the client sends. However, IIS needs to know the host name in order to get the right certificate. Without the host name IIS cannot get to the correct site. As IIS is not able to get to the correct site so it cannot get the right certificate to decrypt the SSL blob to get the host name. This is the classic Chicken and Egg problem. We are turning into circles with no way out.

This is the precise reason; HTTP server can only allow one site per IP-Address: Port combination for HTTPS browsing. If you need to bind another site over HTTPS then you need to get either a different IP-Address or bind the site to a different port.

Reference: https://blogs.msdn.com/b/varunm/archive/2013/06/18/bind-multiple-sites-on-same-ip-address-and-port-in-ssl.aspx

 

However, IIS8 has a new functionality called Server Name Indication (SNI) to overcome this problem.

Problem: As more e-commerce sites come on line and more businesses are storing and sharing sensitive documents online, the ability to host and scale secure sites are increasingly more important. Prior to Windows Server 2012, there were a couple of challenges when it comes to hosting secure sites:

  • SSL Scalability: In a multi-tenanted environment, such as a shared hosting, there is a limitation as to how many secure sites can be hosted on Windows Server, resulting in a low site-density.
  • IPv4 scarcity: Because the network end-point can only be identified with IP:Port binding, where tenants request to use the standard SSL port, 443, hosting a secure site often means offering a dedicated IP address per tenant.

Solution: On Windows Server 2012, IIS supports Server Name Indication (SNI), which is a TLS extension to include a virtual domain as a part of SSL negotiation. What this effectively means is that the virtual domain name, or a hostname, can now be used to identify the network end point. In addition, a highly scalable WebHosting store has been created to complement SNI. The result is that the secure site density is much higher on Windows Server 2012 and it is achieved with just one IP address.

 

>How to expose multi SSL services from the same web role

1. Adjust your service configuration file to apply higher version of OS, which has IIS 8 in it. see below, apply osFamily="4" in your service configuration file.

<?xml version="1.0" encoding="utf-8"?>

<ServiceConfiguration serviceName="WindowsAzure1" xmlns="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="4" osVersion="*" schemaVersion="2013-03.2.0">

  <Role name="WebRole1">

    <Instances count="1" />

<ConfigurationSettings>

More details on Azure OS family and its compatibility with SDK: https://msdn.microsoft.com/en-us/library/azure/ee924680.aspx

2. Add certificate into your project and add endpoints as below. (for the certification specified in https endpoint, that can be any one from the 2 certificates as you want).

3. Adjust service definition file with high lightened below.

<?xml version="1.0" encoding="utf-8"?>

<ServiceDefinition name="WindowsAzure1" xmlns="https://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" schemaVersion="2013-03.2.0">

  <WebRole name="WebRole1" vmsize="Small">

    <Runtime executionContext="elevated" />

    <Sites>

      <Site name="Web">

        <Bindings>

          <Binding name="Endpoint1" endpointName="Endpoint1" />

          <!--<Binding name="Endpoint2" endpointName="Endpoint2"/>-->

        </Bindings>

      </Site>

    </Sites>

    <Endpoints>

      <InputEndpoint name="Endpoint1" protocol="http" port="80" />

      <InputEndpoint name="Endpoint2" protocol="https" port="443" certificate="Certificate2" />

    </Endpoints>

    <Imports>

      <Import moduleName="Diagnostics" />

      <Import moduleName="RemoteAccess" />

      <Import moduleName="RemoteForwarder" />

    </Imports>

    <LocalResources>

      <LocalStorage name="DiagnosticStore" sizeInMB="6144" cleanOnRoleRecycle="false"/>

    </LocalResources> 

    <Certificates>

      <Certificate name="Certificate1" storeLocation="LocalMachine" storeName="My" />

      <Certificate name="Certificate2" storeLocation="LocalMachine" storeName="My" />

    </Certificates>

  </WebRole>

</ServiceDefinition>

……

4. Add your target pfx SSL certificates into the web role project and set their property as “copy always”

5. Right click your web project like WebRole1 in my test and install package from Nuget as below

6. Add codes below in your WebRole.cs, and replace the key information with yours.

using System;

using System.Collections.Generic;

using System.Linq;

using Microsoft.WindowsAzure;

using Microsoft.WindowsAzure.Diagnostics;

using Microsoft.WindowsAzure.ServiceRuntime;

using System.Security.Cryptography.X509Certificates;

using Microsoft.Web.Administration;

using System.Net;

using System.Net.Sockets;

using System.Diagnostics;

 

namespace WebRole1

{

    public class WebRole : RoleEntryPoint

    {

        public override bool OnStart()

        {

            // For information on handling configuration changes

            // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357

            System.Threading.Thread.Sleep(40000);

            try

            {

                var newCert = new X509Certificate2("test1.com-1.pfx", "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);

                var readWriteMyStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);

                readWriteMyStore.Open(OpenFlags.ReadWrite);

                readWriteMyStore.Add(newCert);

                readWriteMyStore.Close();

                System.IO.File.AppendAllText("log.txt", "\r\n cert 1 is installed. @" + DateTime.Now.ToString());

 

                var serverManager = new ServerManager();

                var site = serverManager.Sites[0];

                var binding = site.Bindings.Add(":443:www.test1.com", newCert.GetCertHash(), "My");

                binding.SetAttributeValue("sslFlags", 1); //enables the SNI

                serverManager.CommitChanges();

                System.IO.File.AppendAllText("log.txt", "\r\n binding 1 is added. @" + DateTime.Now.ToString());

 

                var newCert2 = new X509Certificate2("test2.com-1.pfx", "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);

                var readWriteMyStore2 = new X509Store(StoreName.My, StoreLocation.LocalMachine);

                readWriteMyStore2.Open(OpenFlags.ReadWrite);

                readWriteMyStore2.Add(newCert2);

                readWriteMyStore2.Close();

                System.IO.File.AppendAllText("log.txt", "\r\n cert 2 is installed. @" + DateTime.Now.ToString());

 

                var serverManager2 = new ServerManager();

                var site2 = serverManager2.Sites[0];

                var binding2 = site2.Bindings.Add(":443:www.test2.com", newCert2.GetCertHash(), "My");

                binding2.SetAttributeValue("sslFlags", 1); //enables the SNI

                serverManager2.CommitChanges();

                System.IO.File.AppendAllText("log.txt", "\r\n binding 2 is added. @" + DateTime.Now.ToString());

 

                try

                {

                    RemoveDefaultIISSSL();

                }

                catch (Exception ee)

                {

                }

            }

            catch (Exception ex)

            {

                System.IO.File.AppendAllText("log.txt", "\r\n " + ex.Message + " @" + DateTime.Now.ToString());

            }

            return base.OnStart();

        }

 

        public bool RemoveDefaultIISSSL()

        {

            System.IO.File.AppendAllText("log.txt", "\r\n start to get local ip. @" + DateTime.Now.ToString());

            IPHostEntry host;

            string localIP = "127.0.0.1";

            host = Dns.GetHostEntry(Dns.GetHostName());

            foreach (IPAddress ip in host.AddressList)

            {

                if (ip.AddressFamily == AddressFamily.InterNetwork)

                {

                    localIP = ip.ToString();

                    break;

                }

            }

 

            string cmd = string.Format("netsh http delete sslcert ipport={0}:443",localIP);

            System.IO.File.AppendAllText("log.txt", "\r\n local ip is got. @" + cmd + " @"+ DateTime.Now.ToString());

            Process p = new Process();

            p.StartInfo.FileName = @"d:\Windows\System32\cmd.exe";

            p.StartInfo.UseShellExecute = false;

            p.StartInfo.RedirectStandardInput = true;

            p.StartInfo.RedirectStandardOutput = false;

            p.StartInfo.RedirectStandardError = true;

            p.StartInfo.CreateNoWindow = true;

            string Parameter = cmd;

            try

            {

                p.Start();

                p.StandardInput.WriteLine(cmd);

                p.StandardInput.WriteLine("exit");

                p.StandardInput.WriteLine("exit");

                p.WaitForExit();

                p.Close();

            }

            catch (Exception e)

            {

                System.IO.File.AppendAllText("log.txt", "\r\n " + e.Message + " @" + DateTime.Now.ToString());

                return false;

            }

            return true;

        }

    }

}

【update in step 6】:

After double checking the issue and deep dive into IIS internal, I noticed a default SSL setting for IIS that impacted multi SSL bindings, so I updated the code as above and have verified it in sample cloud service.

7. Rebuild the project in local emulator. Then deploy it to azure cloud.

8.Verification: after deploying it to cloud, by Remote access, you can confirm the website is configured as below, that worked as expected.

 

                  # localhost name resolution is handled within DNS itself.

                  #             127.0.0.1       localhost

                  #             ::1             localhost

                104.40.95.228 www.test1.com

                104.40.95.228 www.test2.com

More reference:

Appcmd command to add SSL binding: https://toastergremlin.com/?p=308