Skip to main content
SEMastery
ASP.NETbeginner

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.

11 min readUpdated April 20, 2026

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.

Plain HTTP can be read by an attacker in the middle, while HTTPS hides the data.

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 codeNameMethod kept?Best for
307Temporary RedirectYesDevelopment, default
308Permanent RedirectYesProduction, 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 HTTPS redirection middleware turns an HTTP request into an HTTPS one.

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

Options
Config
Server
Default

Steps

1

Options

HttpsPort set in AddHttpsRedirection

2

Config

HTTPS_PORT or ASPNETCORE_HTTPS_PORTS

3

Server

Kestrel HTTPS endpoint port

4

Default

If none found, no redirect happens

Where UseHttpsRedirection looks to find the HTTPS port to redirect to.

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=2592000

That 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.

With HSTS, the browser upgrades HTTP to HTTPS by itself after the first visit.

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; preload

Here is what each option does:

OptionHeader partWhat it means
MaxAgemax-age=...Seconds the browser keeps the rule. Default 30 days.
IncludeSubDomainsincludeSubDomainsRule also covers all subdomains.
PreloadpreloadMarks 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

Verify
Short
Watch
Extend
Preload

Steps

1

Verify

HTTPS works on every page and subdomain

2

Short

Set MaxAge to 1 day first

3

Watch

Confirm no broken HTTP traffic

4

Extend

Raise MaxAge to 1 year

5

Preload

Submit to the preload list only when sure

A gentle order for enabling HSTS in production without locking yourself out.

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();
Both layers working together: the server redirects, and the browser remembers.

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.

SymptomLikely causeFix
Redirect does nothingHTTPS port not foundSet HttpsPort or configure Kestrel HTTPS endpoint
Redirect loopBehind a proxy doing TLSAdd UseForwardedHeaders before redirection
localhost stuck on HTTPSHSTS enabled in devKeep UseHsts inside the non-Development check
HSTS header missingRequest came over HTTPHSTS is only sent on HTTPS responses
Subdomain not protectedIncludeSubDomains offSet 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 headers

In 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.
  • UseHttpsRedirection is server-side. It catches HTTP requests and redirects them to HTTPS, using a 307 by default or 308 for production.
  • UseHsts is browser-side. It sends the Strict-Transport-Security header 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 with AddHsts (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 MaxAge first, then a year, then the preload list only when fully confident.
  • Behind a reverse proxy, add UseForwardedHeaders first so your app knows the real scheme and avoids redirect loops.

References and further reading

Related Posts