From 6a5b4a2cf00af1a89a0788460b53a6e4a2679b77 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Fri, 23 Dec 2022 04:00:29 +0000 Subject: [PATCH 1/7] Add capabilities to automatically update App Service certificate bindings --- AzureKeyVault.sln | 6 + AzureKeyVault/AkvProperties.cs | 1 + AzureKeyVault/AzureAppServicesClient.cs | 266 ++++++++++++++++++ AzureKeyVault/AzureClient.cs | 6 + AzureKeyVault/AzureKeyVault.csproj | 23 +- AzureKeyVault/Jobs/AzureKeyVaultJob.cs | 15 +- AzureKeyVault/Jobs/Management.cs | 69 +++-- .../AzureKeyVaultBindingTests.csproj | 22 ++ AzureKeyVaultBindingTests/Program.cs | 173 ++++++++++++ README.md | 22 ++ 10 files changed, 565 insertions(+), 38 deletions(-) create mode 100644 AzureKeyVault/AzureAppServicesClient.cs create mode 100644 AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj create mode 100644 AzureKeyVaultBindingTests/Program.cs diff --git a/AzureKeyVault.sln b/AzureKeyVault.sln index 75246d0..6c0dd0b 100644 --- a/AzureKeyVault.sln +++ b/AzureKeyVault.sln @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution readme_source.md = readme_source.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureKeyVaultBindingTests", "AzureKeyVaultBindingTests\AzureKeyVaultBindingTests.csproj", "{B6024A50-BD43-4D30-8291-AC5D536115BD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {2CEC2ACF-E636-45DA-A0B5-3FC4D9F4EFCA}.Debug|Any CPU.Build.0 = Debug|Any CPU {2CEC2ACF-E636-45DA-A0B5-3FC4D9F4EFCA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2CEC2ACF-E636-45DA-A0B5-3FC4D9F4EFCA}.Release|Any CPU.Build.0 = Release|Any CPU + {B6024A50-BD43-4D30-8291-AC5D536115BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6024A50-BD43-4D30-8291-AC5D536115BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6024A50-BD43-4D30-8291-AC5D536115BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6024A50-BD43-4D30-8291-AC5D536115BD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AzureKeyVault/AkvProperties.cs b/AzureKeyVault/AkvProperties.cs index 2b034b6..d025f6d 100644 --- a/AzureKeyVault/AkvProperties.cs +++ b/AzureKeyVault/AkvProperties.cs @@ -11,5 +11,6 @@ public class AkvProperties public string ApplicationId { get; set; } public string ObjectId { get; set; } public string ClientSecret { get; set; } + public bool AutoUpdateAppServiceBindings { get; set; } } } diff --git a/AzureKeyVault/AzureAppServicesClient.cs b/AzureKeyVault/AzureAppServicesClient.cs new file mode 100644 index 0000000..1d1fa1d --- /dev/null +++ b/AzureKeyVault/AzureAppServicesClient.cs @@ -0,0 +1,266 @@ +using System.Collections.Generic; +using System.Linq; +using Azure; +using Azure.Core; +using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.AppService; +using Azure.ResourceManager.AppService.Models; +using Azure.ResourceManager.Resources; +using Azure.Security.KeyVault.Certificates; +using Keyfactor.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic; +using SiteHostNameBindingResource = Azure.ResourceManager.AppService.SiteHostNameBindingResource; + +namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault +{ + public class AzureAppServicesClient + { + // Environment variables required to authenticate using DefaultAzureCredential: + // - AZURE_CLIENT_ID → The app ID value. + // - AZURE_TENANT_ID → The tenant ID value. + // - AZURE_CLIENT_SECRET → The password/credential generated for the app. + + public AzureAppServicesClient(AkvProperties properties) + { + // Each AzureAppServicesClient represents a single resource group (and therefore subscription). + + Log = LogHandler.GetClassLogger(); + Log.LogDebug("Initializing Azure App Services client"); + + // Build ResourceGroupId using SubscriptionId, and ResourceGroupName from properties + string subscriptionId = string.IsNullOrEmpty(properties.SubscriptionId) ? new ResourceIdentifier(properties.StorePath).SubscriptionId : properties.SubscriptionId; + ResourceGroupId = + new ResourceIdentifier( + $"/subscriptions/{subscriptionId}/resourceGroups/{properties.ResourceGroupName}"); + + // Construct Azure Resource Management client using ClientSecretCredential based on properties inside AkvProperties; + ArmClient = new ArmClient(new ClientSecretCredential(properties.TenantId, properties.ApplicationId, properties.ClientSecret)); + + // Get subscription resource defined by resource ID + Subscription = ArmClient.GetSubscriptions().Get(ResourceGroupId.SubscriptionId); + Log.LogDebug("Found subscription called \"{SubscriptionDisplayName}\" ({SubscriptionId})", + Subscription.Data.DisplayName, Subscription.Data.SubscriptionId); + + // Get resource group resource for later use + ResourceGroup = Subscription.GetResourceGroup(ResourceGroupId.ResourceGroupName); + Log.LogDebug("Got resource group resource called \"{ResourceGroupName}\" ({ResourceGroupId})", + ResourceGroup.Data.Name, ResourceGroup.Data.Id); + } + + public AzureAppServicesClient(ResourceIdentifier resourceGroupId, string tenantId = "") + { + // Each AzureAppServicesClient represents a single resource group (and therefore subscription). + + Log = LogHandler.GetClassLogger(); + Log.LogDebug("Initializing Azure App Services client"); + + // Create resource identifier object from resourceId + ResourceGroupId = resourceGroupId; + + // Create Azure Resource Management client + Log.LogDebug("Getting Azure token using DefaultAzureCredential"); + if (string.IsNullOrEmpty(tenantId)) + ArmClient = new ArmClient(new DefaultAzureCredential()); + else + ArmClient = new ArmClient(new DefaultAzureCredential(new DefaultAzureCredentialOptions + { TenantId = tenantId })); + + // Get subscription resource defined by resource ID + Subscription = ArmClient.GetSubscriptions().Get(ResourceGroupId.SubscriptionId); + Log.LogDebug("Found subscription called \"{SubscriptionName}\" ({SubscriptionId})", + Subscription.Data.DisplayName, Subscription.Data.SubscriptionId); + + // Get resource group resource for later use + ResourceGroup = Subscription.GetResourceGroup(ResourceGroupId.ResourceGroupName); + Log.LogDebug("Got resource group resource called \"{ResourceGroupName}\" ({ResourceGroupId})", + ResourceGroup.Data.Name, ResourceGroup.Data.Id); + } + + private ArmClient ArmClient { get; } + private SubscriptionResource Subscription { get; } + private ResourceGroupResource ResourceGroup { get; } + private ResourceIdentifier ResourceGroupId { get; } + private ILogger Log { get; } + + #region Import + + public AppCertificateResource ImportCertificateFromAkv(ResourceIdentifier appServiceResourceId, + ResourceIdentifier keyVaultResourceId, string keyVaultSecretName) + { + // Get Azure Web Site resource using resource ID to get location and app service plan ID + WebSiteResource site = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); + Log.LogDebug("Got WebSiteResource for {Name}", site.Data.Name); + + // Get location from Web Site resource + AzureLocation location = site.Data.Location; + ResourceIdentifier appServicePlanId = site.Data.AppServicePlanId; + + Log.LogDebug("Importing certificate with name {Name} from Key Vault {VaultName}", keyVaultSecretName, + keyVaultResourceId.Name); + + return ResourceGroup.GetAppCertificates().CreateOrUpdate(WaitUntil.Completed, + keyVaultSecretName, + new AppCertificateData(location) + { + KeyVaultSecretName = keyVaultSecretName, + KeyVaultId = keyVaultResourceId, + ServerFarmId = appServicePlanId + }).WaitForCompletion(); + } + + #endregion + + #region Removal + + public void RemoveCertificate(string name) + { + AppCertificateResource cert = GetCertificateResourceByName(name); + if (cert == null) return; + + cert.Delete(WaitUntil.Completed); + } + + #endregion + + #region Download + + public AppCertificateResource GetCertificateResourceByName(string name) + { + return ResourceGroup.GetAppCertificates().GetAll().FirstOrDefault(cert => Equals(cert.Data.Name, name)); + } + + #endregion + + #region Bindings + + public List GetHostnameBindings(ResourceIdentifier appServiceResourceId) + { + // Get Web Site resource using resource ID + WebSiteResource site = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); + Log.LogDebug("Found {0} hostname bindings for {1}", site.Data.HostNames.Count, site.Data.Name); + + // Get hostname bindings for Web Site resource + return site.Data.HostNames.ToList(); + } + + public WebSiteResource GetSiteResourceFromHostname(string hostname) + { + // Use LINQ syntax to find the site with matching hostname + return (from siteResource in ResourceGroup.GetWebSites().GetAll() + let bindings = siteResource.GetSiteHostNameBindings() + from binding in bindings.GetAll() + where binding.Data.Name.Contains(hostname) + select siteResource).FirstOrDefault(); + } + + public SiteHostNameBindingResource UpdateCertificateBinding(ResourceIdentifier appServiceResourceId, + AppCertificateResource certificateResource, HostNameBindingSslState? sslState) + { + // Get Azure Web Site resource using resource ID to get location and app service plan ID + WebSiteResource appServiceResource = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); + + SiteHostNameBindingCollection bindings = appServiceResource.GetSiteHostNameBindings(); + + // Try to add certificate to each matching hostname + foreach (string host in appServiceResource.Data.HostNames) + { + if (!certificateResource.Data.HostNames.Contains(host)) continue; + + bindings.CreateOrUpdate(WaitUntil.Completed, host, new HostNameBindingData + { + SiteName = appServiceResource.Data.RepositorySiteName, + AzureResourceName = appServiceResource.Data.Name, + SslState = sslState, + Thumbprint = certificateResource.Data.Thumbprint + }); + Log.LogDebug("Bound certificate with name {Name} to hostname {Hostname}", certificateResource.Data.Name, host); + } + + return IsCertificateBoundToAppService(appServiceResource, certificateResource); + } + + public List UpdateCertificateBinding(ResourceIdentifier keyVaultResourceId, + KeyVaultCertificateWithPolicy akvCertificateName) + { + // Create a list of SiteHostNameBindingResource to return + List bindings = new List(); + + // Iterate through all DNS SANS attached to certificate, and try to update bindings for each. + foreach (string dnsName in akvCertificateName.Policy.SubjectAlternativeNames.DnsNames) + { + WebSiteResource site = GetSiteResourceFromHostname(dnsName); // Null is returned if no site is found. + if (site == null) continue; + + // If it matches, import the AKV certificate into the app service + AppCertificateResource appCert = ImportCertificateFromAkv(site.Id, keyVaultResourceId, akvCertificateName.Name); + + // Finally, update the hostname binding to use the App Service certificate + bindings.Add(UpdateCertificateBinding(site.Id, appCert, HostNameBindingSslState.SniEnabled).Data.Name); + } + + return bindings; + } + + public void RemoveCertificateBinding(KeyVaultCertificateWithPolicy akvCertificateName) + { + // Get the corresponding App Service certificate resource + AppCertificateResource appCert = GetCertificateResourceByName(akvCertificateName.Name); + if (appCert == null) return; + + // Iterate through each web site and remove the certificate binding if it matches the AKV certificate + foreach (WebSiteResource site in ResourceGroup.GetWebSites().GetAll()) + { + foreach (string host in site.Data.HostNames) + { + if (appCert.Data.HostNames.Contains(host)) + { + RemoveCertificateBinding(site.Id, appCert); + } + } + } + } + + public void RemoveCertificateBinding(ResourceIdentifier appServiceResourceId, + AppCertificateResource certificateResource) + { + WebSiteResource appServiceResource = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); + + foreach (SiteHostNameBindingResource binding in appServiceResource.GetSiteHostNameBindings().GetAll()) + { + if ($"{binding.Data.Thumbprint}" != $"{certificateResource.Data.Thumbprint}") continue; + + binding.Update(WaitUntil.Completed, new HostNameBindingData() + { + SiteName = binding.Data.SiteName, + AzureResourceName = binding.Data.Name, + SslState = HostNameBindingSslState.Disabled, + Thumbprint = null + }); + Log.LogDebug("Removed certificate binding for {Name}", binding.Data.Name); + } + } + + public SiteHostNameBindingResource IsCertificateBoundToAppService(WebSiteResource appServiceResource, + AppCertificateResource cert) + { + if (cert == null) return null; + + // Get bindings from Web Site resource + foreach (SiteHostNameBindingResource binding in appServiceResource.GetSiteHostNameBindings().GetAll()) + if (binding != null && Equals($"{cert.Data.Thumbprint}", $"{binding.Data.Thumbprint}")) + { + Log.LogDebug("Certificate with thumbprint {Thumb} is bound to {Name}", cert.Data.Thumbprint, + appServiceResource.Data.Name); + return binding; + } + + Log.LogDebug("Certificate with thumbprint {Thumb} is not bound to {Name}", cert.Data.Thumbprint, + appServiceResource.Data.Name); + return null; + } + + #endregion + } +} \ No newline at end of file diff --git a/AzureKeyVault/AzureClient.cs b/AzureKeyVault/AzureClient.cs index a685c7d..15ff59e 100644 --- a/AzureKeyVault/AzureClient.cs +++ b/AzureKeyVault/AzureClient.cs @@ -167,6 +167,12 @@ public virtual async Task> GetCertificatesAsyn } return inventoryItems; } + + public virtual KeyVaultCertificateWithPolicy GetCertificate(string secretName) + { + var cert = CertClient.GetCertificate(secretName); + return cert.Value; + } public virtual async Task> GetVaults() { diff --git a/AzureKeyVault/AzureKeyVault.csproj b/AzureKeyVault/AzureKeyVault.csproj index a4d065c..40396ad 100644 --- a/AzureKeyVault/AzureKeyVault.csproj +++ b/AzureKeyVault/AzureKeyVault.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -11,32 +11,35 @@ false false + bin\ + C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\AKV - - - - - + + + + + + - - + + - + - PreserveNewest + Always diff --git a/AzureKeyVault/Jobs/AzureKeyVaultJob.cs b/AzureKeyVault/Jobs/AzureKeyVaultJob.cs index cfafce0..5808566 100644 --- a/AzureKeyVault/Jobs/AzureKeyVaultJob.cs +++ b/AzureKeyVault/Jobs/AzureKeyVaultJob.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections.Generic; using Keyfactor.Orchestrators.Extensions; using Newtonsoft.Json; @@ -23,6 +24,7 @@ public abstract class AzureKeyVaultJob : IOrchestratorJobExtension internal protected virtual AzureClient AzClient { get; set; } internal protected virtual AkvProperties VaultProperties { get; set; } + protected AzureAppServicesClient AppServicesClient { get; set; } public void InitializeStore(dynamic config) { @@ -41,9 +43,20 @@ public void InitializeStore(dynamic config) dynamic properties = JsonConvert.DeserializeObject(config.CertificateStoreDetails.Properties.ToString()); VaultProperties.ResourceGroupName = properties.ResourceGroupName; VaultProperties.VaultName = properties.VaultName; + + VaultProperties.AutoUpdateAppServiceBindings = (bool)properties.AutoUpdateBindings; + // Make binding variable safe in case Keyfactor expects an older version of the extension + if (properties.GetType().GetProperty("AutoUpdateBindings") != null) + VaultProperties.AutoUpdateAppServiceBindings = (bool)properties.AutoUpdateBindings; } AzClient ??= new AzureClient(VaultProperties); - } + + // If the store was configured to auto-update app service bindings, create a client to do so + if (VaultProperties.AutoUpdateAppServiceBindings) + { + AppServicesClient ??= new AzureAppServicesClient(VaultProperties); + } + } } } diff --git a/AzureKeyVault/Jobs/Management.cs b/AzureKeyVault/Jobs/Management.cs index d0115a5..9d87c8e 100644 --- a/AzureKeyVault/Jobs/Management.cs +++ b/AzureKeyVault/Jobs/Management.cs @@ -16,6 +16,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Azure.Core; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -95,42 +96,48 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st { var complete = new JobResult() { Result = OrchestratorJobStatusJobResult.Failure, JobHistoryId = jobHistoryId }; - if (!string.IsNullOrWhiteSpace(pfxPassword)) // This is a PFX Entry + if (string.IsNullOrWhiteSpace(pfxPassword)) // This is a PFX Entry { - if (string.IsNullOrWhiteSpace(alias)) + complete.FailureMessage = "Certificate to add must be in a .PFX file format."; + return complete; + } + + if (string.IsNullOrWhiteSpace(alias)) + { + complete.FailureMessage = "You must supply an alias for the certificate."; + return complete; + } + + try + { + // Import certificate into Azure Key Vault + var cert = AzClient.ImportCertificateAsync(alias, entryContents, pfxPassword).Result; + + // Update app service bindings if necessary + if (VaultProperties.AutoUpdateAppServiceBindings) { - complete.FailureMessage = "You must supply an alias for the certificate."; - return complete; + AppServicesClient.UpdateCertificateBinding(new ResourceIdentifier(VaultProperties.StorePath), cert); } - - try + + // Ensure the return object has a AKV version tag, and Thumbprint + if (!string.IsNullOrEmpty(cert.Properties.Version) && + !string.IsNullOrEmpty(string.Concat(cert.Properties.X509Thumbprint.Select(i => i.ToString("X2")))) + ) { - var cert = AzClient.ImportCertificateAsync(alias, entryContents, pfxPassword).Result; - - // Ensure the return object has a AKV version tag, and Thumbprint - if (!string.IsNullOrEmpty(cert.Properties.Version) && - !string.IsNullOrEmpty(string.Concat(cert.Properties.X509Thumbprint.Select(i => i.ToString("X2")))) - ) - { - complete.Result = OrchestratorJobStatusJobResult.Success; - } - else - { - // uploadCollection is either not null or an exception was thrown. - complete.FailureMessage = $"Unable to add {alias} to {ExtensionName}. Check your network connection, ensure the password is correct, and that your API connection information is correct."; - } + complete.Result = OrchestratorJobStatusJobResult.Success; } - catch (Exception ex) + else { - complete.FailureMessage = $"An error occured while adding {alias} to {ExtensionName}: " + ex.Message; - - if (ex.InnerException != null) - complete.FailureMessage += " - " + ex.InnerException.Message; + // uploadCollection is either not null or an exception was thrown. + complete.FailureMessage = $"Unable to add {alias} to {ExtensionName}. Check your network connection, ensure the password is correct, and that your API connection information is correct."; } } - else // Non-PFX + catch (Exception ex) { - complete.FailureMessage = "Certificate to add must be in a .PFX file format."; + complete.FailureMessage = $"An error occured while adding {alias} to {ExtensionName}: " + ex.Message; + + if (ex.InnerException != null) + complete.FailureMessage += " - " + ex.InnerException.Message; } return complete; @@ -152,6 +159,14 @@ protected virtual JobResult PerformRemoval(string alias, long jobHistoryId) try { + if (VaultProperties.AutoUpdateAppServiceBindings) + { + // If auto binding is enabled for the store, we need to remove the binding from the app service first + AppServicesClient.RemoveCertificateBinding(AzClient.GetCertificate(alias)); + // Then delete the app service certificate + AppServicesClient.RemoveCertificate(alias); + } + var result = AzClient.DeleteCertificateAsync(alias).Result; if (result.Value.Name == alias) diff --git a/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj b/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj new file mode 100644 index 0000000..4b909e2 --- /dev/null +++ b/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/AzureKeyVaultBindingTests/Program.cs b/AzureKeyVaultBindingTests/Program.cs new file mode 100644 index 0000000..eff273b --- /dev/null +++ b/AzureKeyVaultBindingTests/Program.cs @@ -0,0 +1,173 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Azure; +using Azure.Core; +using Azure.Identity; +using Azure.ResourceManager; +using Azure.ResourceManager.AppService; +using Azure.ResourceManager.AppService.Models; +using Azure.ResourceManager.Resources; +using Azure.Security.KeyVault.Certificates; +using Keyfactor.Extensions.Orchestrator.AzureKeyVault; + +namespace AzureKeyVaultBindingTests +{ + internal class Program + { + private static void Main(string[] args) + { + // Test the Azure Key Vault binding capability + + // 1. Set up the Azure Key Vault client and Azure App Service client + Program p = new Program(); + + // 2. Get a bound hostname from the app service that isn't the default hostname + string hostname = p.GetHostname(); + Console.Write("Using hostname " + hostname + "\n"); + + // 3. Enroll a certificate for the hostname with one SAN + X509Certificate2 selfSignedCert = p.GetSelfSignedCert(hostname); + + // 4. Upload the certificate to the Azure Key Vault + string certName = "AKVTest" + Guid.NewGuid().ToString().Substring(0, 6); + KeyVaultCertificateWithPolicy akvCert = p.UploadCertToAkv(selfSignedCert, certName); + + // 5. Bind the certificate to the app service + p.BindCertificateToAppService(akvCert); + + // 6. Verify the certificate is bound to the app service + p.VerifyCertificateBinding(certName); + + // 7. Unbind the certificate from the app service + p.DeleteCertificateBinding(certName); + + // 8. Delete the certificate from the Azure Key Vault + p.DeleteCertFromAkv(certName); + } + + public Program() + { + Console.Write("Configuring clients\n"); + + AppServiceClient = new AzureAppServicesClient(_resourceGroupId); + Console.Write("Created AppServiceClient\n"); + + AkvProperties vaultProperties = new AkvProperties() + { + TenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID") ?? string.Empty, + ResourceGroupName = _resourceGroupId.ResourceGroupName, + VaultName = Environment.GetEnvironmentVariable("AKV_NAME") ?? string.Empty, + ApplicationId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID") ?? string.Empty, + ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty, + AutoUpdateAppServiceBindings = true, + StorePath = _keyVaultResourceId, + }; + KeyVaultClient = new AzureClient(vaultProperties); + Console.Write("Created KeyVaultClient\n"); + } + + private readonly ResourceIdentifier _resourceGroupId = new ResourceIdentifier(Environment.GetEnvironmentVariable("RESOURCE_GROUP_ID") ?? string.Empty); + private readonly ResourceIdentifier _appServiceResourceId = new ResourceIdentifier(Environment.GetEnvironmentVariable("APP_SERVICE_ID") ?? string.Empty); + private readonly ResourceIdentifier _keyVaultResourceId = new ResourceIdentifier(Environment.GetEnvironmentVariable("AKV_ID") ?? string.Empty); + private AzureAppServicesClient AppServiceClient { get; } + private AzureClient KeyVaultClient { get; } + + public string GetHostname() + { + return AppServiceClient.GetHostnameBindings(_appServiceResourceId).First(host => !host.Contains("azurewebsites.net")); + } + + public X509Certificate2 GetSelfSignedCert(string hostname) + { + RSA rsa = RSA.Create(2048); + CertificateRequest req = new CertificateRequest($"CN={hostname}", rsa, HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + SubjectAlternativeNameBuilder subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder(); + subjectAlternativeNameBuilder.AddDnsName(hostname); + req.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build()); + req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); + req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("2.5.29.32.0"), new Oid("1.3.6.1.5.5.7.3.1") }, false)); + + X509Certificate2 selfSignedCert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); + Console.Write($"Created self-signed certificate for {hostname} with thumbprint {selfSignedCert.Thumbprint}\n"); + return selfSignedCert; + } + + public KeyVaultCertificateWithPolicy UploadCertToAkv(X509Certificate2 cert, string certName) + { + string password = Guid.NewGuid().ToString().Substring(0, 10); + // Export cert to PFX and base64 encode it + string pfxBytes = Convert.ToBase64String(cert.Export(X509ContentType.Pfx, password)); + Task createTask = KeyVaultClient.ImportCertificateAsync(certName, pfxBytes, password); + createTask.Wait(); + KeyVaultCertificateWithPolicy akvCert = createTask.Result; + Console.Write("Uploaded certificate to Azure Key Vault\n"); + + AppServiceClient.RemoveCertificateBinding(akvCert); + return akvCert; + } + + public void BindCertificateToAppService(KeyVaultCertificateWithPolicy cert) + { + string hostname = GetHostname(); + X509Certificate2 ssCert = GetSelfSignedCert(hostname); + + WebSiteResource site = AppServiceClient.GetSiteResourceFromHostname(hostname); + + ArmClient armClient = new ArmClient(new DefaultAzureCredential()); + SubscriptionResource subscription = armClient.GetSubscriptions().Get(_resourceGroupId.SubscriptionId); + ResourceGroupResource resourceGroup = subscription.GetResourceGroups().Get(_resourceGroupId.ResourceGroupName); + + try + { + resourceGroup.GetAppCertificates().CreateOrUpdate(WaitUntil.Completed, cert.Name, new AppCertificateData(site.Data.Location) + { + Password = "FooBar", + PfxBlob = ssCert.Export(X509ContentType.Pfx, "FooBar"), + ServerFarmId = site.Data.AppServicePlanId, + }).WaitForCompletion(); + } catch (System.ArgumentException e) + { + // Do nothing because it's gonna fail + Console.Write("Uploaded certificate\n"); + } + + AppCertificateResource appCert = AppServiceClient.GetCertificateResourceByName(cert.Name); + + AppServiceClient.UpdateCertificateBinding(_appServiceResourceId, appCert, + HostNameBindingSslState.SniEnabled); + + AppServiceClient.RemoveCertificateBinding(_appServiceResourceId, appCert); + + AppServiceClient.RemoveCertificate(cert.Name); + + // This is the only thing that needs to be called + //AppServiceClient.UpdateCertificateBinding(_keyVaultResourceId, cert); + } + + public void VerifyCertificateBinding(string name) + { + WebSiteResource site = AppServiceClient.GetSiteResourceFromHostname(GetHostname()); + AppCertificateResource certResource = AppServiceClient.GetCertificateResourceByName(name); + SiteHostNameBindingResource bind = AppServiceClient.IsCertificateBoundToAppService(site, certResource); + Console.Write("Certificate is {0}bound to app service\n", bind == null ? "not " : ""); + } + + public void DeleteCertificateBinding(string name) + { + KeyVaultCertificateWithPolicy cert = KeyVaultClient.GetCertificate(name); + AppServiceClient.RemoveCertificateBinding(cert); + AppServiceClient.RemoveCertificate(cert.Name); + Console.Write("Deleted certificate binding\n"); + } + + public void DeleteCertFromAkv(string certName) + { + Task deleteTask = KeyVaultClient.DeleteCertificateAsync(certName); + deleteTask.Wait(); + Console.Write("Deleted certificate from Azure Key Vault\n"); + } + + } +} \ No newline at end of file diff --git a/README.md b/README.md index a905269..cfae56b 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,7 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur 1) Navigate to the _Custom Fields_ tab and add the following fields - Name: "**VaultName**", Display Name: "**Vault Name**", Required: **true** (checked) - Name: "**ResourceGroupName**", Display Name: "**Resource Group Name**", Required: **true** (checked) + - Name: "**AutoUpdateBindings**", Display Name: "**Auto Update AppService Bindings**", Type: **bool**, Default Value: **false**, Required: **false** (unchecked) ### Install the Extension on the Orchestrator @@ -437,6 +438,8 @@ The steps to do this are: - **Store Path**: This is the Azure Resource Identifier for the Keyvault. Copied from Azure, or created a new Keyvault (see below). - **VaultName**: This is the name of the new or existing Azure Keyvault. - **ResourceGroupName**: The name of the Azure Resource Group that contains the Keyvault. +- **AutoUpdateBindings**: Set to _true_ if you would like the extension to automatically update the certificate bindings when a certificate is imported into the Keyvault. + - Note: If this is set to _true_, the Azure Key Vault needs additional policies. See [the bindings permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. If the vault already exists in azure: The store path can be found by navigating to the existing Keyvault resource in Azure and clicking "Properties" in the left menu. @@ -454,6 +457,25 @@ If the Keyvault does not exist in Azure, and you would like to create it: ![Add Vault](/Images/add-vault.png) +### Permissions Required for Auto App Service Binding Updates + +Azure App Service resources can be configured to use custom domains and TLS certificates. With the default Azure Key Vault orchestrator extension, +the certificate bindings are not automatically updated when a certificate is imported into the Key Vault. This can be done manually by the user, but +it is not ideal. When enabled, the extension will automatically import the AKV certificate as an App Service certificate and update the certificate bindings for any App Service resources whose +domain name matches one of the certificate's DNS Subject Alternative Names (SANs). It's required that the app service be in the same resource group as the Key Vault. + +To enable this feature, create **Access Policies** to grant _read_ permission to the `Microsoft.Azure.WebSites` and `Microsoft.Azure.CertificateRegistration` resource providers. +This should be done using the same method used [to assign access to an individual key vault](#assign-permissions-for-an-individual-key-vault-via-access-policy), where the application name +is replaced by the object ID or name shown below: + +- Microsoft Azure App Service/Microsoft.Azure.WebSites + - Object ID: abfa0a7c-a6b6-4736-8310-5855508787cd (Same for all Azure subscriptions) + - Object ID: 6a02c803-dafd-4136-b4c3-5a6f318b4714 (Azure Government cloud environments) +- Microsoft.Azure.CertificateRegistration + - Object ID: ed47c2a1-bd23-4341-b39c-f4fd69138dd3 + +To access resources in the resource group, the service principal also needs to have _maintain_ permissions over the resource group. + --- ### License From e04d757348839668a10afce42b89d89570ac1553 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Fri, 23 Dec 2022 04:03:20 +0000 Subject: [PATCH 2/7] Update generated README --- README.md | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/README.md b/README.md index cfae56b..4427229 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,6 @@ ___ ## Platform Specific Notes -The minimum version of the Universal Orchestrator Framework needed to run this version of the extension is - The Keyfactor Universal Orchestrator may be installed on either Windows or Linux based platforms. The certificate operations supported by a capability may vary based what platform the capability is installed on. The table below indicates what capabilities are supported based on which platform the encompassing Universal Orchestrator is running. | Operation | Win | Linux | |-----|-----|------| @@ -44,6 +42,7 @@ The Keyfactor Universal Orchestrator may be installed on either Windows or Linux + --- @@ -299,7 +298,6 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur 1) Navigate to the _Custom Fields_ tab and add the following fields - Name: "**VaultName**", Display Name: "**Vault Name**", Required: **true** (checked) - Name: "**ResourceGroupName**", Display Name: "**Resource Group Name**", Required: **true** (checked) - - Name: "**AutoUpdateBindings**", Display Name: "**Auto Update AppService Bindings**", Type: **bool**, Default Value: **false**, Required: **false** (unchecked) ### Install the Extension on the Orchestrator @@ -438,8 +436,6 @@ The steps to do this are: - **Store Path**: This is the Azure Resource Identifier for the Keyvault. Copied from Azure, or created a new Keyvault (see below). - **VaultName**: This is the name of the new or existing Azure Keyvault. - **ResourceGroupName**: The name of the Azure Resource Group that contains the Keyvault. -- **AutoUpdateBindings**: Set to _true_ if you would like the extension to automatically update the certificate bindings when a certificate is imported into the Keyvault. - - Note: If this is set to _true_, the Azure Key Vault needs additional policies. See [the bindings permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. If the vault already exists in azure: The store path can be found by navigating to the existing Keyvault resource in Azure and clicking "Properties" in the left menu. @@ -457,25 +453,6 @@ If the Keyvault does not exist in Azure, and you would like to create it: ![Add Vault](/Images/add-vault.png) -### Permissions Required for Auto App Service Binding Updates - -Azure App Service resources can be configured to use custom domains and TLS certificates. With the default Azure Key Vault orchestrator extension, -the certificate bindings are not automatically updated when a certificate is imported into the Key Vault. This can be done manually by the user, but -it is not ideal. When enabled, the extension will automatically import the AKV certificate as an App Service certificate and update the certificate bindings for any App Service resources whose -domain name matches one of the certificate's DNS Subject Alternative Names (SANs). It's required that the app service be in the same resource group as the Key Vault. - -To enable this feature, create **Access Policies** to grant _read_ permission to the `Microsoft.Azure.WebSites` and `Microsoft.Azure.CertificateRegistration` resource providers. -This should be done using the same method used [to assign access to an individual key vault](#assign-permissions-for-an-individual-key-vault-via-access-policy), where the application name -is replaced by the object ID or name shown below: - -- Microsoft Azure App Service/Microsoft.Azure.WebSites - - Object ID: abfa0a7c-a6b6-4736-8310-5855508787cd (Same for all Azure subscriptions) - - Object ID: 6a02c803-dafd-4136-b4c3-5a6f318b4714 (Azure Government cloud environments) -- Microsoft.Azure.CertificateRegistration - - Object ID: ed47c2a1-bd23-4341-b39c-f4fd69138dd3 - -To access resources in the resource group, the service principal also needs to have _maintain_ permissions over the resource group. - --- ### License From 548db09158b473bd99bf41ed12b34550be20a0ef Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Fri, 23 Dec 2022 04:12:10 +0000 Subject: [PATCH 3/7] Add permission requirements to README --- readme_source.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/readme_source.md b/readme_source.md index d5e8b69..be98b79 100644 --- a/readme_source.md +++ b/readme_source.md @@ -250,6 +250,7 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur 1) Navigate to the _Custom Fields_ tab and add the following fields - Name: "**VaultName**", Display Name: "**Vault Name**", Required: **true** (checked) - Name: "**ResourceGroupName**", Display Name: "**Resource Group Name**", Required: **true** (checked) + - Name: "**AutoUpdateBindings**", Display Name: "**Auto Update AppService Bindings**", Type: **bool**, Default Value: **false**, Required: **false** (unchecked) ### Install the Extension on the Orchestrator @@ -388,6 +389,8 @@ The steps to do this are: - **Store Path**: This is the Azure Resource Identifier for the Keyvault. Copied from Azure, or created a new Keyvault (see below). - **VaultName**: This is the name of the new or existing Azure Keyvault. - **ResourceGroupName**: The name of the Azure Resource Group that contains the Keyvault. +- **AutoUpdateBindings**: Set to _true_ if you would like the extension to automatically update the certificate bindings when a certificate is imported into the Keyvault. + - Note: If this is set to _true_, the Azure Key Vault needs additional policies. See [the bindings permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. If the vault already exists in azure: The store path can be found by navigating to the existing Keyvault resource in Azure and clicking "Properties" in the left menu. @@ -404,6 +407,24 @@ If the Keyvault does not exist in Azure, and you would like to create it: ![Add Vault](/Images/add-vault.png) +### Permissions Required for Auto App Service Binding Updates + +Azure App Service resources can be configured to use custom domains and TLS certificates. With the default Azure Key Vault orchestrator extension, +the certificate bindings are not automatically updated when a certificate is imported into the Key Vault. This can be done manually by the user, but +it is not ideal. When enabled, the extension will automatically import the AKV certificate as an App Service certificate and update the certificate bindings for any App Service resources whose +domain name matches one of the certificate's DNS Subject Alternative Names (SANs). It's required that the app service be in the same resource group as the Key Vault. + +To enable this feature, create **Access Policies** to grant _read_ permission to the `Microsoft.Azure.WebSites` and `Microsoft.Azure.CertificateRegistration` resource providers. +This should be done using the same method used [to assign access to an individual key vault](#assign-permissions-for-an-individual-key-vault-via-access-policy), where the application name +is replaced by the object ID or name shown below: + +- Microsoft Azure App Service/Microsoft.Azure.WebSites + - Object ID: abfa0a7c-a6b6-4736-8310-5855508787cd (Same for all Azure subscriptions) + - Object ID: 6a02c803-dafd-4136-b4c3-5a6f318b4714 (Azure Government cloud environments) +- Microsoft.Azure.CertificateRegistration + - Object ID: ed47c2a1-bd23-4341-b39c-f4fd69138dd3 + +To access resources in the resource group, the service principal also needs to have _maintain_ permissions over the resource group. --- From ff54f8eb654fd5e4fd067d397fb1b9085c976e8a Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Fri, 23 Dec 2022 04:12:59 +0000 Subject: [PATCH 4/7] Change update bindings step --- AzureKeyVaultBindingTests/Program.cs | 35 +--------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/AzureKeyVaultBindingTests/Program.cs b/AzureKeyVaultBindingTests/Program.cs index eff273b..7ab2057 100644 --- a/AzureKeyVaultBindingTests/Program.cs +++ b/AzureKeyVaultBindingTests/Program.cs @@ -110,40 +110,7 @@ public KeyVaultCertificateWithPolicy UploadCertToAkv(X509Certificate2 cert, stri public void BindCertificateToAppService(KeyVaultCertificateWithPolicy cert) { - string hostname = GetHostname(); - X509Certificate2 ssCert = GetSelfSignedCert(hostname); - - WebSiteResource site = AppServiceClient.GetSiteResourceFromHostname(hostname); - - ArmClient armClient = new ArmClient(new DefaultAzureCredential()); - SubscriptionResource subscription = armClient.GetSubscriptions().Get(_resourceGroupId.SubscriptionId); - ResourceGroupResource resourceGroup = subscription.GetResourceGroups().Get(_resourceGroupId.ResourceGroupName); - - try - { - resourceGroup.GetAppCertificates().CreateOrUpdate(WaitUntil.Completed, cert.Name, new AppCertificateData(site.Data.Location) - { - Password = "FooBar", - PfxBlob = ssCert.Export(X509ContentType.Pfx, "FooBar"), - ServerFarmId = site.Data.AppServicePlanId, - }).WaitForCompletion(); - } catch (System.ArgumentException e) - { - // Do nothing because it's gonna fail - Console.Write("Uploaded certificate\n"); - } - - AppCertificateResource appCert = AppServiceClient.GetCertificateResourceByName(cert.Name); - - AppServiceClient.UpdateCertificateBinding(_appServiceResourceId, appCert, - HostNameBindingSslState.SniEnabled); - - AppServiceClient.RemoveCertificateBinding(_appServiceResourceId, appCert); - - AppServiceClient.RemoveCertificate(cert.Name); - - // This is the only thing that needs to be called - //AppServiceClient.UpdateCertificateBinding(_keyVaultResourceId, cert); + AppServiceClient.UpdateCertificateBinding(_keyVaultResourceId, cert); } public void VerifyCertificateBinding(string name) From bc63410b60cb489f905c349e04585aa7f676b760 Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Fri, 23 Dec 2022 04:13:40 +0000 Subject: [PATCH 5/7] Update generated README --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 4427229..628df47 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,7 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur 1) Navigate to the _Custom Fields_ tab and add the following fields - Name: "**VaultName**", Display Name: "**Vault Name**", Required: **true** (checked) - Name: "**ResourceGroupName**", Display Name: "**Resource Group Name**", Required: **true** (checked) + - Name: "**AutoUpdateBindings**", Display Name: "**Auto Update AppService Bindings**", Type: **bool**, Default Value: **false**, Required: **false** (unchecked) ### Install the Extension on the Orchestrator @@ -436,6 +437,8 @@ The steps to do this are: - **Store Path**: This is the Azure Resource Identifier for the Keyvault. Copied from Azure, or created a new Keyvault (see below). - **VaultName**: This is the name of the new or existing Azure Keyvault. - **ResourceGroupName**: The name of the Azure Resource Group that contains the Keyvault. +- **AutoUpdateBindings**: Set to _true_ if you would like the extension to automatically update the certificate bindings when a certificate is imported into the Keyvault. + - Note: If this is set to _true_, the Azure Key Vault needs additional policies. See [the bindings permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. If the vault already exists in azure: The store path can be found by navigating to the existing Keyvault resource in Azure and clicking "Properties" in the left menu. @@ -452,6 +455,24 @@ If the Keyvault does not exist in Azure, and you would like to create it: ![Add Vault](/Images/add-vault.png) +### Permissions Required for Auto App Service Binding Updates + +Azure App Service resources can be configured to use custom domains and TLS certificates. With the default Azure Key Vault orchestrator extension, +the certificate bindings are not automatically updated when a certificate is imported into the Key Vault. This can be done manually by the user, but +it is not ideal. When enabled, the extension will automatically import the AKV certificate as an App Service certificate and update the certificate bindings for any App Service resources whose +domain name matches one of the certificate's DNS Subject Alternative Names (SANs). It's required that the app service be in the same resource group as the Key Vault. + +To enable this feature, create **Access Policies** to grant _read_ permission to the `Microsoft.Azure.WebSites` and `Microsoft.Azure.CertificateRegistration` resource providers. +This should be done using the same method used [to assign access to an individual key vault](#assign-permissions-for-an-individual-key-vault-via-access-policy), where the application name +is replaced by the object ID or name shown below: + +- Microsoft Azure App Service/Microsoft.Azure.WebSites + - Object ID: abfa0a7c-a6b6-4736-8310-5855508787cd (Same for all Azure subscriptions) + - Object ID: 6a02c803-dafd-4136-b4c3-5a6f318b4714 (Azure Government cloud environments) +- Microsoft.Azure.CertificateRegistration + - Object ID: ed47c2a1-bd23-4341-b39c-f4fd69138dd3 + +To access resources in the resource group, the service principal also needs to have _maintain_ permissions over the resource group. --- From 5ca3c83717ea04fc2e0e6518e764c6779f40f099 Mon Sep 17 00:00:00 2001 From: Hayden Roszell Date: Tue, 27 Dec 2022 17:11:14 -0700 Subject: [PATCH 6/7] Change AppServiceClient to update bindings across all available resource groups --- AzureKeyVault/AzureAppServicesClient.cs | 217 ++++++++++-------- AzureKeyVault/AzureKeyVault.csproj | 30 ++- AzureKeyVault/Jobs/Management.cs | 34 ++- .../AzureKeyVaultBindingTests.csproj | 6 +- AzureKeyVaultBindingTests/Program.cs | 130 ++++++----- readme_source.md | 26 ++- 6 files changed, 258 insertions(+), 185 deletions(-) diff --git a/AzureKeyVault/AzureAppServicesClient.cs b/AzureKeyVault/AzureAppServicesClient.cs index 1d1fa1d..5cd0db4 100644 --- a/AzureKeyVault/AzureAppServicesClient.cs +++ b/AzureKeyVault/AzureAppServicesClient.cs @@ -10,8 +10,6 @@ using Azure.Security.KeyVault.Certificates; using Keyfactor.Logging; using Microsoft.Extensions.Logging; -using Microsoft.VisualBasic; -using SiteHostNameBindingResource = Azure.ResourceManager.AppService.SiteHostNameBindingResource; namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault { @@ -22,6 +20,11 @@ public class AzureAppServicesClient // - AZURE_TENANT_ID → The tenant ID value. // - AZURE_CLIENT_SECRET → The password/credential generated for the app. + // The methods in this class mostly return lists because certificate resources representing the same certificate + // can exist in multiple resource groups. More specifically, App Service Certificate resources are bound to + // a specific resource group and region combination. This means that if a certificate DNS hostname list matches + // hostnames in multiple resource groups, the certificate needs to be imported and bound multiple times. + public AzureAppServicesClient(AkvProperties properties) { // Each AzureAppServicesClient represents a single resource group (and therefore subscription). @@ -29,36 +32,28 @@ public AzureAppServicesClient(AkvProperties properties) Log = LogHandler.GetClassLogger(); Log.LogDebug("Initializing Azure App Services client"); - // Build ResourceGroupId using SubscriptionId, and ResourceGroupName from properties - string subscriptionId = string.IsNullOrEmpty(properties.SubscriptionId) ? new ResourceIdentifier(properties.StorePath).SubscriptionId : properties.SubscriptionId; - ResourceGroupId = - new ResourceIdentifier( - $"/subscriptions/{subscriptionId}/resourceGroups/{properties.ResourceGroupName}"); + string subscriptionId = string.IsNullOrEmpty(properties.SubscriptionId) + ? new ResourceIdentifier(properties.StorePath).SubscriptionId + : properties.SubscriptionId; // Construct Azure Resource Management client using ClientSecretCredential based on properties inside AkvProperties; - ArmClient = new ArmClient(new ClientSecretCredential(properties.TenantId, properties.ApplicationId, properties.ClientSecret)); + ArmClient = new ArmClient(new ClientSecretCredential(properties.TenantId, properties.ApplicationId, + properties.ClientSecret)); // Get subscription resource defined by resource ID - Subscription = ArmClient.GetSubscriptions().Get(ResourceGroupId.SubscriptionId); + Subscription = ArmClient.GetDefaultSubscription(); Log.LogDebug("Found subscription called \"{SubscriptionDisplayName}\" ({SubscriptionId})", Subscription.Data.DisplayName, Subscription.Data.SubscriptionId); - - // Get resource group resource for later use - ResourceGroup = Subscription.GetResourceGroup(ResourceGroupId.ResourceGroupName); - Log.LogDebug("Got resource group resource called \"{ResourceGroupName}\" ({ResourceGroupId})", - ResourceGroup.Data.Name, ResourceGroup.Data.Id); } - public AzureAppServicesClient(ResourceIdentifier resourceGroupId, string tenantId = "") + public AzureAppServicesClient(string subscriptionId, string tenantId = "") { // Each AzureAppServicesClient represents a single resource group (and therefore subscription). + // Each AzureAppServicesClient represents a single subscription. Log = LogHandler.GetClassLogger(); Log.LogDebug("Initializing Azure App Services client"); - // Create resource identifier object from resourceId - ResourceGroupId = resourceGroupId; - // Create Azure Resource Management client Log.LogDebug("Getting Azure token using DefaultAzureCredential"); if (string.IsNullOrEmpty(tenantId)) @@ -68,25 +63,77 @@ public AzureAppServicesClient(ResourceIdentifier resourceGroupId, string tenantI { TenantId = tenantId })); // Get subscription resource defined by resource ID - Subscription = ArmClient.GetSubscriptions().Get(ResourceGroupId.SubscriptionId); + // TODO this should actually be getDefaultSubscription() but that doesn't work for some reason + Subscription = ArmClient.GetSubscriptions().Get(subscriptionId); Log.LogDebug("Found subscription called \"{SubscriptionName}\" ({SubscriptionId})", Subscription.Data.DisplayName, Subscription.Data.SubscriptionId); - - // Get resource group resource for later use - ResourceGroup = Subscription.GetResourceGroup(ResourceGroupId.ResourceGroupName); - Log.LogDebug("Got resource group resource called \"{ResourceGroupName}\" ({ResourceGroupId})", - ResourceGroup.Data.Name, ResourceGroup.Data.Id); } private ArmClient ArmClient { get; } private SubscriptionResource Subscription { get; } - private ResourceGroupResource ResourceGroup { get; } - private ResourceIdentifier ResourceGroupId { get; } private ILogger Log { get; } + private string FilterWildcard(string dnsName) + { + return dnsName.Contains("*") ? dnsName.Replace("*", "") : dnsName; + } + + public IEnumerable GetSiteResourceFromHostname(string hostname) + { + // Use LINQ syntax to find the site with matching hostname. + // Search across every resource group in the subscription. + return ( + from resourceGroup in Subscription.GetResourceGroups().GetAll() + from site in resourceGroup.GetWebSites().GetAll() + from hostnameBinding in site.Data.HostNames + where hostnameBinding.Contains(hostname) + select site).ToList(); + } + + #region Removal + + public void RemoveCertificate(string thumbprint) + { + foreach (AppCertificateResource certificateResource in GetCertificateResourceByThumbprint(thumbprint)) + certificateResource?.Delete(WaitUntil.Completed); + } + + #endregion + + #region Download + + public IEnumerable GetCertificateResourceByThumbprint(string thumbprint) + { + // Query across all resource groups in the subscription to find the certificate with the matching name + // Note that the same certificate could be deployed across multiple resource groups, so there could be + // more than 1 AppCertificateResource returned. + return ( + from resourceGroup in Subscription.GetResourceGroups().GetAll() + from cert in resourceGroup.GetAppCertificates().GetAll() + // AppCertificateResource wraps thumbprint in '"' so we need to remove them before comparing + where $"{cert.Data.Thumbprint}".Replace("\"", "") == thumbprint + select cert + ).ToList(); + } + + #endregion + #region Import - public AppCertificateResource ImportCertificateFromAkv(ResourceIdentifier appServiceResourceId, + public IEnumerable ImportCertificateFromAzureKeyVault( + ResourceIdentifier keyVaultResourceId, + KeyVaultCertificateWithPolicy akvCertificateName) + { + return from dnsName in akvCertificateName.Policy.SubjectAlternativeNames.DnsNames + select GetSiteResourceFromHostname(dnsName) + into sites // Get the site resource for each DNS name + from site in sites + where site != null // Filter out any sites that don't match + select ImportCertificateFromAzureKeyVault(site.Id, keyVaultResourceId, + akvCertificateName.Name); // Import the certificate into the site + } + + public AppCertificateResource ImportCertificateFromAzureKeyVault(ResourceIdentifier appServiceResourceId, ResourceIdentifier keyVaultResourceId, string keyVaultSecretName) { // Get Azure Web Site resource using resource ID to get location and app service plan ID @@ -99,8 +146,14 @@ public AppCertificateResource ImportCertificateFromAkv(ResourceIdentifier appSer Log.LogDebug("Importing certificate with name {Name} from Key Vault {VaultName}", keyVaultSecretName, keyVaultResourceId.Name); - - return ResourceGroup.GetAppCertificates().CreateOrUpdate(WaitUntil.Completed, + + // Get resource group resource + string resourceGroupId = + $"/subscriptions/{appServiceResourceId.SubscriptionId}/resourceGroups/{appServiceResourceId.ResourceGroupName}"; + ResourceGroupResource resourceGroup = + ArmClient.GetResourceGroupResource(new ResourceIdentifier(resourceGroupId)).Get(); + + return resourceGroup.GetAppCertificates().CreateOrUpdate(WaitUntil.Completed, keyVaultSecretName, new AppCertificateData(location) { @@ -112,55 +165,24 @@ public AppCertificateResource ImportCertificateFromAkv(ResourceIdentifier appSer #endregion - #region Removal - - public void RemoveCertificate(string name) - { - AppCertificateResource cert = GetCertificateResourceByName(name); - if (cert == null) return; - - cert.Delete(WaitUntil.Completed); - } - - #endregion - - #region Download - - public AppCertificateResource GetCertificateResourceByName(string name) - { - return ResourceGroup.GetAppCertificates().GetAll().FirstOrDefault(cert => Equals(cert.Data.Name, name)); - } - - #endregion - #region Bindings - public List GetHostnameBindings(ResourceIdentifier appServiceResourceId) + public IEnumerable GetHostnameBindings(ResourceIdentifier appServiceResourceId) { // Get Web Site resource using resource ID WebSiteResource site = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); Log.LogDebug("Found {0} hostname bindings for {1}", site.Data.HostNames.Count, site.Data.Name); - + // Get hostname bindings for Web Site resource return site.Data.HostNames.ToList(); } - public WebSiteResource GetSiteResourceFromHostname(string hostname) - { - // Use LINQ syntax to find the site with matching hostname - return (from siteResource in ResourceGroup.GetWebSites().GetAll() - let bindings = siteResource.GetSiteHostNameBindings() - from binding in bindings.GetAll() - where binding.Data.Name.Contains(hostname) - select siteResource).FirstOrDefault(); - } - public SiteHostNameBindingResource UpdateCertificateBinding(ResourceIdentifier appServiceResourceId, AppCertificateResource certificateResource, HostNameBindingSslState? sslState) { // Get Azure Web Site resource using resource ID to get location and app service plan ID WebSiteResource appServiceResource = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); - + SiteHostNameBindingCollection bindings = appServiceResource.GetSiteHostNameBindings(); // Try to add certificate to each matching hostname @@ -175,63 +197,56 @@ public SiteHostNameBindingResource UpdateCertificateBinding(ResourceIdentifier a SslState = sslState, Thumbprint = certificateResource.Data.Thumbprint }); - Log.LogDebug("Bound certificate with name {Name} to hostname {Hostname}", certificateResource.Data.Name, host); + Log.LogDebug("Bound certificate with name {Name} to hostname {Hostname}", certificateResource.Data.Name, + host); } return IsCertificateBoundToAppService(appServiceResource, certificateResource); } - public List UpdateCertificateBinding(ResourceIdentifier keyVaultResourceId, - KeyVaultCertificateWithPolicy akvCertificateName) + public IEnumerable UpdateCertificateBinding(AppCertificateResource certificate) { - // Create a list of SiteHostNameBindingResource to return - List bindings = new List(); - // Iterate through all DNS SANS attached to certificate, and try to update bindings for each. - foreach (string dnsName in akvCertificateName.Policy.SubjectAlternativeNames.DnsNames) - { - WebSiteResource site = GetSiteResourceFromHostname(dnsName); // Null is returned if no site is found. - if (site == null) continue; - - // If it matches, import the AKV certificate into the app service - AppCertificateResource appCert = ImportCertificateFromAkv(site.Id, keyVaultResourceId, akvCertificateName.Name); - - // Finally, update the hostname binding to use the App Service certificate - bindings.Add(UpdateCertificateBinding(site.Id, appCert, HostNameBindingSslState.SniEnabled).Data.Name); - } - - return bindings; + return ( + from host in certificate.Data.HostNames + select GetSiteResourceFromHostname(host) + into sites + from site in sites + where site != null + select UpdateCertificateBinding(site.Id, certificate, HostNameBindingSslState.SniEnabled).Data.Name + ).ToList(); } - public void RemoveCertificateBinding(KeyVaultCertificateWithPolicy akvCertificateName) + public IEnumerable RemoveCertificateBinding(KeyVaultCertificateWithPolicy akvCertificateName) { - // Get the corresponding App Service certificate resource - AppCertificateResource appCert = GetCertificateResourceByName(akvCertificateName.Name); - if (appCert == null) return; - - // Iterate through each web site and remove the certificate binding if it matches the AKV certificate - foreach (WebSiteResource site in ResourceGroup.GetWebSites().GetAll()) - { - foreach (string host in site.Data.HostNames) - { - if (appCert.Data.HostNames.Contains(host)) - { - RemoveCertificateBinding(site.Id, appCert); - } - } - } + string thumb = akvCertificateName.Properties.X509Thumbprint.Aggregate("", (current, b) => current + b.ToString("X2"));; + + // Search for the site that the certificate is bound to across all resource groups in the subscription + // Then, remove the certificate binding from the site. + + // Returns a list of thumbprints with size according to the number of binding removals. + return ( + from appCert in GetCertificateResourceByThumbprint(thumb) // Get list of cert resources + from resourceGroupResource in Subscription.GetResourceGroups().GetAll() + from webSiteResource in resourceGroupResource.GetWebSites().GetAll() + from binding in webSiteResource.GetSiteHostNameBindings().GetAll() + where appCert.Data.HostNames.Any(host => binding.Data.Name.Contains(host)) + select RemoveCertificateBinding(webSiteResource.Id, appCert) + ).ToList(); } - public void RemoveCertificateBinding(ResourceIdentifier appServiceResourceId, + public string RemoveCertificateBinding(ResourceIdentifier appServiceResourceId, AppCertificateResource certificateResource) { WebSiteResource appServiceResource = ArmClient.GetWebSiteResource(appServiceResourceId).Get(); + string removalBindingThumbprint = null; foreach (SiteHostNameBindingResource binding in appServiceResource.GetSiteHostNameBindings().GetAll()) { if ($"{binding.Data.Thumbprint}" != $"{certificateResource.Data.Thumbprint}") continue; - binding.Update(WaitUntil.Completed, new HostNameBindingData() + removalBindingThumbprint = $"{binding.Data.Thumbprint}"; + binding.Update(WaitUntil.Completed, new HostNameBindingData { SiteName = binding.Data.SiteName, AzureResourceName = binding.Data.Name, @@ -240,6 +255,8 @@ public void RemoveCertificateBinding(ResourceIdentifier appServiceResourceId, }); Log.LogDebug("Removed certificate binding for {Name}", binding.Data.Name); } + + return removalBindingThumbprint; } public SiteHostNameBindingResource IsCertificateBoundToAppService(WebSiteResource appServiceResource, diff --git a/AzureKeyVault/AzureKeyVault.csproj b/AzureKeyVault/AzureKeyVault.csproj index 40396ad..4784bc5 100644 --- a/AzureKeyVault/AzureKeyVault.csproj +++ b/AzureKeyVault/AzureKeyVault.csproj @@ -11,30 +11,26 @@ false false - bin\ + bin - C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions\AKV + bin - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/AzureKeyVault/Jobs/Management.cs b/AzureKeyVault/Jobs/Management.cs index 9d87c8e..031bbd0 100644 --- a/AzureKeyVault/Jobs/Management.cs +++ b/AzureKeyVault/Jobs/Management.cs @@ -14,9 +14,11 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Azure.Core; +using Azure.Security.KeyVault.Certificates; using Keyfactor.Logging; using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; @@ -116,7 +118,20 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st // Update app service bindings if necessary if (VaultProperties.AutoUpdateAppServiceBindings) { - AppServicesClient.UpdateCertificateBinding(new ResourceIdentifier(VaultProperties.StorePath), cert); + List bindingList = ( + // Import certificate from Azure Key Vault to Azure App Service + from certificateResource in AppServicesClient.ImportCertificateFromAzureKeyVault(new ResourceIdentifier(VaultProperties.StorePath), cert) + // Bind certificate to any Azure App Service that has a hostname matching the certificate's DNS SAN + select AppServicesClient.UpdateCertificateBinding(certificateResource) into bindings + from binding in bindings + select binding + ).ToList(); + + // Print the list of bindings that were updated + if (bindingList.Count > 0) + { + logger.LogInformation($"Updated bindings for the following Azure App Services: {string.Join(", ", bindingList)}"); + } } // Ensure the return object has a AKV version tag, and Thumbprint @@ -161,10 +176,19 @@ protected virtual JobResult PerformRemoval(string alias, long jobHistoryId) { if (VaultProperties.AutoUpdateAppServiceBindings) { - // If auto binding is enabled for the store, we need to remove the binding from the app service first - AppServicesClient.RemoveCertificateBinding(AzClient.GetCertificate(alias)); - // Then delete the app service certificate - AppServicesClient.RemoveCertificate(alias); + // If auto binding is enabled for the store, we need to remove the binding from the app service + // before we can delete the certificate from AKV. + + // Get the certificate object using the alias that it's stored under. + KeyVaultCertificateWithPolicy akvCertObject = AzClient.GetCertificate(alias); + + // RemoveCertificateBinding returns a list of thumbprints associated with certificate bindings + // that were removed. + foreach (string bindingRemovalThumb in AppServicesClient.RemoveCertificateBinding(akvCertObject)) + { + // Finally, foreach thumbprint that was removed, delete the certificate from Azure App Service. + AppServicesClient.RemoveCertificate(bindingRemovalThumb); + } } var result = AzClient.DeleteCertificateAsync(alias).Result; diff --git a/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj b/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj index 4b909e2..aae3f2a 100644 --- a/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj +++ b/AzureKeyVaultBindingTests/AzureKeyVaultBindingTests.csproj @@ -2,17 +2,15 @@ Exe - net6.0 - enable + netcoreapp3.1 enable + - - diff --git a/AzureKeyVaultBindingTests/Program.cs b/AzureKeyVaultBindingTests/Program.cs index 7ab2057..aba2551 100644 --- a/AzureKeyVaultBindingTests/Program.cs +++ b/AzureKeyVaultBindingTests/Program.cs @@ -1,11 +1,13 @@ -using System.Security.Cryptography; +using System; +using System.Linq; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; using Azure; using Azure.Core; using Azure.Identity; using Azure.ResourceManager; using Azure.ResourceManager.AppService; -using Azure.ResourceManager.AppService.Models; using Azure.ResourceManager.Resources; using Azure.Security.KeyVault.Certificates; using Keyfactor.Extensions.Orchestrator.AzureKeyVault; @@ -14,30 +16,63 @@ namespace AzureKeyVaultBindingTests { internal class Program { + private readonly ResourceIdentifier _appServiceResourceId = + new ResourceIdentifier(Environment.GetEnvironmentVariable("APP_SERVICE_ID") ?? string.Empty); + + private readonly ResourceIdentifier _keyVaultResourceId = + new ResourceIdentifier(Environment.GetEnvironmentVariable("AKV_ID") ?? string.Empty); + + private readonly ResourceIdentifier _resourceGroupId = + new ResourceIdentifier(Environment.GetEnvironmentVariable("RESOURCE_GROUP_ID") ?? string.Empty); + + public Program() + { + Console.Write("Configuring clients\n"); + + AkvProperties vaultProperties = new AkvProperties + { + TenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID") ?? string.Empty, + ResourceGroupName = _resourceGroupId.ResourceGroupName, + VaultName = Environment.GetEnvironmentVariable("AKV_NAME") ?? string.Empty, + ApplicationId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID") ?? string.Empty, + ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty, + AutoUpdateAppServiceBindings = true, + StorePath = _keyVaultResourceId + }; + KeyVaultClient = new AzureClient(vaultProperties); + Console.Write("Created KeyVaultClient\n"); + + AppServiceClient = new AzureAppServicesClient(vaultProperties); + Console.Write("Created AppServiceClient\n"); + } + + private AzureAppServicesClient AppServiceClient { get; } + private AzureClient KeyVaultClient { get; } + private static void Main(string[] args) { // Test the Azure Key Vault binding capability - + // 1. Set up the Azure Key Vault client and Azure App Service client Program p = new Program(); - + // 2. Get a bound hostname from the app service that isn't the default hostname string hostname = p.GetHostname(); Console.Write("Using hostname " + hostname + "\n"); - + // 3. Enroll a certificate for the hostname with one SAN X509Certificate2 selfSignedCert = p.GetSelfSignedCert(hostname); - + // 4. Upload the certificate to the Azure Key Vault string certName = "AKVTest" + Guid.NewGuid().ToString().Substring(0, 6); KeyVaultCertificateWithPolicy akvCert = p.UploadCertToAkv(selfSignedCert, certName); // 5. Bind the certificate to the app service p.BindCertificateToAppService(akvCert); - + // 6. Verify the certificate is bound to the app service p.VerifyCertificateBinding(certName); - + // 7. Unbind the certificate from the app service p.DeleteCertificateBinding(certName); @@ -45,36 +80,10 @@ private static void Main(string[] args) p.DeleteCertFromAkv(certName); } - public Program() - { - Console.Write("Configuring clients\n"); - - AppServiceClient = new AzureAppServicesClient(_resourceGroupId); - Console.Write("Created AppServiceClient\n"); - - AkvProperties vaultProperties = new AkvProperties() - { - TenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID") ?? string.Empty, - ResourceGroupName = _resourceGroupId.ResourceGroupName, - VaultName = Environment.GetEnvironmentVariable("AKV_NAME") ?? string.Empty, - ApplicationId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID") ?? string.Empty, - ClientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET") ?? string.Empty, - AutoUpdateAppServiceBindings = true, - StorePath = _keyVaultResourceId, - }; - KeyVaultClient = new AzureClient(vaultProperties); - Console.Write("Created KeyVaultClient\n"); - } - - private readonly ResourceIdentifier _resourceGroupId = new ResourceIdentifier(Environment.GetEnvironmentVariable("RESOURCE_GROUP_ID") ?? string.Empty); - private readonly ResourceIdentifier _appServiceResourceId = new ResourceIdentifier(Environment.GetEnvironmentVariable("APP_SERVICE_ID") ?? string.Empty); - private readonly ResourceIdentifier _keyVaultResourceId = new ResourceIdentifier(Environment.GetEnvironmentVariable("AKV_ID") ?? string.Empty); - private AzureAppServicesClient AppServiceClient { get; } - private AzureClient KeyVaultClient { get; } - public string GetHostname() { - return AppServiceClient.GetHostnameBindings(_appServiceResourceId).First(host => !host.Contains("azurewebsites.net")); + return AppServiceClient.GetHostnameBindings(_appServiceResourceId) + .First(host => !host.Contains("azurewebsites.net")); } public X509Certificate2 GetSelfSignedCert(string hostname) @@ -82,45 +91,63 @@ public X509Certificate2 GetSelfSignedCert(string hostname) RSA rsa = RSA.Create(2048); CertificateRequest req = new CertificateRequest($"CN={hostname}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - + SubjectAlternativeNameBuilder subjectAlternativeNameBuilder = new SubjectAlternativeNameBuilder(); subjectAlternativeNameBuilder.AddDnsName(hostname); req.CertificateExtensions.Add(subjectAlternativeNameBuilder.Build()); - req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false)); - req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("2.5.29.32.0"), new Oid("1.3.6.1.5.5.7.3.1") }, false)); - + req.CertificateExtensions.Add(new X509KeyUsageExtension( + X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | + X509KeyUsageFlags.DigitalSignature, false)); + req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension( + new OidCollection { new Oid("2.5.29.32.0"), new Oid("1.3.6.1.5.5.7.3.1") }, false)); + X509Certificate2 selfSignedCert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); - Console.Write($"Created self-signed certificate for {hostname} with thumbprint {selfSignedCert.Thumbprint}\n"); + Console.Write( + $"Created self-signed certificate for {hostname} with thumbprint {selfSignedCert.Thumbprint}\n"); return selfSignedCert; } - + public KeyVaultCertificateWithPolicy UploadCertToAkv(X509Certificate2 cert, string certName) { string password = Guid.NewGuid().ToString().Substring(0, 10); // Export cert to PFX and base64 encode it string pfxBytes = Convert.ToBase64String(cert.Export(X509ContentType.Pfx, password)); - Task createTask = KeyVaultClient.ImportCertificateAsync(certName, pfxBytes, password); + Task createTask = + KeyVaultClient.ImportCertificateAsync(certName, pfxBytes, password); createTask.Wait(); KeyVaultCertificateWithPolicy akvCert = createTask.Result; Console.Write("Uploaded certificate to Azure Key Vault\n"); - + AppServiceClient.RemoveCertificateBinding(akvCert); return akvCert; } + public void BindCertificateToAppService(KeyVaultCertificateWithPolicy cert) { - AppServiceClient.UpdateCertificateBinding(_keyVaultResourceId, cert); + // Import certificate from Azure Key Vault into any applicable app service. + foreach (AppCertificateResource importedCert in AppServiceClient.ImportCertificateFromAzureKeyVault( + _keyVaultResourceId, cert)) + { + AppServiceClient.UpdateCertificateBinding(importedCert); + } } public void VerifyCertificateBinding(string name) { - WebSiteResource site = AppServiceClient.GetSiteResourceFromHostname(GetHostname()); - AppCertificateResource certResource = AppServiceClient.GetCertificateResourceByName(name); - SiteHostNameBindingResource bind = AppServiceClient.IsCertificateBoundToAppService(site, certResource); - Console.Write("Certificate is {0}bound to app service\n", bind == null ? "not " : ""); + KeyVaultCertificateWithPolicy akvCertObject = KeyVaultClient.GetCertificate(name); + string thumbprint = + akvCertObject.Properties.X509Thumbprint.Aggregate("", (current, b) => current + b.ToString("X2")); + + foreach (WebSiteResource site in AppServiceClient.GetSiteResourceFromHostname(GetHostname())) + foreach (AppCertificateResource certResource in AppServiceClient.GetCertificateResourceByThumbprint( + thumbprint)) + { + SiteHostNameBindingResource bind = AppServiceClient.IsCertificateBoundToAppService(site, certResource); + Console.Write("Certificate is {0}bound to app service\n", bind == null ? "not " : ""); + } } - + public void DeleteCertificateBinding(string name) { KeyVaultCertificateWithPolicy cert = KeyVaultClient.GetCertificate(name); @@ -128,13 +155,12 @@ public void DeleteCertificateBinding(string name) AppServiceClient.RemoveCertificate(cert.Name); Console.Write("Deleted certificate binding\n"); } - + public void DeleteCertFromAkv(string certName) { Task deleteTask = KeyVaultClient.DeleteCertificateAsync(certName); deleteTask.Wait(); Console.Write("Deleted certificate from Azure Key Vault\n"); } - } } \ No newline at end of file diff --git a/readme_source.md b/readme_source.md index be98b79..0e7af31 100644 --- a/readme_source.md +++ b/readme_source.md @@ -251,6 +251,7 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur - Name: "**VaultName**", Display Name: "**Vault Name**", Required: **true** (checked) - Name: "**ResourceGroupName**", Display Name: "**Resource Group Name**", Required: **true** (checked) - Name: "**AutoUpdateBindings**", Display Name: "**Auto Update AppService Bindings**", Type: **bool**, Default Value: **false**, Required: **false** (unchecked) + - See the [auto update bindings](#auto-app-service-binding-updates) and [associated permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. ### Install the Extension on the Orchestrator @@ -390,7 +391,7 @@ The steps to do this are: - **VaultName**: This is the name of the new or existing Azure Keyvault. - **ResourceGroupName**: The name of the Azure Resource Group that contains the Keyvault. - **AutoUpdateBindings**: Set to _true_ if you would like the extension to automatically update the certificate bindings when a certificate is imported into the Keyvault. - - Note: If this is set to _true_, the Azure Key Vault needs additional policies. See [the bindings permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. + - Note: If this is set to _true_, the Azure Key Vault and Service Principal need additional policies. See the [auto update bindings](#auto-app-service-binding-updates) and [associated permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. If the vault already exists in azure: The store path can be found by navigating to the existing Keyvault resource in Azure and clicking "Properties" in the left menu. @@ -407,12 +408,22 @@ If the Keyvault does not exist in Azure, and you would like to create it: ![Add Vault](/Images/add-vault.png) -### Permissions Required for Auto App Service Binding Updates +### Auto App Service Binding Updates -Azure App Service resources can be configured to use custom domains and TLS certificates. With the default Azure Key Vault orchestrator extension, -the certificate bindings are not automatically updated when a certificate is imported into the Key Vault. This can be done manually by the user, but -it is not ideal. When enabled, the extension will automatically import the AKV certificate as an App Service certificate and update the certificate bindings for any App Service resources whose -domain name matches one of the certificate's DNS Subject Alternative Names (SANs). It's required that the app service be in the same resource group as the Key Vault. +Azure App Service resources can be configured to use custom domains and TLS certificates to secure them. Azure's turnkey solution to manage App Service TLS certificates is to purchase a certificate through Azure App Service Certificate Orders. +When a certificate is purchased through Azure App Service Certificate Orders, the certificate is automatically added to the Key Vault associated with the App Service resource. The certificate is also automatically added to the App Service resource's hostname bindings. +If a certificate was already purchased through Azure App Service Certificate Orders, Azure Key Vault automatically rotates and otherwise manages the certificate and its usage. However, if a certificate is obtained in another way and then imported into +an Azure Key Vault, Azure will not assist in the management of the certificate, including automatic updating of App Service hostname bindings, which is not ideal. This is where the Keyfactor Azure Key Vault integration can help. For example, a certificate may be +used within an organization for multiple purposes, including for TLS on an Azure App Service. + +When the "AutoUpdateBindings" option is enabled, the Keyfactor Azure Key Vault extension will automatically find App Services with bound hostnames that match the certificate's DNS SANs and update the hostname bindings. Then, when the certificate is EOL, +Keyfactor will automatically remove the TLS bindings and remove the App Service Certificate's reference to Azure Key Vault and associated Key Vault certificate entry. Furthermore, if Keyfactor is configured to automatically renew certificates, the +workflow for removing and re-adding bindings is handled automatically. + +This feature is designed to be constrained to the permissions set for the service principal used. Specifically, the extension can only update hostname bindings for App Services inside Resource Groups that the Service Principal has access to. For example, if +App Services exist in a resource group outside of the desired scope of management, the extension will not update the bindings for those resources. + +#### Permissions Required for Auto App Service Binding Updates To enable this feature, create **Access Policies** to grant _read_ permission to the `Microsoft.Azure.WebSites` and `Microsoft.Azure.CertificateRegistration` resource providers. This should be done using the same method used [to assign access to an individual key vault](#assign-permissions-for-an-individual-key-vault-via-access-policy), where the application name @@ -424,7 +435,8 @@ is replaced by the object ID or name shown below: - Microsoft.Azure.CertificateRegistration - Object ID: ed47c2a1-bd23-4341-b39c-f4fd69138dd3 -To access resources in the resource group, the service principal also needs to have _maintain_ permissions over the resource group. +To access App Services resources, the service principal created for the Keyfactor Azure Key Vault extension needs to have _maintain_ permissions over any resource groups that contain App Services resources that will be managed by Keyfactor. +This can be done by adding the service principal to any desired resource group access policies (IAM). --- From 27c4e6cc3df811c5c9e4a5ea1f6ed37b144b216c Mon Sep 17 00:00:00 2001 From: Keyfactor Date: Wed, 28 Dec 2022 00:12:16 +0000 Subject: [PATCH 7/7] Update generated README --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 628df47..e37882d 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,7 @@ Now we can navigate to the Keyfactor platform and create the store type for Azur - Name: "**VaultName**", Display Name: "**Vault Name**", Required: **true** (checked) - Name: "**ResourceGroupName**", Display Name: "**Resource Group Name**", Required: **true** (checked) - Name: "**AutoUpdateBindings**", Display Name: "**Auto Update AppService Bindings**", Type: **bool**, Default Value: **false**, Required: **false** (unchecked) + - See the [auto update bindings](#auto-app-service-binding-updates) and [associated permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. ### Install the Extension on the Orchestrator @@ -438,7 +439,7 @@ The steps to do this are: - **VaultName**: This is the name of the new or existing Azure Keyvault. - **ResourceGroupName**: The name of the Azure Resource Group that contains the Keyvault. - **AutoUpdateBindings**: Set to _true_ if you would like the extension to automatically update the certificate bindings when a certificate is imported into the Keyvault. - - Note: If this is set to _true_, the Azure Key Vault needs additional policies. See [the bindings permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. + - Note: If this is set to _true_, the Azure Key Vault and Service Principal need additional policies. See the [auto update bindings](#auto-app-service-binding-updates) and [associated permission section](#permissions-required-for-auto-app-service-binding-updates) for more information. If the vault already exists in azure: The store path can be found by navigating to the existing Keyvault resource in Azure and clicking "Properties" in the left menu. @@ -455,12 +456,22 @@ If the Keyvault does not exist in Azure, and you would like to create it: ![Add Vault](/Images/add-vault.png) -### Permissions Required for Auto App Service Binding Updates +### Auto App Service Binding Updates -Azure App Service resources can be configured to use custom domains and TLS certificates. With the default Azure Key Vault orchestrator extension, -the certificate bindings are not automatically updated when a certificate is imported into the Key Vault. This can be done manually by the user, but -it is not ideal. When enabled, the extension will automatically import the AKV certificate as an App Service certificate and update the certificate bindings for any App Service resources whose -domain name matches one of the certificate's DNS Subject Alternative Names (SANs). It's required that the app service be in the same resource group as the Key Vault. +Azure App Service resources can be configured to use custom domains and TLS certificates to secure them. Azure's turnkey solution to manage App Service TLS certificates is to purchase a certificate through Azure App Service Certificate Orders. +When a certificate is purchased through Azure App Service Certificate Orders, the certificate is automatically added to the Key Vault associated with the App Service resource. The certificate is also automatically added to the App Service resource's hostname bindings. +If a certificate was already purchased through Azure App Service Certificate Orders, Azure Key Vault automatically rotates and otherwise manages the certificate and its usage. However, if a certificate is obtained in another way and then imported into +an Azure Key Vault, Azure will not assist in the management of the certificate, including automatic updating of App Service hostname bindings, which is not ideal. This is where the Keyfactor Azure Key Vault integration can help. For example, a certificate may be +used within an organization for multiple purposes, including for TLS on an Azure App Service. + +When the "AutoUpdateBindings" option is enabled, the Keyfactor Azure Key Vault extension will automatically find App Services with bound hostnames that match the certificate's DNS SANs and update the hostname bindings. Then, when the certificate is EOL, +Keyfactor will automatically remove the TLS bindings and remove the App Service Certificate's reference to Azure Key Vault and associated Key Vault certificate entry. Furthermore, if Keyfactor is configured to automatically renew certificates, the +workflow for removing and re-adding bindings is handled automatically. + +This feature is designed to be constrained to the permissions set for the service principal used. Specifically, the extension can only update hostname bindings for App Services inside Resource Groups that the Service Principal has access to. For example, if +App Services exist in a resource group outside of the desired scope of management, the extension will not update the bindings for those resources. + +#### Permissions Required for Auto App Service Binding Updates To enable this feature, create **Access Policies** to grant _read_ permission to the `Microsoft.Azure.WebSites` and `Microsoft.Azure.CertificateRegistration` resource providers. This should be done using the same method used [to assign access to an individual key vault](#assign-permissions-for-an-individual-key-vault-via-access-policy), where the application name @@ -472,7 +483,8 @@ is replaced by the object ID or name shown below: - Microsoft.Azure.CertificateRegistration - Object ID: ed47c2a1-bd23-4341-b39c-f4fd69138dd3 -To access resources in the resource group, the service principal also needs to have _maintain_ permissions over the resource group. +To access App Services resources, the service principal created for the Keyfactor Azure Key Vault extension needs to have _maintain_ permissions over any resource groups that contain App Services resources that will be managed by Keyfactor. +This can be done by adding the service principal to any desired resource group access policies (IAM). ---