.netazuresdkazure-web-app-service

How to bind a Managed Certificate using the Azure SDK?



Note: the answer may be in either VB.NET or C#. I have no preference for this Q&A.


How do we go about binding a Managed Certificate to a Custom Domain using the new SDK?

Unfortunately, the samples don't cover this task. (In fact, they don't compile, with hundreds of broken references, so it's impossible to tell whether they're even accurate.)

And the documentation isn't very helpful, either. There's this guidance, and this, but those appear to be for purchased certificates. Nothing in that namespace mentions a Managed Certificate.

Here's the problem.

I've been able to successfully add a Custom Domain to my Azure App Service, using this code:

Dim tenantId As String = "89C4A752-7028-4F94-BF6D-A5B0AB83A30A"
Dim clientId As String = "AC4E5551-B056-4769-84AD-F7016E289122"
Dim clientSecret As String = "EJY5du3PVx#o2P3b*B^25t@LoVu8LX2Lgo"
Dim resourceGroupName As String = "group"
Dim webAppName As String = "site"
Dim customDomain As String = "example.com"

' Authenticate and get the client
Dim credential = New ClientSecretCredential(tenantId, clientId, clientSecret)
Dim armClient = New ArmClient(credential)

' Get the web app
Dim subscription = armClient.GetDefaultSubscriptionAsync.Result
Dim resourceGroup = subscription.GetResourceGroups.Get(resourceGroupName)
Dim webApp = resourceGroup.Value.GetWebSites.Get(webAppName)

' Set the domain properties
Dim domainProperties = New HostNameBindingData With {
  .CustomHostNameDnsRecordType = CustomHostNameDnsRecordType.A,
  .HostNameType = AppServiceHostNameType.Managed
}

Me.UpdateDns(webApp)

Dim op = webApp.Value.GetSiteHostNameBindings.CreateOrUpdate(Azure.WaitUntil.Completed, customDomain, domainProperties)

That works. The domain is added. But it's not bound to anything.

Adding a binding to a Managed Certificate is another matter entirely. I tried setting the .SslState property, like so:

Dim domainProperties = New HostNameBindingData With {
  .CustomHostNameDnsRecordType = CustomHostNameDnsRecordType.CName,
  .HostNameType = AppServiceHostNameType.Managed,
  .SslState = HostNameBindingSslState.SniEnabled
}

...but that results in an error:

Parameter Thumbprint is null or empty.

There is a .ThumbprintString property on the HostNameBindingData class, but where do we get that value from?

The repo referenced in this answer almost gets there, but it's nine years old and we're on a completely revamped SDK by now. Besides, he's uploading a .PFX, which is something completely different.

How do I create a new Managed Certificate and bind it to my newly added Custom Domain?

--EDIT--

In fact, they don't compile, with hundreds of broken references, so it's impossible to tell whether they're even accurate.

I got the source to build; it was a lot easier than I'd expected. All it needed was installation of the specific .NET SDK version indicated in the Global.json file in the repo root.

Oh... and an appropriate Package Source Mapping entry, assuming that's in use.


