HTTPS, HSTS and Certificates
Redirect HTTP to HTTPS, learn what the HSTS header commits you to, trust the dev certificate, and terminate TLS at a proxy.
app.UseHsts(); app.UseHttpsRedirection();HTTPS is ordinary HTTP carried inside an encrypted connection. The encryption is called TLS. A certificate is a file a server presents to prove it owns a domain name. The browser checks that certificate and refuses to continue if it does not match the name in the address bar.
Redirecting
app.UseHttpsRedirection() answers a plain HTTP request with a redirect to the same URL on https. The default status code is 307. In production set RedirectStatusCode to 308, so caches and clients treat the move as permanent. The redirect itself travels in the clear, so the first request is still visible to anyone on the network. Redirecting is a courtesy to humans typing URLs, not a defence.
HSTS
HSTS is a single response header, Strict-Transport-Security, sent by app.UseHsts(). It tells the browser to use HTTPS for this host and nothing else. MaxAge defaults to 30 days. Turn on IncludeSubDomains only when every subdomain really has a certificate. The header is only sent on HTTPS responses, and loopback addresses are excluded by default, which is why you never see it while developing.
The development certificate
Run dotnet dev-certs https --trust once per machine. It installs a certificate that only your machine trusts.
TLS at a proxy
Behind nginx or a Kubernetes ingress, the proxy holds the real certificate and speaks plain HTTP to your app. Your app then believes every request is insecure. app.UseForwardedHeaders() reads X-Forwarded-Proto and corrects Request.Scheme. Call it first.
Example
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
});
// 308 instead of the default 307: the move to HTTPS is permanent.
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
});
var app = builder.Build();
// Never in Development: the header would stick to localhost for a year.
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.MapGet("/clock", () => "UTC");
app.Run();When to use it
- Force every browser request to a public site onto HTTPS and keep it there.
- Stop a redirect loop on an app running behind a Kubernetes ingress that terminates TLS.
- Get localhost HTTPS working again after a browser starts rejecting the development certificate.
More examples
Behind a reverse proxy or ingress
The proxy terminates TLS, so the request arrives at your app as plain HTTP. Without UseForwardedHeaders, /scheme reports "http", UseHttpsRedirection redirects a request that was already secure, and you get a redirect loop. Reach for this whenever the app runs behind an ingress, a load balancer or a CDN.
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// Clearing both lists trusts whoever sent the headers. That is only safe
// when the app is reachable exclusively through the proxy, as in a
// Kubernetes cluster where the pod port is not published.
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var app = builder.Build();
// First, so later middleware sees the scheme the client actually used.
app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.MapGet("/scheme", (HttpContext http) => http.Request.Scheme);
app.Run();Trusting the development certificate
The development certificate is signed by your own machine, not by a public authority, so only your machine accepts it. Reach for the clean-and-trust pair when a browser or an HttpClient suddenly reports an untrusted certificate on localhost after an OS or SDK upgrade.
# Install and trust a certificate for localhost. Once per machine.
dotnet dev-certs https --trust
# If the browser starts rejecting localhost, remove it and start again.
dotnet dev-certs https --clean
dotnet dev-certs https --trustAn API that refuses plain HTTP outright
For a service with no browser traffic, redirecting is worse than refusing: the client has already sent the body over HTTP. Opening only an HTTPS endpoint means a plain HTTP attempt fails at connect time. Reach for this on internal APIs that handle card data or tokens.
var builder = WebApplication.CreateBuilder(args);
// Only an HTTPS endpoint is opened, so there is no HTTP port to redirect from.
// An API client that follows a redirect may resend a request body in the clear;
// failing the connection is honest about the problem.
builder.WebHost.UseUrls("https://*:8443");
var app = builder.Build();
app.MapPost("/payments", (Payment payment) => Results.Accepted($"/payments/{payment.Id}"));
app.Run();
record Payment(string Id, decimal Amount);
Discussion