Skip to content

Commit

Permalink
Smart renew logic
Browse files Browse the repository at this point in the history
Mimic letsencrypt-siteextension
  • Loading branch information
ohadschn committed Apr 26, 2019
1 parent d86d81c commit 7706f05
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
Expand All @@ -7,6 +8,7 @@
using LetsEncrypt.Azure.Core;
using LetsEncrypt.Azure.Core.Models;
using OhadSoft.AzureLetsEncrypt.Renewal.Configuration;
using OhadSoft.AzureLetsEncrypt.Renewal.Util;

namespace OhadSoft.AzureLetsEncrypt.Renewal.Management
{
Expand Down Expand Up @@ -47,42 +49,37 @@ private static async Task RenewCore(RenewalParameters renewalParams)

Trace.TraceInformation("Adding SSL cert for '{0}'...", GetWebAppFullName(renewalParams));

if (renewalParams.RenewXNumberOfDaysBeforeExpiration > 0 && await HasCertificateSafe(webAppEnvironment))
bool addNewCert = true;
if (renewalParams.RenewXNumberOfDaysBeforeExpiration > 0)
{
await manager.RenewCertificate(false, renewalParams.RenewXNumberOfDaysBeforeExpiration);
}
else
{
await manager.AddCertificate();
var staging = acmeConfig.BaseUri.Contains("staging", StringComparison.OrdinalIgnoreCase);
var letsEncryptHostNames = await CertificateHelper.GetLetsEncryptHostNames(webAppEnvironment, staging);
Trace.TraceInformation("Let's Encrypt host names (staging: {0}): {1}", staging, String.Join(", ", letsEncryptHostNames));

ICollection<string> missingHostNames = acmeConfig.Hostnames.Except(letsEncryptHostNames, StringComparer.OrdinalIgnoreCase).ToArray();
if (missingHostNames.Count > 0)
{
Trace.TraceInformation(
"Detected host name(s) with no associated Let's Encrypt certificates, will add a new certificate: {0}",
String.Join(", ", missingHostNames));
}
else
{
Trace.TraceInformation("All host names associated with Let's Encrypt certificates, will perform cert renewal");
addNewCert = false;
}
}

Trace.TraceInformation("SSL cert added successfully to '{0}'", renewalParams.WebApp);
}

private static async Task<bool> HasCertificateSafe(AzureWebAppEnvironment webAppEnvironment)
{
try
if (addNewCert)
{
return await HasCertificate(webAppEnvironment);
await manager.AddCertificate();
}
catch (Exception e)
else
{
Trace.TraceWarning("Could not determine whether certificate is installed - assuming it is: {0}", e);
return true;
await manager.RenewCertificate(false, renewalParams.RenewXNumberOfDaysBeforeExpiration);
}
}

private static async Task<bool> HasCertificate(AzureWebAppEnvironment azureEnvironment)
{
using (var webSiteClient = await ArmHelper.GetWebSiteManagementClient(azureEnvironment))
{
var certs = await webSiteClient.Certificates.ListByResourceGroupWithHttpMessagesAsync(azureEnvironment.ResourceGroupName);
var site = webSiteClient.WebApps.GetSiteOrSlot(azureEnvironment.ResourceGroupName, azureEnvironment.WebAppName, azureEnvironment.SiteSlotName);

return certs.Body.Any(s =>
s.Issuer.Contains("Let's Encrypt")
&& site.HostNameSslStates.Any(hostNameBindings => hostNameBindings.Thumbprint == s.Thumbprint));
}
Trace.TraceInformation("Let's Encrypt SSL certs & bindings renewed for '{0}'", renewalParams.WebApp);
}

private static IAzureDnsEnvironment GetAzureDnsEnvironment(RenewalParameters renewalParams)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
<Compile Include="Management\RenewalParameters.cs" />
<Compile Include="Management\RenewalManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Util\CertificateHelper.cs" />
<Compile Include="Util\StringExtensions.cs" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using LetsEncrypt.Azure.Core;
using LetsEncrypt.Azure.Core.Models;
using Microsoft.Azure.Management.WebSites.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace OhadSoft.AzureLetsEncrypt.Renewal.Util
{
public static class CertificateHelper
{
// https://github.com/sjkp/letsencrypt-siteextension/blob/8e758579b21b0dac5269337e30ac88b629818889/LetsEncrypt.SiteExtension.Core/CertificateManager.cs#L146
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Task")]
public static async Task<IReadOnlyList<string>> GetLetsEncryptHostNames(IAzureWebAppEnvironment webAppEnvironment, bool staging)
{
Site site;
using (var client = await ArmHelper.GetWebSiteManagementClient(webAppEnvironment))
{
Trace.TraceInformation(
"Getting Web App '{0}' (slot '{1}') from resource group '{2}'",
webAppEnvironment.WebAppName,
webAppEnvironment.SiteSlotName,
webAppEnvironment.ResourceGroupName);

site = client.WebApps.GetSiteOrSlot(webAppEnvironment.ResourceGroupName, webAppEnvironment.WebAppName, webAppEnvironment.SiteSlotName);
}

using (var httpClient = await ArmHelper.GetHttpClient(webAppEnvironment))
{
var certRequestUri = $"/subscriptions/{webAppEnvironment.SubscriptionId}/providers/Microsoft.Web/certificates?api-version=2016-03-01";
Trace.TraceInformation("GET {0}", certRequestUri);
var response = await ArmHelper.ExponentialBackoff().ExecuteAsync(async () => await httpClient.GetAsync(certRequestUri));

Trace.TraceInformation("Reading ARM certificate query response");
var body = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();

var letsEncryptCerts = ExtractCertificates(body).Where(s => s.Issuer.Contains(staging ? "Fake LE" : "Let's Encrypt"));

var leCertThumbprints = new HashSet<string>(letsEncryptCerts.Select(c => c.Thumbprint));
return site.HostNameSslStates.Where(ssl => leCertThumbprints.Contains(ssl.Thumbprint)).Select(ssl => ssl.Name).ToArray();
}
}

// https://github.com/sjkp/letsencrypt-siteextension/blob/8e758579b21b0dac5269337e30ac88b629818889/LetsEncrypt.SiteExtension.Core/CertificateManager.cs#L204
internal static IEnumerable<Certificate> ExtractCertificates(string body)
{
Trace.TraceInformation("Deserializing certificates from ARM response");

var json = JToken.Parse(body);
return json.Type == JTokenType.Object && json["value"] != null
? JsonConvert.DeserializeObject<Certificate[]>(json["value"].ToString(), JsonHelper.DefaultSerializationSettings)
: JsonConvert.DeserializeObject<Certificate[]>(body, JsonHelper.DefaultSerializationSettings);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace OhadSoft.AzureLetsEncrypt.Renewal.Util
{
// https://stackoverflow.com/a/444818/67824
public static class StringExtensions
{
public static bool Contains(this string source, string substring, StringComparison stringComparison)
{
if (source == null) throw new ArgumentNullException(nameof(source));
return source.IndexOf(substring, stringComparison) >= 0;
}
}
}

0 comments on commit 7706f05

Please sign in to comment.