Solution

  • I finally managed to get this worked out, thanks to a very helpful soul over at the SDK repo.

    After some refinement, here's the code I ended up with (below). Note that the DNS functions are performed using the DnsClient and CloudflareClient packages.

    And without further ado, here's the code:

    Private Async Function UpdateCreateSecureBind() As Task
      Dim oResourceGroup As Response(Of ResourceGroupResource)
      Dim oSubscription As SubscriptionResource
      Dim oWebSite As Response(Of WebSiteResource)
      Dim sHostName As String
    
      oSubscription = Await ArmClient.GetDefaultSubscriptionAsync
      oResourceGroup = Await oSubscription.GetResourceGroups.GetAsync(RESOURCE_GROUP_NAME)
      sHostName = "example.com"
      oWebSite = oResourceGroup.Value.GetWebSites.Get(APP_SERVICE_NAME)
    
      Await UpdateDnsAsync(sHostName, oWebSite.Value.Data.CustomDomainVerificationId)
      Await CreateDomainAsync(sHostName)
      Await CreateCertificateAsync(sHostName)
      Await BindCertificateAsync(sHostName)
    End Function
    
    Private Async Function UpdateDnsAsync(HostName As String, VerificationId As String) As Task
      Dim oCloudflare As Intexx.Cloudflare.ICloudflare
      Dim oDnsRecord As Result(Of DnsRecord)
      Dim oDnsClient As IDnsClient
      Dim oFqdn As Result(Of String)
    
      oDnsClient = New Dns.DnsClient
      oCloudflare = New Intexx.Cloudflare.Cloudflare(HostName, My.Resources.Cloudflare.Token)
      oFqdn = Await oDnsClient.ResolveFqdnAsync(APP_SERVICE_DOMAIN)
    
      oDnsRecord = Await oCloudflare.AddOrUpdateRecordAsync("@", DnsRecordType.A, oFqdn.Value)
    
      Do While Not (Await oDnsClient.ValidateRecordAsync(oDnsRecord.Value.Name, oDnsRecord.Value.Content, QueryType.A)).Value
        Await Task.Delay(1000)
      Loop
    
      oDnsRecord = Await oCloudflare.AddOrUpdateRecordAsync($"asuid.{HostName}", DnsRecordType.Txt, VerificationId)
    
      Do While Not (Await oDnsClient.ValidateRecordAsync(oDnsRecord.Value.Name, oDnsRecord.Value.Content, QueryType.TXT)).Value
        Await Task.Delay(1000)
      Loop
    End Function
    
    Private Async Function CreateDomainAsync(HostName As String) As Task
      Dim oResourceGroup As Response(Of ResourceGroupResource)
      Dim oSubscription As SubscriptionResource
      Dim oBindingData As HostNameBindingData
      Dim oOperation As ArmOperation(Of SiteHostNameBindingResource)
      Dim oBindings As SiteHostNameBindingCollection
      Dim oWebSite As Response(Of WebSiteResource)
    
      oSubscription = Await ArmClient.GetDefaultSubscriptionAsync
      oResourceGroup = Await oSubscription.GetResourceGroups.GetAsync(RESOURCE_GROUP_NAME)
      oWebSite = oResourceGroup.Value.GetWebSites.Get(APP_SERVICE_NAME)
      oBindings = oWebSite.Value.GetSiteHostNameBindings
    
      oBindingData = New HostNameBindingData With {
        .CustomHostNameDnsRecordType = CustomHostNameDnsRecordType.A,
        .HostNameType = AppServiceHostNameType.Verified
      }
    
      oOperation = Await oBindings.CreateOrUpdateAsync(WaitUntil.Completed, HostName, oBindingData)
    End Function
    
    Private Async Function CreateCertificateAsync(HostName As String) As Task
      Dim oCertificateData As AppCertificateData
      Dim oResourceGroup As Response(Of ResourceGroupResource)
      Dim oCertificates As AppCertificateCollection
      Dim oSubscription As SubscriptionResource
      Dim oOperation As ArmOperation(Of AppCertificateResource)
      Dim oLocation As AzureLocation
      Dim oWebSite As Response(Of WebSiteResource)
    
      oSubscription = Await ArmClient.GetDefaultSubscriptionAsync
      oResourceGroup = Await oSubscription.GetResourceGroups.GetAsync(RESOURCE_GROUP_NAME)
      oCertificates = oResourceGroup.Value.GetAppCertificates
      oLocation = New AzureLocation("East US")
      oWebSite = oResourceGroup.Value.GetWebSites.Get(APP_SERVICE_NAME)
    
      oCertificateData = New AppCertificateData(oLocation) With {
        .CanonicalName = HostName,
        .ServerFarmId = oWebSite.Value.Data.AppServicePlanId
      }
      oCertificateData.HostNames.Add(HostName)
    
      oOperation = Await oCertificates.CreateOrUpdateAsync(WaitUntil.Completed, HostName, oCertificateData)
    End Function
    
    Private Async Function BindCertificateAsync(HostName As String) As Task
      Dim oAppCertificate As AppCertificateResource
      Dim oResourceGroup As Response(Of ResourceGroupResource)
      Dim oSubscription As SubscriptionResource
      Dim oSslStates As IList(Of HostNameSslState)
      Dim oSslState As HostNameSslState
      Dim oWebSite As Response(Of WebSiteResource)
      Dim oPatch As SitePatchInfo
    
      oSubscription = Await ArmClient.GetDefaultSubscriptionAsync
      oResourceGroup = Await oSubscription.GetResourceGroups.GetAsync(RESOURCE_GROUP_NAME)
      oAppCertificate = Await oResourceGroup.Value.GetAppCertificates.GetAsync(HostName)
      oWebSite = oResourceGroup.Value.GetWebSites.Get(APP_SERVICE_NAME)
      oSslStates = oWebSite.Value.Data.HostNameSslStates
      oSslState = oSslStates.FirstOrDefault(Function(SslState) SslState.Name.Equals(HostName, StringComparison.OrdinalIgnoreCase))
      oPatch = New SitePatchInfo
    
      oSslState.ThumbprintString = oAppCertificate.Data.ThumbprintString
      oSslState.SslState = HostNameBindingSslState.SniEnabled
      oSslState.ToUpdate = True
      oPatch.HostNameSslStates.Add(oSslState)
    
      Await oWebSite.Value.UpdateAsync(oPatch)
    End Function
    

    This works.