/// <summary> /// Checks whether the Azure resource needs a new certificate /// </summary> /// <returns>True if a new certificate should be requested</returns> public static async Task <bool> NeedsNewCertificateAsync() { ResourceConfiguration resource = await GetResourceConfigurationAsync(); IAppServiceCertificate existingCert = resource.ExistingCertificates? .Where(c => c.Issuer.Contains(Constants.DefaultCA)) .OrderByDescending(c => c.ExpirationDate) .FirstOrDefault(); if (existingCert == null) { return(true); } TimeSpan timeUntilExpiry = existingCert.ExpirationDate - DateTime.Now; if (timeUntilExpiry < Settings.TimeBeforeExpiryToRenew) { return(true); } _logger.LogInformation($" Existing certificate with thumbprint {existingCert.Thumbprint} is not close to expiry. A new certificate is not required."); return(false); }
/// <summary> /// Update an existing certificate /// </summary> private async Task <IAppServiceCertificate> UpdateExistingCertificateAsync(IAppServiceCertificate existingCertificate, byte[] bytes) { _logger.LogInformation("Updating existing Azure certificate name {CertificateName}.", existingCertificate.Name); // Azure doesn't let us update a certificate which is bound (the api fails with 409 Conflict), and we can't // unbind the running certificate without causing problems for a running site // So we will need to create a new certificate, bind to that, and then delete the existing one return(await CreateNewCertificateAsync(bytes, existingCertificate.RegionName)); }
private void RemoveCertificate(IAppServiceManager webSiteClient, IAppServiceCertificate s, AzureWebAppSettings setting) { try { webSiteClient.AppServiceCertificates.DeleteByResourceGroup(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName, s.Name); } catch { } }
/// <summary> /// If we've had to replace an existing certificate, then we'll need to delete the old one /// We actually delete any matching certificate created by FluffySpoon which ISN'T the one we /// just created /// </summary> /// <returns></returns> private async Task DeleteOldCertificatesAsync(IAppServiceCertificate newCertificate) { foreach (var certificate in await GetExistingAzureCertificatesAsync(CertificateType.Site)) { if (certificate.Thumbprint != newCertificate.Thumbprint) { _logger.LogInformation("Deleting old Azure certificate {CertificateName}", certificate.Name); await _client.WebApps.Manager .AppServiceCertificates .Inner .DeleteAsync( _azureOptions.ResourceGroupName, certificate.Name); } } }
/// <summary> /// Create a new certificate from scratch /// </summary> private async Task <IAppServiceCertificate> CreateNewCertificateAsync(byte[] bytes, string regionName) { var certificateName = TagName + "_" + Guid.NewGuid(); _logger.LogInformation("Creating new Azure certificate with name {CertificateName} in resource group {ResourceGroupName}, region {Region}.", certificateName, _azureOptions.ResourceGroupName, regionName); IAppServiceCertificate azureCertificate = await _client.WebApps.Manager .AppServiceCertificates .Define(certificateName) .WithRegion(regionName) .WithExistingResourceGroup(_azureOptions.ResourceGroupName) .WithPfxByteArray(bytes) .WithPfxPassword(nameof(FluffySpoon)) .CreateAsync(); _logger.LogTrace("Created new Azure certificate with name {CertificateName}", azureCertificate.Name); var tags = new Dictionary <string, string>(); foreach (var tag in azureCertificate.Tags) { tags.Add(tag.Key, tag.Value); } tags.Add(TagName, GetTagValue(CertificateType.Site)); _logger.LogInformation("Updating tags: {Tags} on certificate {CertificateName}", tags, azureCertificate.Name); await _client.WebApps.Manager .AppServiceCertificates .Inner .UpdateAsync( _azureOptions.ResourceGroupName, azureCertificate.Name, new CertificatePatchResource() { Tags = tags }); return(azureCertificate); }
private async Task UpdateAppBindingsAsync(AzureAppInstance appInstance, IAppServiceCertificate azureCertificate, string[] domains) { _logger.LogInformation("Checking host name bindings for app {AppName}", appInstance.DisplayName); string[] domainsToUpgrade = appInstance.HostNames .Where(boundDomain => DoesDomainMatch(boundDomain, domains)) .ToArray(); foreach (var domain in domainsToUpgrade) { _logger.LogDebug("Checking host name binding for domain {DomainName}", domain); HostNameBindingInner existingBinding = await appInstance.GetHostNameBindingAsync(_client, _azureOptions.ResourceGroupName, domain); if (DoesBindingNeedUpdating(existingBinding, azureCertificate.Thumbprint)) { _logger.LogDebug("Updating host name binding for app {AppName} domain {DomainName}", appInstance.DisplayName, domain); var newBinding = new HostNameBindingInner( azureResourceType: AzureResourceType.Website, hostNameType: HostNameType.Verified, customHostNameDnsRecordType: CustomHostNameDnsRecordType.CName, sslState: SslState.SniEnabled, thumbprint: azureCertificate.Thumbprint); await appInstance.SetHostNameBindingAsync(_client, _azureOptions.ResourceGroupName, domain, newBinding); } } }
public async Task PersistAsync(CertificateType persistenceType, IPersistableCertificate certificate) { if (certificate.RawData.Length == 0) { _logger.LogWarning("Tried to persist empty certificate."); return; } if (persistenceType != CertificateType.Site) { _logger.LogTrace("Skipping certificate persistence because a certificate of type {CertificateType} can't be persisted in Azure.", persistenceType); return; } var domains = _letsEncryptOptions.Domains.ToArray(); _logger.LogInformation("Creating new Azure certificate of type {CertificateType} and domains {DomainNames}.", persistenceType, String.Join(", ", domains)); var apps = await _client.WebApps.ListByResourceGroupAsync(_azureOptions.ResourceGroupName); var relevantApps = new HashSet <AzureAppInstance>(); foreach (var app in apps) { _logger.LogTrace("Checking hostnames of app {AppName} (AppHostNames: {HostNames}) against domains {DomainNames}.", app.Name, app.HostNames, String.Join(", ", domains)); if (DoDomainsMatch(app.HostNames, domains)) { _logger.LogTrace("App {AppName} matches a domain", app.Name); relevantApps.Add(new AzureAppInstance(app)); } if (app.DeploymentSlots != null) { var slots = await app.DeploymentSlots.ListAsync(); foreach (var slot in slots) { _logger.LogTrace( "Checking hostnames of app {AppName}/slot {slot} (AppHostNames: {HostNames}) against domains {Domains}.", app.Name, slot.Name, slot.HostNames, String.Join(", ", domains)); if (DoDomainsMatch(slot.HostNames, domains)) { _logger.LogTrace("App {AppName}/slot {slot} matches a domain", app.Name, slot.Name); relevantApps.Add(new AzureAppInstance(app, slot)); } } } } if (!relevantApps.Any()) { throw new InvalidOperationException( $"Could not find an app that has a hostname created for domains {String.Join(", ", domains)}."); } var regionName = relevantApps.FirstOrDefault()?.RegionName; _logger.LogInformation("Found region name to use to use for new certificate: {RegionName}", regionName); IAppServiceCertificate newCertificate = await CreateOrUpdateCertificateAsync(certificate.RawData, regionName); foreach (var appTuple in relevantApps) { await UpdateAppBindingsAsync(appTuple, newCertificate, domains); } await DeleteOldCertificatesAsync(newCertificate); }
public AzureCertificate(IAppServiceCertificate certificate) { _certificate = certificate; }
private async Task RemoveCertificate(IAppServiceManager webSiteClient, IAppServiceCertificate s, AzureWebAppSettings setting) { await webSiteClient.AppServiceCertificates.DeleteByResourceGroupAsync(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName, s.Name); }
private bool IsValidCertificate(IAppServiceCertificate certificate) { return(certificate.HostNames.Contains(_hostname) && certificate.ExpirationDate > DateTime.Now.AddDays(7) && certificate.Issuer.ToLower().Contains("let's encrypt")); }