HTTPS Redirection and HSTS in ASP.NET Core: A Simple Guide
Learn how to configure HTTPS redirection and HSTS in ASP.NET Core with simple examples, diagrams, and clear advice for development and production.
A one-way gate at the railway station
Think about a busy railway station in India. There are two entrances. One is a safe, guarded gate where a staff member checks tickets. The other is an old broken gate at the back that anyone can walk through, with no checking at all.
Now imagine the station puts up a sign at the broken gate that says: "This gate is closed. Please use the main guarded gate." Anyone who walks up is gently pointed to the safe entrance. That sign is HTTPS redirection. The broken back gate is plain HTTP, and the guarded main gate is HTTPS.
But there is one more clever trick. Imagine the station tells every regular traveller: "From now on, always come straight to the main gate. Do not even walk towards the back gate again." After hearing this once, travellers stop visiting the broken gate completely. That instruction is HSTS — HTTP Strict Transport Security. It teaches the browser to skip the unsafe gate on its own, before it even sets off.
In this guide we will learn both ideas, set them up in ASP.NET Core, and understand when to use each. Let us start with why HTTP is unsafe in the first place.
Why plain HTTP is risky
When your browser talks to a website over HTTP, the data travels in plain text. Anyone sitting between you and the server — on the same public Wi-Fi, at an internet café, or controlling a network router — can read it or even change it. This is called a man-in-the-middle attack.
HTTPS wraps that same conversation inside TLS (Transport Layer Security). TLS encrypts the data, so a person in the middle sees only scrambled bytes. They cannot read your password, your session cookie, or your bank details.
So the goal is simple: never let real traffic travel over plain HTTP. ASP.NET Core gives us two tools to push everything onto HTTPS.
The two tools are:
- HTTPS Redirection (
UseHttpsRedirection) — the server-side sign that points HTTP visitors to HTTPS. - HSTS (
UseHsts) — the browser-side rule that makes the browser choose HTTPS by itself.
They work best together. Let us look at each one closely.
Part 1: HTTPS redirection
HTTPS redirection is middleware. When a request arrives over HTTP, the middleware does not run your controller or endpoint. Instead it replies with a redirect status code and a Location header pointing to the same URL but with https://. The browser then makes a fresh request to that HTTPS address.
Here is the simplest setup in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
// Redirect HTTP requests to HTTPS.
app.UseHttpsRedirection();
app.MapControllers();
app.Run();That single line, app.UseHttpsRedirection(), is enough for most apps. By default it sends a 307 Temporary Redirect. The browser keeps the original request method (GET, POST, and so on) and tries again on HTTPS.
Choosing the redirect status code and port
You can tune the middleware by registering options with AddHttpsRedirection. In production it is common to use a 308 Permanent Redirect so browsers and search engines remember the rule. You can also set the HTTPS port explicitly.
builder.Services.AddHttpsRedirection(options =>
{
// 308 = permanent. Tells clients the move is permanent.
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
// The port your HTTPS endpoint listens on.
options.HttpsPort = 443;
});A quick comparison of the two redirect codes:
| Status code | Name | Method kept? | Best for |
|---|---|---|---|
| 307 | Temporary Redirect | Yes | Development, default |
| 308 | Permanent Redirect | Yes | Production, SEO friendly |
Both 307 and 308 keep the HTTP method, unlike the older 302 and 301 which browsers sometimes turned into a GET. That is why ASP.NET Core picks 307 by default.
How the middleware finds the HTTPS port
The redirection middleware needs to know which port to redirect to. It looks in a few places, in order:
HTTPS port discovery
Steps
Options
HttpsPort set in AddHttpsRedirection
Config
HTTPS_PORT or ASPNETCORE_HTTPS_PORTS
Server
Kestrel HTTPS endpoint port
Default
If none found, no redirect happens
If none of these are found, the middleware logs a warning and does not redirect. So if your redirects silently fail, the missing HTTPS port is the first thing to check.
Part 2: HSTS
Redirection has one small gap. The very first request still goes out over HTTP before the redirect comes back. On a hostile network, that first plain request can be hijacked. HSTS closes this gap.
HSTS is a response header named Strict-Transport-Security. When the browser receives it over HTTPS, it remembers a rule: "for this site, always use HTTPS for the next N seconds." After that, the browser upgrades any http:// link to https:// inside the browser, before sending anything. The unsafe first request disappears.
Here is the default header value ASP.NET Core sends:
Strict-Transport-Security: max-age=2592000That number, 2592000, is 30 days in seconds — the framework default.
Turning HSTS on
The standard ASP.NET Core template already includes this:
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();Notice the if (!app.Environment.IsDevelopment()) check. HSTS is intentionally off in development. Browsers cache the HSTS rule very strongly. If you turned it on for localhost while testing, your browser might refuse plain HTTP to localhost for up to a year, which can break your other local projects. So we only enable it outside Development.
Configuring HSTS options
You shape the header with AddHsts. The main options are MaxAge, IncludeSubDomains, Preload, and ExcludedHosts.
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365); // how long browsers remember
options.IncludeSubDomains = true; // apply to api.site.com, etc.
options.Preload = true; // allow inclusion in preload list
options.ExcludedHosts.Add("localhost"); // never send HSTS to these
});With those settings, the header becomes:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadHere is what each option does:
| Option | Header part | What it means |
|---|---|---|
| MaxAge | max-age=... | Seconds the browser keeps the rule. Default 30 days. |
| IncludeSubDomains | includeSubDomains | Rule also covers all subdomains. |
| Preload | preload | Marks the site for the browser preload list. |
| ExcludedHosts | (none) | Hosts that never receive the header, like localhost. |
A safe rollout plan for HSTS
Because the rule is so sticky, turn it on carefully. A mistake with a long MaxAge can lock users out of your site over HTTP for a very long time, and there is no easy way to undo it from the server.
Rolling out HSTS safely
Steps
Verify
HTTPS works on every page and subdomain
Short
Set MaxAge to 1 day first
Watch
Confirm no broken HTTP traffic
Extend
Raise MaxAge to 1 year
Preload
Submit to the preload list only when sure
The preload list is a list baked into browsers like Chrome and Firefox. Sites on it are forced to HTTPS even on the very first ever visit, with no header needed. Getting on is easy; getting off is slow. So only set Preload = true and submit your domain once you are completely sure HTTPS works everywhere, forever.
Putting it all together
Here is a more complete Program.cs that wires up both features with production-friendly settings. The order of middleware matters, so read the comments.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});
builder.Services.AddHsts(options =>
{
// Start small the first time, then increase later.
options.MaxAge = TimeSpan.FromDays(1);
options.IncludeSubDomains = true;
options.Preload = false; // flip to true only when fully confident
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
// HSTS first, so the header is added before redirection logic.
app.UseHsts();
}
// Redirect any HTTP that still arrives.
app.UseHttpsRedirection();
app.MapControllers();
app.Run();A note on reverse proxies and load balancers
Many real apps sit behind a reverse proxy like Nginx, or a cloud load balancer, or a service like Cloudflare. In that setup, TLS often ends at the proxy, and the proxy talks to your app over plain HTTP inside the private network. Your app may then think every request is HTTP and try to redirect in a loop.
The fix is the Forwarded Headers middleware. The proxy adds an X-Forwarded-Proto: https header, and this middleware reads it so your app knows the original request was secure.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
var app = builder.Build();
// Must run BEFORE UseHttpsRedirection and UseHsts.
app.UseForwardedHeaders();Place UseForwardedHeaders early, before the HTTPS middleware, so the rest of the pipeline sees the correct scheme.
Common mistakes and how to avoid them
A few problems come up again and again. Here is a short table to keep nearby.
| Symptom | Likely cause | Fix |
|---|---|---|
| Redirect does nothing | HTTPS port not found | Set HttpsPort or configure Kestrel HTTPS endpoint |
| Redirect loop | Behind a proxy doing TLS | Add UseForwardedHeaders before redirection |
| localhost stuck on HTTPS | HSTS enabled in dev | Keep UseHsts inside the non-Development check |
| HSTS header missing | Request came over HTTP | HSTS is only sent on HTTPS responses |
| Subdomain not protected | IncludeSubDomains off | Set IncludeSubDomains = true |
One detail worth repeating: the HSTS header is only sent on HTTPS responses. The browser ignores an HSTS header that arrives over plain HTTP, by design. So if you do not see the header, first confirm the response itself came over HTTPS.
Testing your setup
You can check both features quickly from the command line. Use curl with the -I flag to see only the response headers.
// Not C#, but here is the idea as commands you can run:
//
// curl -I http://localhost:5000/
// -> expect a 307 or 308 with a Location: https://... header
//
// curl -I https://localhost:5001/
// -> expect Strict-Transport-Security in the headersIn a browser, open the developer tools, go to the Network tab, and click any request. Look at the Response Headers section. You should see the redirect on the HTTP request, and the Strict-Transport-Security header on the HTTPS responses once you are running outside Development.
If you ever need to clear a stuck HSTS rule during testing, Chrome has a page at chrome://net-internals/#hsts where you can delete the rule for a single domain.
Quick recap
- HTTP is unsafe because data travels in plain text. HTTPS encrypts it with TLS.
UseHttpsRedirectionis server-side. It catches HTTP requests and redirects them to HTTPS, using a 307 by default or 308 for production.UseHstsis browser-side. It sends theStrict-Transport-Securityheader so the browser upgrades to HTTPS by itself, closing the gap of the first plain request.- Configure redirection with
AddHttpsRedirection(status code, port) and HSTS withAddHsts(MaxAge,IncludeSubDomains,Preload,ExcludedHosts). - HSTS is off in development on purpose, because browsers cache the rule strongly and could break your localhost work.
- Roll out HSTS slowly: small
MaxAgefirst, then a year, then the preload list only when fully confident. - Behind a reverse proxy, add
UseForwardedHeadersfirst so your app knows the real scheme and avoids redirect loops.
References and further reading
- Enforce HTTPS in ASP.NET Core — Microsoft Learn
- HstsMiddleware source — dotnet/aspnetcore on GitHub
- HstsOptions source — dotnet/aspnetcore on GitHub
- What is HSTS and why is it in my ASP.NET Core app? — Khalid Abuhakmeh
- Configuring HTTPS Redirection and HSTS in ASP.NET Core — Anton Martyniuk
Related Posts
Authentication and Authorization Best Practices in ASP.NET Core
A friendly guide to authentication and authorization in ASP.NET Core for .NET 10 — JWT, cookies, claims, roles, policies, and security best practices with diagrams.
API Key Authentication in ASP.NET Core: The Secure Way
Learn how to add API key authentication to your ASP.NET Core API the right way. Use an AuthenticationHandler, hash keys, compare safely, and follow 2026 security best practices, with diagrams and code.
3 Ways To Create Middleware In ASP.NET Core (Beginner Guide)
Learn the 3 ways to create middleware in ASP.NET Core: inline request delegates, convention-based classes, and factory-based IMiddleware, with simple diagrams and code.
Rate Limiting in ASP.NET Core: A Simple, Complete Guide
Learn rate limiting in ASP.NET Core with simple examples. Understand fixed window, sliding window, token bucket, and concurrency limiters, with diagrams, code, and real-world advice on which to pick.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
Refresh Tokens and Token Revocation in ASP.NET Core: A Beginner Guide
Learn refresh tokens and token revocation in ASP.NET Core with simple words, diagrams, and code. Short access tokens, rotation, reuse detection, and safe logout.