Let's Encrypt certificates for ASP.NET Core on Azure

Let's Encrypt is a new certificate authority that provides free certificates for web server validation. It issues domain-validated (DV) certificates meaning that the certificate authority has proven that the requesting party has control over some DNS domain (more on that later). And the best thing: it's fully automated through an API and a command-line client.

Free DV certificates seem to be the new trend nowadays with Symantec being the next player in the market announcing they're giving them away for free. Let's Encrypt issued their first certificate on September 14, 2015 and announced on March 8, 2016 that they were at one million after just three months in public beta.

ASP.NET Core

I happen to be developing an ASP.NET Core website for a customer of ours that required a certificate so Let's Encrypt seemed to make sense1. The easiest way to automatically connect a Let's Encrypt certificate to an Azure web site is via the excellent Let's Encrypt site extension by Simon J.K. Pedersen. Please note that there's both a x86 and x64 version.

There's some excellent guidance on installing and configuring the extension elsewhere on the web so I won't go into details on that. What I'd like to discuss is how to configure your ASP.NET Core web application in such a way that Let's Encrypt actually returns a certificate when asked to do so.

ACME

You may wonder: how does Let's Encrypt actually validate a certificate request? It issues domain-validated certificates so how does it actually validate that you are the owner of the domain? Enter ACME: the Automatic Certificate Management Environment. ACME is an IETF internet draft (and still a work-in-progress, for the latest version, check out their GitHub repo).

The entire purpose of the ACME specification is to provide a contract between a certificate authority and an entity requesting a certificate (the applicant) so that the certificate request process can be entirely automated.

If an applicant requests a certificate, he has to provide the URL to which the certificate should be applied, e.g. example.com. Let's Encrypt now expects a number of files to present at the following (browsable) url: http://example.com/.well-known/acme-challenge/. In my website, the contents of this folder are the following:

ACME challenge

So that's how Let's Encrypt checks that you own the domain: by checking the presence of a specific set of files in a specific location on the domain you claim to be the owner of. In official terms this is called challenge-response authentication. Please read the ACME specification if you want to know what these files actually mean.

The Let's Encrypt site extension makes sure there is a .well-known/acme-challenge folder in the wwwroot folder of your site and that it has the correct contents2. Here's the same folder as seen from the KUDU console:

Back to ASP.NET Core

So all is well and we call upon our site extension to request and install the certificate. And, well, nothing happens, no errors but also no certificate. The output from the Azure WebJob functions that execute the request provides no details at all (not even in the logging):

What goes wrong is actually two things:

  • ASP.NET Core disables directory browsing by default and the .well-known/acme-challenge folder must be browsable
  • ASP.NET Core (and IIS for that matter) do not by default serve files without extension

So how do we fix this? As with most ASP.NET Core configuration: through middleware. First some code, then the explanation:

public class Startup  
{
  ...
  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
    var rootPath = Path.GetFullPath(".");
    var acmeChallengePath =
        Path.Combine(rootPath, @".well-known\acme-challenge");

    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
      FileProvider = new PhysicalFileProvider(acmeChallengePath),
      RequestPath = new PathString("/.well-known/acme-challenge"),
    });

    app.UseStaticFiles(new StaticFileOptions
    {
      ServeUnknownFileTypes = true
    });
  }
}

Most of the code should be clear but some points of interest:

  • The directory browser middleware must be configured with an absolute path so I take the current path (D:\home\site\wwwroot) and append .well-known\acme-challenge to it.
  • This configuration ensures that only the acme-challenge folder is browsable, not every folder in your website.
  • The static files middleware is configured to serve unknown file types to clients. The files in the acme-challenge folder are all extensionless so without a known file type.
  • It's impossible to limit serving of unknown file types to a specific folder.

So, with this middleware configuration in place we can again request a certificate and this time it will work. At least it did for me ;-)

I hope this post has given you some background information on cool new technology like Let's Encrypt and ACME and will help you in setting up Let's Encrypt for your ASP.NET Core websites.

Notes
  1. We actually tried the old-school way of getting a certificate first but that took so much time we decided to try the Let's Encrypt route.
  2. The Azure Let's Encrypt site extension uses the ACMESharp library for actual communication with the Let's Encrypt API.