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.
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.