Serving Let's Encrypt ACME challenge files from ASP.NET Core, part 2
Posted: (EET/GMT+2)
Pretty much all web applications need a TLS certificate, especially if they are to be public applications. I like to use ASP.NET for those, and so, if you prefer to use Let's Encrypt certificates, you will need to learn how an ASP.NET Core application can also serve Let's Encrypt HTTP-01 challenge files.
This post is a revisit of my February post in which I showed an example of using static files as the routing. Today's approach is more dynamic in the routing part, and I wanted to talk a little more about the actual challenge itself. So here goes.
You can read more about the HTTP-01 challenge at Let's Encrypt web site, but shortly put, it requires you to serve an extenionless text file at a certain specific folder at the root of your ASP.NET application. You could place that challenge file under wwwroot, but if you do it after the application has been deployed, the defailt routing would return a 404 Not Found error instead, because files not present at build/publish time are not served.
The HTTP-01 challenge uses this URL shape:
http://example.com/.well-known/acme-challenge/{token}
The token has no file extension. That is important, because some static file configurations
are easier to get wrong when the file does not look like a normal .txt file.
One simple solution is to map the challenge route directly in Program.cs:
app.MapGet("/.well-known/acme-challenge/{token}",
(string token, IWebHostEnvironment environment) =>
{
if (token != Path.GetFileName(token))
{
return Results.BadRequest();
}
string challengeFile = Path.Combine(
environment.WebRootPath,
".well-known", "acme-challenge",
token);
return File.Exists(challengeFile)
? Results.File(challengeFile, "text/plain")
: Results.NotFound();
})
.AllowAnonymous();
This code reads files from:
wwwroot\.well-known\acme-challenge\
For example, if the ACME client creates this file:
wwwroot\.well-known\acme-challenge\abc123
...then ASP.NET Core can serve it from:
http://example.com/.well-known/acme-challenge/abc123
Note that you need to serve plain HTTP at the moment when Let's Encrypt reads the data back from the server. Once the process is complete, you won't need plain HTTP anymore and can continue to switch back to HTTPS.
Note also how in the above code the Path.GetFileName check is intentional. It rejects unexpected path-like values and keeps the route limited to a single file name.
The endpoint also uses AllowAnonymous. Certificate validation must be reachable without normal application authentication. Most applications require authentication, so be sure test this URL separately before relying on automatic renewal.
You can also create a temporary test file manually:
mkdir wwwroot\.well-known\acme-challenge echo hello > wwwroot\.well-known\acme-challenge\test-token
Then check that the browser or curl returns:
hello
Good things to verify:
- the route is reachable without login
- the file is served as plain text
- the challenge token has no extension
- the endpoint works through the reverse proxy or load balancer
- HTTP to HTTPS redirects do not break validation.
For a single server, this is often enough. For multiple frontends, remember that Let's Encrypt must be able to read the challenge response from the public endpoint it checks. Make sure the challenge file is available on the correct server, shared storage, or routed to a common validation endpoint.
This is not a replacement for a full certificate automation strategy, but it is a practical small fix for ASP.NET Core applications that need to serve ACME challenge files reliably.
Happy hacking!