Programmatically upload and configure a SSL certificate for an Azure Cloud Service Deployment

Our team has recently been working on the Service Gateway project- its available in the Microsoft Azure Web Sites gallery so you can try it out!

Gallery

Anyhow back to the post. One of the recent features I implemented was HTTPS support for the actual Gateway Cloud Service project deployment. Now for a typical Cloud Service deployment simply configuring your cloud project in Visual Studio to add a HTTPS endpoint on 443 then choosing a certificate is adequate and easy. In the case of the Service Gateway Management Console we are actually providing a web portal experience that allows end users to provision a Cloud Service in template form and in this case each user will want to provide their own certificates for their own gateway domain. To do that required a little bit of work using the Microsoft Azure Management Libraries, a certificate *.pfx + password provided by the user and a template *.cspkg and *.cscfg.  This blog will share the key code snippets you will require, of course the full code is available on http://sg.codeplex.com

Create a cloud service project template

HttpsCloudProject

Configure a https endpoint and port binding


HttpsEndpoint Set the certificate thumbprint to 0’s, we will later replace this templated value with the uploaded certificate

<?xml version="1.0" encoding="UTF-8"?>
<ServiceConfiguration xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" serviceName="CloudProjectHttps" osFamily="3" osVersion="*" schemaVersion="2013-10.2.2">
   <Role name="Router">
      <Instances count="2" />
      <ConfigurationSettings>
         <Setting name="ConfigLocation" value="https://configtest.blob.core.windows.net/defaultconfig/v2/index.json" />
         <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="DefaultEndpointsProtocol=https;AccountName=your-account;AccountKey=your-key;" />
      </ConfigurationSettings>
      <Certificates>
         <Certificate name="CertificateName" thumbprint="0000000000000000000000000000000000000000" thumbprintAlgorithm="sha1" />
      </Certificates>
   </Role>
</ServiceConfiguration>

Implement code to retrieve the certificate thumbprint to be used in the *.cscfg and then upload the certificate. In the code listing below the deployment certificate is of type HttpPostedFileBase i.e file uploaded through an MVC app. For full code of the deployment class refer to the previously shared codeplex link

.....
   if(success && deployment.IsHttpsEnabled) 
   { 
       byte[] rawCert = await GetCertRawBytes(deployment.Certificate); 
       deployment.CertificateThumbprint = GetThumbprint(rawCert, deployment.CertificatePassword); 
       var uploadCertStatus = await UploadCertificateAsync(deployment.ServiceName, rawCert, deployment.CertificatePassword); 
       success &= uploadCertStatus.Success; await UpdateDeploymentProgressAsync(deployment, uploadCertStatus); 
   } 

.....
            
private async Task<byte[]> GetCertRawBytes(HttpPostedFileBase certificate) 
{ 
    byte[] rawCert; 
    using (var stream = certificate.InputStream) 
    { 
        rawCert = new byte[stream.Length]; 
        await stream.ReadAsync(rawCert, 0, rawCert.Length); 
    } 
    return rawCert; 
}

note the key store flags below are required to use X509Certificate2 on Microsoft Azure web sites


private string GetThumbprint(byte[] rawCert, string certPassword) 
{ 
   var x509 = new X509Certificate2(rawCert, certPassword, X509KeyStorageFlags.MachineKeySet); 
   return x509.Thumbprint; 
} 

To upload the actual certificate for the service you want to deploy you can use ComputeManagementClient from WAML. Note that its disposable so if your just newing it up in your method you should use it within a using. In the code below the private member variable is disposed by the enclosing class implementing IDisposable.

private async Task UploadCertificateAsync(string serviceName, byte[] rawCert, string certPassword = null) 
{ 
   var response = await _computeManagementClient.Value.ServiceCertificates.CreateAsync(serviceName, new ServiceCertificateCreateParameters() 
      { 
         CertificateFormat = CertificateFormat.Pfx, 
         Data = rawCert, 
         Password = certPassword.ToString() 
      }); 
} 

There is a lot going on in the following method, the key thing to note is that deployment.ServiceConfigTemplateLocation is just a url to a public blob that is the *.cscfg from above. i.e download it, replace all the template values

