Lets Encrypt on Azure Web Apps using a Function App for Automated Renewal

24/12/2017

In my original incarnation of Lets Encrypt Site Extension for Azure Web app, there was a very tightly coupling between web app that hosted the site extension and the site the certificate was requested for. It relied on environment variables in the Web App and it also used Azure Web Job that had to be hosted as part of the Web App. That worked fine for many small single site deployments, but the Lets Encrypt site extension is also being used by in more advanced scenarios than I originally anticipated, where the tight coupling is less than ideal. As a result of that, I have over the last year with every release refactored parts of the code, to make it more modular and allow it to be used to request certificates for other web apps than the one the site extension is installed in.

This has been possible since release 0.8.5, but I haven’t taken the time to write up any documentation on how to do so. Yes, I know, I have been slacking off, but writing documentation is not my preferred spare time job 🙂

Currently there are three ways to use the site extension:

A final note on the different versions, the 64 bit version is not being supported anymore, the reason being that you can now safely use the two main version also on 64 bit environments, so there is no reason for me to continue to publish a separate 64 bit version. If you are still on the 64 bit version please upgrade to 0.8.5.

Using Azure Function for Automated Lets Encrypt Certificate Renewal for Azure Web Apps

Lets take a look at how you can use the “Azure Let’s Encrypt (No Web Jobs)” site extension to request Lets Encrypt certificates for any Azure Web App in your Azure Subscription.

Let’s first take a look at how my azure resources are setup.

The first thing I did was to create a Service Principal in my Azure AD (as you also would with the old setup).

Then I created a resource group that I called Lets-Encrypt-Management, within this resource group I deployed a Azure Function App on the Dynamic Plan. Onto that Function App I installed the Lets Encrypt Site Extension without the Web
Job.
To install a site extension in a Function App you select “Platform Features > Extensions”

Then search for lets encrypt and pick the extension named “Azure Let’s Encrypt (No Web Jobs)”, once installed it should look like this:

Finally in the last resource group I have Web Site with custom domain name attached, which I want to request a Let’s Encrypt certificate for. My service principal has been granted contribute permission to this resource group. In practice I could have any number of web sites, all being managed by separate functions inside a single function app.

With is setup in place, it is time to write a function app that can request and install a certificate, and renew it.

My preferred language is C#, so I choose a C# Timer Triggered function (but the code should be easy to port to any other language).

The great thing about the Timer Triggered functions are that you can manually invoke them, as well as having them run on a predefined schedule. That way you can manually invoke them to make the first installation of the certificate, and after the first invocation they will get triggered with the frequency you choose. A starting point for the cron schedule could be to run it on the first of every month with 0 0 0 1 * *, you can look up more cron expressions here https://codehollow.com/2017/02/azure-functions-time-trigger-cron-cheat-sheet/, just remember that you can only request 5 certificates per week per domain. Also if you want to be nice to Lets Encrypt you pick a random minutes and or day, so that everyone using this setup in Azure don’t hit the Lets Encrypt servers at the same time.

With the cron schedule configured and the function created it is time to write some code.

The code we are going to write, is nothing more than a simple HTTP call to the API endpoint in the Lets Encrypt extension that we installed in the function app. Obviously we need to supply the right parameters to the API, for it to request and install the certificate for the right web app, further more we need to provide a basic authentication header for the request, as we can’t call the API hosted in the site extension without it.

As the site extension is hosted in the Kudu Portal, we can use the publishing credentials for the function app to authenticate our call. You can download the publishing credentials from the portal and open the file in e.g. notepad.

The values that are of interest is the userName and userPWD.

The code of my function app is the following (note I have removed passwords and you have to replace tenant and subscription information as well as URLs with your own)
[csharp]
#r "Newtonsoft.Json"

using System;
using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
using System.Threading;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
log.Info($"C# Timer trigger function executed at: {myTimer.ToString()}");
var userName = "$YOUR-PUBLISHING-CREDENTIAL-USER";
var userPWD = "YOUR-PUBLISHING-CREDENTIAL-PASSWORD";
var client = new HttpClient();
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{userName}:{userPWD}")));

var body = new {
AzureEnvironment = new {
//AzureWebSitesDefaultDomainName = "string", //Defaults to azurewebsites.net
//ServicePlanResourceGroupName = "string", //Defaults to ResourceGroupName
//SiteSlotName = "string", //Not required if site slots isn’t used
WebAppName = "webappcfmv5fy7lcq7o",
//AuthenticationEndpoint = "string", //Defaults to https://login.windows.net/
ClientId = "0fe33f98-e1cd-47ad-80c1-f8578fe3cfc8",
ClientSecret = "YOUR-CLIENT-SECRET",
//ManagementEndpoint = "string", //Defaults to https://management.azure.com
ResourceGroupName = "sjkp.letsencrypttest", //Resource group of the web app
SubscriptionId = "14fe4c66-c75a-4323-881b-ea53c1d86a9d",
Tenant = "f386b536-faf3-4000-adec-1f6d78dbf0bf", //Azure AD tenant ID
//TokenAudience = "string" //Defaults to https://management.core.windows.net/
},
AcmeConfig = new {
RegistrationEmail = "[email protected]",
Host = "letsencrypt.sjkp.dk",
AlternateNames = new string[
]{},
RSAKeyLength = 2048,
PFXPassword = "pass@word1", //Replace with your own
UseProduction = false //Replace with true if you want production certificate from Lets Encrypt
},
CertificateSettings = new {
UseIPBasedSSL = false
},
AuthorizationChallengeProviderConfig = new {
DisableWebConfigUpdate = false
}
};

var res = await client.PostAsync("https://[REPLACE-WITH-URL-OF-FUNCTIONAPP].scm.azurewebsites.net/letsencrypt/api/certificates/challengeprovider/http/kudu/certificateinstall/azurewebapp?api-version=2017-09-01",
new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json"));

log.Info(await res.Content.ReadAsStringAsync());
}

[/csharp]

With this relatively short function you can request certificates for your Web App. Please be aware that it is not good security practices to keep secrets in the code, I will post a follow up to this article, where I go into depth with this aspect. But if you are less concerned about security, then the above will work, and it is also a simpler way for me to demo the functionality.

Another question that you might have is why don’t we not just use the nuget package directly from the function app, why bother with installing the site extension and calling its API, when we could do everything from code?. Well the main reason is that the Function App runtime have some conflicting assembly versions with the assemblies that the nuget uses, thus it simply wont work, because Function apps at the moment doesn’t support assembly binding redirects. That is going to change, I expect, and once it does I will look into that approach. Another reason for doing it this way is that I wanted to demo, how to use the API, because you can use the API from other platforms, e.g. Logic Apps or Microsoft flow, to accomplish the same as this function code does but in a more declarative manner, if you prefer that.