DEV Community

John Henderson
John Henderson

Posted on

Automatic HTTPS on Kestrel in 2026, now that LettuceEncrypt is archived

If you run an ASP.NET Core app directly on Kestrel, with no nginx or cloud load balancer in front, getting a real TLS certificate has always been the awkward part. For years the answer was LettuceEncrypt, which obtained and renewed a Let's Encrypt certificate inside your app. That project was archived in April 2025, and its last release targets .NET 6. So the question is open again: how do you do this now?

Two things have changed, and both matter.

Certificates are getting shorter

Let's Encrypt started issuing six-day certificates this year, and it is taking the default lifetime from 90 days down to 45. The motivation is security, since a leaked key is useful for less time, but the practical effect is that manual renewal is finished. A certificate you rotate by hand, or with a cron job on a fixed schedule, will not keep up. Renewal has to be automatic, and it has to be frequent.

Renewal timing is no longer a guess

Alongside the shorter lifetimes, the authority now tells you when to renew. ACME Renewal Information (RFC 9773, published September 2025) hands the client a suggested renewal window. Instead of picking an arbitrary threshold like "renew when 30 days are left," the client asks the CA and follows the window it returns. That also lets the CA spread renewals out so it is not hit by everyone at once.

What that means for a .NET app

You want a client that runs in process, answers the ACME challenge from your own pipeline, and renews on the CA's schedule with no restart. LettuceEncrypt did the first two but never implemented RFC 9773, and it is no longer maintained. FluffySpoon's EncryptWeMust is in a similar state.

I wrote one for this, AutoHttps (disclosure: I am the author, it is MIT on GitHub). Setup is one call:

builder.Services.AddAutoHttps(options =>
{
    options.DomainNames.Add("example.com");
    options.EmailAddress = "[email protected]";
    options.AcceptTermsOfService = true;
});
Enter fullscreen mode Exit fullscreen mode

That gets a certificate on first start and keeps it renewed. It answers http-01 from your request pipeline, does dns-01 and wildcards, works with Let's Encrypt or any ACME authority, and has no NuGet dependencies, because the RFC 8555 client is written against the shared framework. For the short-lived certificates above, you opt into the profile:

options.Profile = CertificateProfiles.ShortLived;
Enter fullscreen mode Exit fullscreen mode

The one catch

This only works if Kestrel is the thing terminating TLS. If nginx, IIS, or a load balancer in front holds the certificate, the cert belongs there and an in-process client cannot help. That is the honest boundary: in-process ACME is for the case where your app is the edge.

Coming from LettuceEncrypt

The APIs are close. DomainNames, EmailAddress, and AcceptTermsOfService keep the same names, so most of a migration is renaming the config section and deleting the UseLettuceEncrypt call. There is a short migration guide in the repo that maps the rest.

Repo: https://github.com/astralmaster/AutoHttps
NuGet: https://www.nuget.org/packages/AutoHttps

Top comments (2)

Collapse
 
iqtechsolutions profile image
Ivan Rossouw

One operational guardrail I’d add is monitoring the certificate actually served at the edge, not only successful ACME orders. In a scaled app there are at least three states: issued by the CA, persisted to shared storage, and selected by each Kestrel instance; a TLS-terminating proxy adds another boundary. I’d record time-to-expiry and the served thumbprint per hostname and replica, then compare those with an external TLS probe. A useful failure drill is to let renewal succeed while one replica cannot read the updated store. Alerting should identify the stale served fingerprint before expiry, rather than merely recording a successful order. Does AutoHttps expose those states today, or is the intended backstop its event IDs plus an external probe?

Collapse
 
jhenderson1992 profile image
John Henderson

Yes, it's correct to watch the served certificate, not the order. The current version exposes per-replica state: an inspector that returns the served thumbprint and expiry date, a metric for the seconds remaining, and a health check, each reported by the instance itself. A replica that cannot read the updated store then shows a stale thumbprint and a shrinking margin while its peers reset, logs the store read failure (event 114), and never logs the adopt (event 120). That is enough to alert on the stale fingerprint before it expires.

What it does not do is cross the process boundary: it will not aggregate across replicas, and it cannot see what a TLS-terminating proxy serves at the edge. Your comment makes me want to add the served thumbprint as an attribute on the expiry metric, so a dashboard can group by fingerprint and catch a diverging replica without scraping each pod.