private async Task ProvisionCloudServiceAsync(Deployment deployment) 
{ 
   var status = new DeployStatus(); 
   status.Success = true; 

   using (var config = await _computeManagementClient.Value.HttpClient.GetAsync(deployment.ServiceConfigTemplateLocation)) 
   { 
      if (config.IsSuccessStatusCode) 
      { 
         using (var configStream = await config.Content.ReadAsStreamAsync()) 
         { 
            //Update the service configuration *.cscfg for the specifics for this deployment 
            var document = XDocument.Load(configStream); 
            var svcConfig = document.Root; 
            var ns = (XNamespace)svcConfig.Attribute("xmlns").Value; 
            var instances = svcConfig.Element(ns + "Role").Element(ns + "Instances"); 
            var settings = svcConfig.Element(ns + "Role").Elements(ns + "ConfigurationSettings"); 
            var configLocation = settings.Descendants().First(x => x.Attribute("name").Value == "ConfigLocation"); 
            var diagnosticsConnection = settings.Descendants().First(x => x.Attribute("name").Value == "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString"); 
            
            instances.SetAttributeValue("count", deployment.InstanceCount); 
            configLocation.SetAttributeValue("value", deployment.RoleIndexConfigLocation); 
            
            var storageAccountConnection = GetStorageAccountConnectionString(deployment.SelectedStorageAccount, deployment.StorageAccountKey); 
            diagnosticsConnection.SetAttributeValue("value", storageAccountConnection); 

            //upload to storage account 
            var package = await CopyDeploymentPackageAsync(deployment, CloudStorageAccount.Parse(storageAccountConnection), "deployments", _maxCopyRetries, _copyRetrySleepPeriod);
            deployment.ServicePackageFileLocation = package.Uri.ToString(); 

            if(deployment.IsHttpsEnabled) 
            { 
               var certificates = svcConfig.Element(ns + "Role").Elements(ns + "Certificates"); 
               var thumprintSetting = certificates.Descendants().First(x => x.Attribute("name").Value == "CertificateName"); 
               thumprintSetting.SetAttributeValue("thumbprint", deployment.CertificateThumbprint); 
            } 

            try 
            { 
               await UpdateDeploymentProgressAsync(deployment, new DeployStatus(string.Format("Deploying Cloud Service '{0}'", deployment.ServiceName))); 
               
               //deploy the cloud service into the provisioned slot using the uploaded *.cspkg and *.cscfg 
               var response = await _computeManagementClient.Value.Deployments.CreateAsync(deployment.ServiceName, DeploymentSlot.Production, new DeploymentCreateParameters 
                              { 
                                 Label = deployment.ServiceName, 
                                 Name = deployment.ServiceName, 
                                 PackageUri = package.Uri, 
                                 Configuration = document.ToString(), 
                                 StartDeployment = true, 
                              });
               status.Success = response.StatusCode == System.Net.HttpStatusCode.OK && response.Error == null; 
               
               if(!status.Success) 
               { 
                  status.Message = string.Format("Error Deploying Cloud Service '{0}', \r\n Response Code {1} \r\n Error Code: {2} \r\n Error Message: {3} ", deployment.ServiceName, response.StatusCode, response.Error.Code, response.Error.Message); 
               } 
            } 
            catch (Exception ex) 
            { 
               Debug.WriteLine(ex.ToString()); status.Success = false; status.Message = string.Format("Error Deploying Cloud Service '{0}', \r\n Error Message: {1}", deployment.ServiceName, ex.Message);
            } 
         } 
      } 
      else 
      { 
         status.Success = false; status.Message = string.Format("Unable to locate Service Configuration template at {0}, On requesting template response status code is {1}", deployment.ServiceConfigTemplateLocation, config.StatusCode); 
      } 
   } 

   if (status.Success) 
   { 
      status.Message = string.Format("Finished Deployment of Cloud Service '{0}' successfully", deployment.ServiceName); 
   } 
   
   return status; 
} 

That’s about it for uploading a certificate for your cloud service then dynamically updating the service config for the thumbprint of an arbitrary certificate provided by the user. I think it would probably be useful to pull together a detailed post on how the deployment of the cloud service works as well.

To learn more about the Microsoft Azure Management Libraries checkout Microsoft Azure Management Libraries on NuGet and my colleague Brady Gasters Blog

Hope this helps,
Nick Harris