Lets Encrypt Azure Functions and Managed Service Identity

25/12/2017

In my previous blog post I demonstrated how, Azure functions together with the API of the Lets Encrypt Site Extension can be used to request SSL certificates. I also did a sloppy job in the last post to secure the secrets for the service principal and the publishing credential, so I will try to fix that in this post.

Application code shouldn’t contain any secrets, and a good way to avoid that, is to use the recently added Azure feature Managed Service Identity (MSI). MSI allows your application code to retrieve secrets from e.g. Azure Key Vault in a easy to use and secure way.

Before you can use MSI you have to enable it for you Web App or Function App. Enabling MSI creates a service principal with the name of your Web App, and adds some environment variables to your Web App that allows your application code to use that service principal to retrieve secrets from Azure Key Vault, or Access Tokens for other Azure Resources.

You enable it under “Platform Features > Managed Service Identity”

Since my Function App is named letsencryptmgt, my service principal will be named the same.

Once I have enabled MSI and gotten my Service Principal, it is time to grant it access to the Key Vault that the secrets are stored in. If you store secrets as secrets in the Vault, the service principal should have at least Get permissions for that.

I have also granted mine Get rights for Keys and actually (not shown in the screenshot) I also gave it Import permissions to Certificates, because then I can save the retrieved Lets Encrypt certificate in my key vault. Currently I don’t have any reason for storing the certificate, but thought it would be nice to show that it is possible.

With the permissions for the key vault in place, it is time to save the two secrets that the code from the previous blog posts need. I save the publishing credentials as a secret named publishingpassword and the Client Secret for the service principal used by the Site Extension as clientsecret.

Using the MSI to retrieve secrets from Key Vault from a function app, is very easy, it is basically 3 lines of code and the addition of a two nuget packages.
[csharp]
AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));

var publishingSecret = await keyVaultClient.GetSecretAsync("https://letsencrypt-keyvault.vault.azure.net/secrets/publishingpassword").ConfigureAwait(false);
[/csharp]

The thing to notice is that the GetSecretAsync method uses the secret identifier (the URL of the secret), which in my case is https://letsencrypt-keyvault.vault.azure.net/secrets/publishingpassword yours will obviously be different depending on what you named your Key Vault and secret.

For above to work you have to add the following two nugets (you do that by adding a project.json to your function app)
[js]
{
"frameworks": {
"net46":{
"dependencies": {
"Microsoft.Azure.KeyVault": "2.3.2",
"Microsoft.Azure.Services.AppAuthentication": "1.1.0-preview"
}
}
}
}
[/js]

Now that we know hos to use MSI to retrieve Key Vault secrets, lets take a look at how the improved Function code for requesting Lets Encrypt Certificates using the API could look.

[csharp]
using System;
using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
using System.Threading;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;

public static async Task Run(TimerInfo myTimer, TraceWriter log)
{
log.Info($"C# Timer trigger function executed at: {DateTime.Now}");
AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));

var publishingSecret = await keyVaultClient.GetSecretAsync("https://letsencrypt-keyvault.vault.azure.net/secrets/publishingpassword").ConfigureAwait(false);

var clientSecret = await keyVaultClient.GetSecretAsync("https://letsencrypt-keyvault.vault.azure.net/secrets/clientsecret").ConfigureAwait(false);

log.Info($"C# Timer trigger function executed at: {myTimer.ToString()}");
var userName = "$letsencryptmgt";
var userPWD = publishingSecret.Value;
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 = clientSecret.Value,
//ManagementEndpoint = "string", //Defaults to https://management.azure.com
ResourceGroupName = "sjkp.letsencrypttest",
SubscriptionId = "14fe4c66-c75a-4323-881b-ea53c1d86a9d",
Tenant = "f386b536-faf3-4000-adec-1f6d78dbf0bf",
//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",
UseProduction = false
},
CertificateSettings = new {
UseIPBasedSSL = false
},
AuthorizationChallengeProviderConfig = new {
DisableWebConfigUpdate = false
}
};

var res = await client.PostAsync("https://letsencryptmgt.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"));

var content = await res.Content.ReadAsStringAsync();

var response = JsonConvert.DeserializeObject<dynamic>(content);

await KeyVaultClientExtensions.ImportCertificateAsync(keyVaultClient, "https://letsencrypt-keyvault.vault.azure.net","letsencrypt-sjkp-dk", response.CertificateInfo.PfxCertificate.ToString(), response.CertificateInfo.Password.ToString());
}
[/csharp]

The last 3 lines of code shows how you can read the response from the Lets Encrypt Site Extension API and retrieve the obtained certificate and store it back into Key Vault. The response from the API contains the certificate base64 encoded, a format which the KeyVaultClientExtensions.ImportCertificateAsync are able to store directly.

You might ask why would this be useful. One good use case would be if you used the DNS challenge flow that the site extension also supports (which I should explain in another another post), then you can get a certificate without having a web app to install it into as long as you use Azure DNS for the domain name that you are requesting the certificate for. When the certificate is retrieved and safely stored in Key Vault it can be used in e.g. VMs, with Azure App Service Certificate or together with Traffic Manager and hopefully one day Azure CDN when they allow bringing your own SSL certificates.