From 489312973cc3f7431a95ee884c166d6ec4e6dca9 Mon Sep 17 00:00:00 2001 From: Sean Donaghy Date: Wed, 23 Jul 2025 16:20:22 +0100 Subject: [PATCH 1/5] For custom domains, support custom certs that are stored in keyVault --- src/Farmer/Arm/Web.fs | 4 + src/Farmer/Builders/Builders.WebApp.fs | 15 +++- src/Farmer/Types.fs | 11 +++ src/Tests/Tests.fsproj | Bin 10796 -> 10248 bytes src/Tests/WebApp.fs | 106 +++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 3 deletions(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 2cdd1f7ac..4b2867ea0 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -603,6 +603,8 @@ type Certificate = SiteId: LinkedResource ServicePlanId: LinkedResource DomainName: string + KeyVaultId: string option + KeyVaultSecretName: string option } member this.ResourceName = ResourceName this.DomainName @@ -637,6 +639,8 @@ type Certificate = {| serverFarmId = this.ServicePlanId.ResourceId.Eval() canonicalName = this.DomainName + keyVaultId = this.KeyVaultId + keyVaultSecretName = this.KeyVaultSecretName |} |} diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 11f1b6534..6d5ff4ace 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -914,13 +914,20 @@ type WebAppConfig = match customDomain with | SecureDomain (customDomain, certOptions) -> - let cert = - { + let cert = { Location = location SiteId = Managed this.ResourceId ServicePlanId = Managed this.ServicePlanId DomainName = customDomain - } + KeyVaultId = + match certOptions with + | CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultCertificate.keyVaultId) + | _ -> None + KeyVaultSecretName = + match certOptions with + | CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultCertificate.keyVaultSecretName) + | _ -> None + } // Get the resource group which contains the app service plan let aspRgName = @@ -963,6 +970,8 @@ type WebAppConfig = match certOptions with | AppManagedCertificate -> Some(SniBased(cert.GetThumbprintReference aspRgName)) + | CustomCertificateFromKeyVault kvCert -> + Some(SniBased(kvCert.thumbprint)) | CustomCertificate thumbprint -> Some(SniBased thumbprint) } diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index 4d5361eb9..3e56ca417 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -365,10 +365,21 @@ type ResourceRef<'TConfig> = | LinkedResource r -> r | AutoGeneratedResource r -> Managed(r.resourceId config) +type KeyVaultCertificate = { + keyVaultId: string + keyVaultSecretName: string +} + +type KeyVaultCustomCertificateOptions = { + thumbprint: ArmExpression + keyVaultCertificate: KeyVaultCertificate +} + //Choose whether you'd like an auto generated app service managed certificate or if you have your own custom certificate of type CertificateOptions = | AppManagedCertificate | CustomCertificate of thumbprint: ArmExpression + | CustomCertificateFromKeyVault of keyVaultCertificate: KeyVaultCustomCertificateOptions type DomainConfig = | SecureDomain of domain: string * cert: CertificateOptions diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index c2ed1832ff4cfe02409132a88ea1dd496e08a686..8abb982f061c45ffbab4c7a6bc691741b5408b52 100644 GIT binary patch delta 80 zcmZ1z(h getResources + + let wa = resources |> getResource |> List.head + let nested = resources |> getResource + let expectedDomainName = "customDomain.io" + + // Testing HostnameBinding + let hostnameBinding = + nested.[0].Resources |> getResource |> List.head + + let expectedSslState = None + let exepectedSiteId = (Managed(Arm.Web.sites.resourceId wa.Name)) + + Expect.equal + hostnameBinding.DomainName + expectedDomainName + $"HostnameBinding domain name should have {expectedDomainName}" + + Expect.equal + hostnameBinding.SslState + expectedSslState + $"HostnameBinding should have a {expectedSslState} Ssl state" + + Expect.equal + hostnameBinding.SiteId + exepectedSiteId + $"HostnameBinding SiteId should be {exepectedSiteId}" + + // Testing certificate + let cert = nested.[1].Resources |> getResource |> List.head + + Expect.equal + cert.KeyVaultId.Value + keyVaultId + $"Certificate should be in KeyVault {keyVaultId}" + + Expect.equal + cert.KeyVaultSecretName.Value + keyVaultSecretName + $"Certificate should be in KeyVault secret {keyVaultSecretName}" + + // Testing certificate + let cert = nested.[1].Resources |> getResource |> List.head + + Expect.equal + cert.DomainName + expectedDomainName + $"Certificate domain name should have {expectedDomainName}" + + // Testing hostname/certificate link. + let bindingDeployment = nested.[2] + + let innerResource = + bindingDeployment.Resources |> getResource |> List.head + + let innerExpectedSslState = Some(SslState.SniBased thumbprint) + + Expect.stringStarts + bindingDeployment.DeploymentName.Value + "[concat" + "resourceGroupDeployment name should start as a valid ARM expression" + + Expect.stringEnds + bindingDeployment.DeploymentName.Value + ")]" + "resourceGroupDeployment stage should end as a valid ARM expression" + + Expect.equal + bindingDeployment.Resources.Length + 1 + "resourceGroupDeployment stage should only contain one resource" + + Expect.equal + bindingDeployment.Dependencies.Count + 1 + "resourceGroupDeployment stage should only contain one dependencies" + + Expect.equal + innerResource.SslState + innerExpectedSslState + $"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template" + } + test "Supports secure custom domains with app service managed certificate" { let webappName = "test" From 5481f2b4bd3959b6cce99f19ffd822a1af92fdda Mon Sep 17 00:00:00 2001 From: Blake Date: Wed, 23 Jul 2025 17:48:00 +0100 Subject: [PATCH 2/5] rm expecto --- src/Tests/Tests.fsproj | Bin 10248 -> 10796 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index 8abb982f061c45ffbab4c7a6bc691741b5408b52..c2ed1832ff4cfe02409132a88ea1dd496e08a686 100644 GIT binary patch delta 132 zcmeAOSQEnZ|KG-dJW<|Mh6;uPhE#@Rh7yMS$%*3PjFyw@rGh8Raj{uKMH%!M!Wc3c ziWy275*czBf`KYa8B%~E`9PTvpx$Djh$B!W5ol5gkY5Bcht&coYcN?-ReCa$n%`tS U)pL^r)G{VdkT%==MN~ux0QmSJw*UYD delta 80 zcmZ1z(h Date: Thu, 24 Jul 2025 11:22:05 +0100 Subject: [PATCH 3/5] fix keyvault certificates --- src/Farmer/Arm/Web.fs | 6 ++++-- src/Farmer/Builders/Builders.WebApp.fs | 6 +++--- src/Farmer/Types.fs | 7 +------ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Farmer/Arm/Web.fs b/src/Farmer/Arm/Web.fs index 4b2867ea0..4648fa1a3 100644 --- a/src/Farmer/Arm/Web.fs +++ b/src/Farmer/Arm/Web.fs @@ -607,7 +607,9 @@ type Certificate = KeyVaultSecretName: string option } - member this.ResourceName = ResourceName this.DomainName + // Use the domain name for the certificate resource name if this is an azure managed cert, else it needs to be the keyvault secret name to avoid conflicts when using the same keyvault cert across multiple apps. + // This hack ensures only one cert is created per resource group when using a keyvault certificate, even if you have multiple farmer web apps. + member this.ResourceName = if this.KeyVaultSecretName.IsSome then ResourceName this.KeyVaultSecretName.Value else ResourceName this.DomainName member this.Thumbprint = this.GetThumbprintReference None member this.GetThumbprintReference certificateResourceGroup = @@ -638,7 +640,7 @@ type Certificate = properties = {| serverFarmId = this.ServicePlanId.ResourceId.Eval() - canonicalName = this.DomainName + canonicalName = if this.KeyVaultId.IsSome then None else Some(this.DomainName) keyVaultId = this.KeyVaultId keyVaultSecretName = this.KeyVaultSecretName |} diff --git a/src/Farmer/Builders/Builders.WebApp.fs b/src/Farmer/Builders/Builders.WebApp.fs index 6d5ff4ace..a6650ed6f 100644 --- a/src/Farmer/Builders/Builders.WebApp.fs +++ b/src/Farmer/Builders/Builders.WebApp.fs @@ -921,11 +921,11 @@ type WebAppConfig = DomainName = customDomain KeyVaultId = match certOptions with - | CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultCertificate.keyVaultId) + | CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultId) | _ -> None KeyVaultSecretName = match certOptions with - | CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultCertificate.keyVaultSecretName) + | CustomCertificateFromKeyVault kvCert -> Some(kvCert.keyVaultSecretName) | _ -> None } @@ -971,7 +971,7 @@ type WebAppConfig = | AppManagedCertificate -> Some(SniBased(cert.GetThumbprintReference aspRgName)) | CustomCertificateFromKeyVault kvCert -> - Some(SniBased(kvCert.thumbprint)) + Some(SniBased(cert.GetThumbprintReference aspRgName)) | CustomCertificate thumbprint -> Some(SniBased thumbprint) } diff --git a/src/Farmer/Types.fs b/src/Farmer/Types.fs index 3e56ca417..8b6353ab8 100644 --- a/src/Farmer/Types.fs +++ b/src/Farmer/Types.fs @@ -369,17 +369,12 @@ type KeyVaultCertificate = { keyVaultId: string keyVaultSecretName: string } - -type KeyVaultCustomCertificateOptions = { - thumbprint: ArmExpression - keyVaultCertificate: KeyVaultCertificate -} //Choose whether you'd like an auto generated app service managed certificate or if you have your own custom certificate of type CertificateOptions = | AppManagedCertificate | CustomCertificate of thumbprint: ArmExpression - | CustomCertificateFromKeyVault of keyVaultCertificate: KeyVaultCustomCertificateOptions + | CustomCertificateFromKeyVault of keyVaultCertificate: KeyVaultCertificate type DomainConfig = | SecureDomain of domain: string * cert: CertificateOptions From 80c38622da0be564fb5278f9d7457febbd872b1d Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 24 Jul 2025 11:55:31 +0100 Subject: [PATCH 4/5] Fix tests --- src/Tests/WebApp.fs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/Tests/WebApp.fs b/src/Tests/WebApp.fs index a6caf2aa3..0fe218493 100644 --- a/src/Tests/WebApp.fs +++ b/src/Tests/WebApp.fs @@ -1230,21 +1230,16 @@ let tests = test "Supports secure custom domains with custom certificate from Key Vault" { let webappName = "test" - let thumbprint = ArmExpression.literal "1111583E8FABEF4C0BEF694CBC41C28FB81CD111" let keyVaultId = "keyVaultId" let keyVaultSecretName = "keyVaultSecretName" - let keyVaultCustomCertificateOptions: KeyVaultCustomCertificateOptions - = { - thumbprint = thumbprint - keyVaultCertificate = { - keyVaultId = keyVaultId - keyVaultSecretName = keyVaultSecretName - } - } + let keyVaultCertificate: KeyVaultCertificate = { + keyVaultId = keyVaultId + keyVaultSecretName = keyVaultSecretName + } - let domainConfig = SecureDomain("customDomain.io", CustomCertificateFromKeyVault keyVaultCustomCertificateOptions) + let domainConfig = SecureDomain("customDomain.io", CustomCertificateFromKeyVault keyVaultCertificate) let resources = webApp { @@ -1306,8 +1301,6 @@ let tests = let innerResource = bindingDeployment.Resources |> getResource |> List.head - let innerExpectedSslState = Some(SslState.SniBased thumbprint) - Expect.stringStarts bindingDeployment.DeploymentName.Value "[concat" @@ -1327,11 +1320,6 @@ let tests = bindingDeployment.Dependencies.Count 1 "resourceGroupDeployment stage should only contain one dependencies" - - Expect.equal - innerResource.SslState - innerExpectedSslState - $"hostnameBinding should have a {innerExpectedSslState} Ssl state inside the resourceGroupDeployment template" } test "Supports secure custom domains with app service managed certificate" { From 657b33e43a9392306e5f2a4437ab2ab87ce83d0d Mon Sep 17 00:00:00 2001 From: Blake Date: Thu, 24 Jul 2025 11:56:23 +0100 Subject: [PATCH 5/5] bump version --- src/Farmer/Farmer.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 725f4cd0d..77d124010 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -2,7 +2,7 @@ Farmer - 1.6.2 + 1.6.3 Farmer makes authoring ARM templates easy! Copyright 2019-2022 Compositional IT Ltd. Compositional IT