Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
sjkp committed Apr 29, 2019
2 parents 011e67a + b40b12a commit e863482
Show file tree
Hide file tree
Showing 20 changed files with 150 additions and 341 deletions.
13 changes: 13 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<MsBuildAllProjects>$(MsBuildAllProjects);$(MsBuildThisFileFullPath)</MsBuildAllProjects>
</PropertyGroup>
<PropertyGroup>
<!-- edit this value to change the current major.minor version -->
<VersionPrefix>1.0</VersionPrefix>
<Product>Let's Encrypt Azure</Product>
<Company>SJKP</Company>
<!-- append the build number if it is available -->
<VersionPrefix Condition=" '$(BUILD_BUILDID)' != '' ">$(VersionPrefix).$(BUILD_BUILDID)</VersionPrefix>
</PropertyGroup>
</Project>
2 changes: 2 additions & 0 deletions LetsEncrypt.Azure.DotNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
Dockerfile = Dockerfile
readme.md = readme.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt.Azure.FunctionV2", "examples\LetsEncrypt.Azure.FunctionV2\LetsEncrypt.Azure.FunctionV2.csproj", "{FF8A14C9-8AC7-4057-A2EC-BA31C3965079}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace LetsEncrypt.Azure.FunctionV2
public static class AutoRenewCertificate
{
[FunctionName("AutoRenewCertificate")]
public static async Task Run([TimerTrigger("24:00:00", RunOnStartup = true)]TimerInfo myTimer, ILogger log)
public static async Task Run([TimerTrigger("%CertRenewSchedule%", RunOnStartup = false)]TimerInfo myTimer, ILogger log)
{
log.LogInformation($"Renewing certificate at: {DateTime.Now}");

Expand Down
15 changes: 12 additions & 3 deletions examples/LetsEncrypt.Azure.FunctionV2/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,18 @@ public static async Task InstallOrRenewCertificate(ILogger log)
IServiceCollection serviceCollection = new ServiceCollection();

serviceCollection.AddSingleton<ILogger>(log)
.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Information)
.AddAzureAppService(Configuration.GetSection("AzureAppService").Get<AzureWebAppSettings>())
.AddSingleton<IKeyVaultClient>(kvClient)
.Configure<LoggerFilterOptions>(options => options.MinLevel = LogLevel.Information);
var certificateConsumer = Configuration.GetValue<string>("CertificateConsumer");
if (string.IsNullOrEmpty(certificateConsumer))
{
serviceCollection.AddAzureAppService(Configuration.GetSection("AzureAppService").Get<AzureWebAppSettings>());
}
else if (certificateConsumer.Equals("NullCertificateConsumer"))
{
serviceCollection.AddNullCertificateConsumer();
}

serviceCollection.AddSingleton<IKeyVaultClient>(kvClient)
.AddKeyVaultCertificateStore(vaultBaseUrl);


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault;
using System.Web.Http;

namespace LetsEncrypt.Azure.FunctionV2
{
Expand All @@ -31,8 +32,8 @@ public static async Task<IActionResult> Run(
return new OkResult();
} catch(Exception ex)
{

return new ObjectResult(ex.ToString());
log.LogError(ex.ToString());
return new ExceptionResult(ex, true);

}
}
Expand Down
5 changes: 4 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Let's Encrypt Azure
[![Build status](https://dev.azure.com/letsencrypt/letsencrypt/_apis/build/status/LetsEncrypt-Azure-CI)](https://dev.azure.com/letsencrypt/letsencrypt/_build/latest?definitionId=4)

The easiest and most robust method for deploying Let's Encrypt Wild Card Certificate to Azure Web Apps.

# Getting Started
## Azure DNS + Azure Web
Deployment template for setting up Let's Encrypt wild card certificate for Azure Web App (hosting plan and web app must be colocated in same resource group). Hostname must already be configured on the Web App and the DNS must be setup in Azure.

<a href="https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsjkp%2Fletsencrypt-azure%2Fmaster%2Fsrc%2FLetsEncrypt.Azure.ResourceGroup%2FTemplates%2Fletsencrypt.functionapp.renewer.json" target="_blank"><img src="http://azuredeploy.net/deploybutton.png"/></a>

# What is Let's Encrypt Azure
Expand All @@ -26,7 +29,7 @@ Let's Encrypt Azure is my second attempt to bring support for Let's Encrypt cert
# How it works
Let's Encrypt Azure, works by deploying a resouce group with an Azure Function that runs code that talks to Let's Encrypt to request and renew the certificate, using the DNS challenge. Since DNS challenge is used the Function app needs access to the DNS provider used for the domain. All secrets required for the process are stored in Azure Key Vault. Once a certificate is generated it can be stored a various certificate storage locations and consumed by different certificate consumers. It used application insights for storing logs.

![Overview of infrastructure](media/letsencrypt-azure-overview.png)
![Overview of infrastructure](media/letsencrypt-azure-overiew.png)

## Certificate Storage
The recommend certificate storage location is Azure Key Vault, but is is possible to configure the Azure Function to store the certificate in Azure Blob Storage as well.
Expand Down
16 changes: 15 additions & 1 deletion src/LetsEncrypt.Azure.Core.V2/AcmeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task<CertificateInstallModel> RequestDnsChallengeCertificate(IAcmeD

logger.LogInformation("Finished validating dns challenge token, response was {ChallengeStatus} more info at {ChallengeStatusUrl}", chalResp.Status, chalResp.Url);

var privateKey = KeyFactory.NewKey(KeyAlgorithm.RS256);
var privateKey = await GetOrCreateKey(acmeConfig.AcmeEnvironment.BaseUri, acmeConfig.Host);
var cert = await order.Generate(new Certes.CsrInfo
{
CountryName = acmeConfig.CsrInfo.CountryName,
Expand Down Expand Up @@ -101,6 +101,20 @@ public async Task<CertificateInstallModel> RequestDnsChallengeCertificate(IAcmeD
};
}

private async Task<IKey> GetOrCreateKey(Uri acmeDirectory, string host)
{
string secretName = $"privatekey{host}--{acmeDirectory.Host}";
var key = await this.certificateStore.GetSecret(secretName);
if (string.IsNullOrEmpty(key))
{
var privatekey = KeyFactory.NewKey(KeyAlgorithm.RS256);
await this.certificateStore.SaveSecret(secretName, privatekey.ToPem());
return privatekey;
}

return KeyFactory.FromPem(key);
}

private async Task<AcmeContext> GetOrCreateAcmeContext(Uri acmeDirectoryUri, string email)
{
AcmeContext acme = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using LetsEncrypt.Azure.Core.V2.CertificateConsumers;

namespace LetsEncrypt.Azure.Core.V2
{
public class AzureWebAppService
public class AzureWebAppService : ICertificateConsumer
{
private readonly AzureWebAppSettings[] settings;
private readonly ILogger<AzureWebAppService> logger;
Expand Down Expand Up @@ -88,26 +89,30 @@ private static IAppServiceManager GetAppServiceManager(AzureWebAppSettings setti
return new AppServiceManager(restClient, settings.AzureSubscription.SubscriptionId, settings.AzureSubscription.Tenant);
}

public List<string> RemoveExpired(int removeXNumberOfDaysBeforeExpiration = 0)
public async Task<List<string>> CleanUp()
{
return await this.CleanUp(0);
}
public async Task<List<string>> CleanUp(int removeXNumberOfDaysBeforeExpiration = 0)
{
var removedCerts = new List<string>();
foreach (var setting in this.settings)
{
var appServiceManager = GetAppServiceManager(setting);
var certs = appServiceManager.AppServiceCertificates.ListByResourceGroup(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName);
var certs = await appServiceManager.AppServiceCertificates.ListByResourceGroupAsync(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName);

var tobeRemoved = certs.Where(s => s.ExpirationDate < DateTime.UtcNow.AddDays(removeXNumberOfDaysBeforeExpiration) && (s.Issuer.Contains("Let's Encrypt") || s.Issuer.Contains("Fake LE"))).ToList();

tobeRemoved.ForEach(s => RemoveCertificate(appServiceManager, s, setting));
tobeRemoved.ForEach(async s => await RemoveCertificate(appServiceManager, s, setting));

removedCerts.AddRange(tobeRemoved.Select(s => s.Thumbprint).ToList());
}
return removedCerts;
}

private void RemoveCertificate(IAppServiceManager webSiteClient, IAppServiceCertificate s, AzureWebAppSettings setting)
private async Task RemoveCertificate(IAppServiceManager webSiteClient, IAppServiceCertificate s, AzureWebAppSettings setting)
{
webSiteClient.AppServiceCertificates.DeleteByResourceGroup(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName, s.Name);
await webSiteClient.AppServiceCertificates.DeleteByResourceGroupAsync(setting.ServicePlanResourceGroupName ?? setting.ResourceGroupName, s.Name);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using LetsEncrypt.Azure.Core.V2.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace LetsEncrypt.Azure.Core.V2.CertificateConsumers
{
public interface ICertificateConsumer
{
/// <summary>
/// Installs/assigns the new certificate.
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
Task Install(ICertificateInstallModel model);

/// <summary>
/// Remove any expired certificates
/// </summary>
/// <returns>List of thumbprint for certificates removed</returns>
Task<List<string>> CleanUp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LetsEncrypt.Azure.Core.V2.Models;

namespace LetsEncrypt.Azure.Core.V2.CertificateConsumers
{
/// <summary>
/// Certificate consumer that does do anything.
/// </summary>
public class NullCertificateConsumer : ICertificateConsumer
{
public Task<List<string>> CleanUp()
{
return Task.FromResult(new List<string>());
}

public Task Install(ICertificateInstallModel model)
{
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>Library for easy retrieval of Let's Encrypt wildcard certificates using version 2 api. Support easy install to Azure Web Apps and storage in Azure Key Vault or Blob Storage.</Description>
<Version>1.0.0-beta01</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Description>Library for easy retrieval of Let's Encrypt wildcard certificates using version 2 api. Support easy install to Azure Web Apps and storage in Azure Key Vault or Blob Storage.</Description>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>

<ItemGroup>
Expand Down
12 changes: 7 additions & 5 deletions src/LetsEncrypt.Azure.Core.V2/LetsencryptService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using LetsEncrypt.Azure.Core.V2.CertificateStores;
using LetsEncrypt.Azure.Core.V2.CertificateConsumers;
using LetsEncrypt.Azure.Core.V2.CertificateStores;
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -11,14 +12,15 @@ public class LetsencryptService
{
private readonly AcmeClient acmeClient;
private readonly ICertificateStore certificateStore;
private readonly ICertificateConsumer certificateConsumer;
private readonly AzureWebAppService azureWebAppService;
private readonly ILogger<LetsencryptService> logger;

public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateStore, AzureWebAppService azureWebAppService, ILogger<LetsencryptService> logger = null)
public LetsencryptService(AcmeClient acmeClient, ICertificateStore certificateStore, ICertificateConsumer certificateConsumer, ILogger<LetsencryptService> logger = null)
{
this.acmeClient = acmeClient;
this.certificateStore = certificateStore;
this.azureWebAppService = azureWebAppService;
this.certificateConsumer = certificateConsumer;
this.logger = logger ?? NullLogger<LetsencryptService>.Instance;
}
public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBeforeExpiration)
Expand All @@ -45,10 +47,10 @@ public async Task Run(AcmeDnsRequest acmeDnsRequest, int renewXNumberOfDaysBefor
Host = acmeDnsRequest.Host
};
}
await azureWebAppService.Install(model);
await certificateConsumer.Install(model);

logger.LogInformation("Removing expired certificates");
var expired = azureWebAppService.RemoveExpired();
var expired = await certificateConsumer.CleanUp();
logger.LogInformation("The following certificates was removed {Thumbprints}", string.Join(", ", expired.ToArray()));

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using LetsEncrypt.Azure.Core.V2.CertificateStores;
using LetsEncrypt.Azure.Core.V2.CertificateConsumers;
using LetsEncrypt.Azure.Core.V2.CertificateStores;
using LetsEncrypt.Azure.Core.V2.DnsProviders;
using LetsEncrypt.Azure.Core.V2.Models;
using Microsoft.Azure.KeyVault;
Expand Down Expand Up @@ -64,6 +65,16 @@ public static IServiceCollection AddAcmeClient<TDnsProvider>(this IServiceCollec
.AddTransient<IDnsProvider, TDnsProvider>();
}

public static IServiceCollection AddNullCertificateConsumer(this IServiceCollection serviceCollection)
{

return serviceCollection
.AddTransient<ICertificateConsumer, NullCertificateConsumer>()
.AddTransient<LetsencryptService>();
}



public static IServiceCollection AddAzureAppService(this IServiceCollection serviceCollection, params AzureWebAppSettings[] settings)
{
if (settings == null || settings.Length == 0)
Expand All @@ -73,7 +84,7 @@ public static IServiceCollection AddAzureAppService(this IServiceCollection serv

return serviceCollection
.AddSingleton(settings)
.AddTransient<AzureWebAppService>()
.AddTransient<ICertificateConsumer, AzureWebAppService>()
.AddTransient<LetsencryptService>();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,11 @@
<Visible>False</Visible>
</None>
<Content Include="Deploy-AzureResourceGroup.ps1" />
<Content Include="Templates\letsencrypt.azure.core.json" />
<Content Include="Templates\letsencrypt.azure.core.parameters.json" />
<None Include="Scripts\Create-CoreInfrastructure.ps1" />
<None Include="Scripts\Import-Certificate.ps1" />
<None Include="Scripts\KeyVault.pfx" />
<None Include="Scripts\New-Certificate.ps1" />
<None Include="Templates\letsencrypt.functionapp.renewer.parameters.json" />
<None Include="Templates\letsencrypt.azure.website.parameters.json" />
<None Include="Templates\letsencrypt.azure.website.json" />
<None Include="Templates\letsencrypt.functionapp.renewer.json" />
</ItemGroup>
<Target Name="GetReferenceAssemblyPaths" />
Expand Down
Loading

0 comments on commit e863482

Please sign in to comment.