# ASP.NET Core HTTPS Redirect Loop Behind Reverse Proxy: Root Cause and Fix

Infinite HTTPS redirect loops are one of the most disorienting production failures an ASP.NET Core team can hit. The app worked perfectly in development, passed staging smoke tests, and then went live behind nginx, an AWS Application Load Balancer, or an Azure Application Gateway — and suddenly every browser request triggers a chain of 301 or 307 redirects until the browser gives up with an `ERR_TOO_MANY_REDIRECTS` error. The root cause is almost always the same: `UseHttpsRedirection` is issuing a redirect because the request *looks* like HTTP, but the reverse proxy has already terminated the TLS session and is forwarding the request over plain HTTP internally. The app never sees the original HTTPS scheme, so it keeps redirecting — forever.

The full implementation — including properly configured `ForwardedHeadersOptions`, `KnownProxies` setup, integration tests that smoke-test the proxy handshake, and environment-specific pipeline configurations — is available on [Patreon](https://www.patreon.com/CodingDroplets), with the complete production-ready codebase that enterprise teams can adapt directly. Understanding the concepts here is the first half; seeing it wired together correctly in a real deployment is what Chapter 15 of the [ASP.NET Core Web API: Zero to Production course](https://aspnetcoreapi.codingdroplets.com/) covers — including the Docker and nginx configuration that makes it all click.

[![ASP.NET Core Web API: Zero to Production](https://newsletter.codingdroplets.com/images/aspnet-core-api-course-banner-1.jpg align="center")](https://aspnetcoreapi.codingdroplets.com/)

## Why the Redirect Loop Happens

To understand the fix, you need to understand what happens at the network boundary.

When a client makes an HTTPS request to your production domain, the reverse proxy (nginx, YARP, ALB, Traefik) handles the TLS handshake. It decrypts the request and forwards it to your Kestrel instance over plain HTTP — typically on port 80 or 5000. From Kestrel's perspective, the incoming request scheme is `http`, not `https`.

`UseHttpsRedirection` looks at `context.Request.Scheme`. When it sees `http`, it issues a redirect to the HTTPS equivalent. The browser follows the redirect. The proxy terminates TLS again, passes the request as `http` again. ASP.NET Core redirects again. Loop.

The fix is not to disable HTTPS redirection. The fix is to tell your ASP.NET Core application what the *original* scheme was, before the proxy terminated TLS. That information is carried in the `X-Forwarded-Proto` header, which reverse proxies set when forwarding requests. ASP.NET Core exposes `ForwardedHeadersMiddleware` specifically to read this header and rewrite `Request.Scheme` before any other middleware executes.

## The Five Root Causes Teams Encounter

**1\.** `UseForwardedHeaders` **is not called at all**

The most common cause. The app uses `UseHttpsRedirection` but never registers the middleware that reads `X-Forwarded-Proto`. Every request from behind the proxy arrives with `Scheme = http` regardless of what the client used.

**2\.** `UseForwardedHeaders` **is registered after** `UseHttpsRedirection`

Middleware order in ASP.NET Core's pipeline matters absolutely. If `UseForwardedHeaders` runs *after* `UseHttpsRedirection`, the scheme is already evaluated and acted upon before the forwarded header is applied. The result is identical to not calling it at all.

**3\. The known proxy list is not configured**

`ForwardedHeadersMiddleware` applies a security constraint by default: it only trusts forwarded headers from addresses in `KnownProxies` or `KnownNetworks`. If the proxy's internal IP is not listed, the middleware silently ignores the `X-Forwarded-Proto` header. The scheme stays `http`. The redirect loop continues.

In development, this often passes because the loopback address (`127.0.0.1`) is trusted by default. In production, the proxy has a different internal IP, and the check fails silently.

**4\. The proxy is not sending** `X-Forwarded-Proto`

Some proxies need explicit configuration to set forwarding headers. nginx does not send them by default — you must add `proxy_set_header X-Forwarded-Proto $scheme;` to your nginx location block. Without it, the header is never received by the ASP.NET Core app, and the middleware has nothing to process.

**5\.** `ASPNETCORE_FORWARDEDHEADERS_ENABLED` **is not set in containerised deployments**

When running in containers (Docker Compose, Kubernetes, Azure Container Apps), ASP.NET Core has an environment variable shortcut: setting `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` enables `UseForwardedHeaders` automatically with basic defaults including `X-Forwarded-For` and `X-Forwarded-Proto`. Teams routinely miss this in Kubernetes deployments, fall back to relying on the variable being absent, and wonder why the loop persists.

## How to Diagnose the Issue

Before applying a fix, confirm you're dealing with a forwarded headers problem and not something else.

**Check 1: Inspect response headers for a redirect chain.** Using `curl -v` or browser DevTools, confirm you are getting `301` or `307` responses in a loop. The `Location` header should show the same URL being redirected to itself, switching between `http://` and `https://`.

**Check 2: Check your application logs.** Set the minimum log level to `Debug` for `Microsoft.AspNetCore.HttpOverrides`. If the forwarded headers middleware is processing or rejecting headers, it logs the outcome. A log line containing "Skipping unknown proxy" or "forwarded header not applied" confirms cause 3 or 4.

**Check 3: Add a diagnostic middleware temporarily.** Drop a short middleware before `UseForwardedHeaders` that logs `context.Request.Scheme`, `context.Request.Headers["X-Forwarded-Proto"]`, and `context.Connection.RemoteIpAddress`. This gives you the exact values ASP.NET Core is receiving, before any rewriting occurs. Remove it before next deployment.

**Check 4: Confirm the proxy is sending headers.** On your proxy machine, or via a temporary diagnostic endpoint, verify that `X-Forwarded-Proto: https` is present on requests arriving at the Kestrel port. If the header is absent, the fix is in your proxy configuration, not your application code.

## The Fix: Correct Middleware Order and Configuration

The fix involves three parts: configure `ForwardedHeadersOptions` with the correct trusted proxies, register `UseForwardedHeaders` as the first substantive middleware in the pipeline, and verify your proxy configuration.

### ASP.NET Core Pipeline Configuration

In `Program.cs`, the middleware registration order must be:

```csharp
app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseHsts();
// ... rest of pipeline
```

The `ForwardedHeadersOptions` must be configured in the service registration, not inline at middleware call time:

```csharp
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
    // Trust all proxies (only when running behind a trusted perimeter)
    // For stricter environments: add specific IPs or CIDR ranges
});
```

Clearing `KnownNetworks` and `KnownProxies` and not adding specific values means the middleware will trust forwarded headers from any source. This is acceptable only when your Kestrel port is not publicly accessible — only the proxy can reach it. If Kestrel is exposed directly alongside the proxy, add specific trusted proxy IPs using `options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"))` or add CIDR ranges via `options.KnownNetworks`.

For **containerised deployments with Kubernetes or Docker Compose**, the environment variable approach is often cleaner than code:

```plaintext
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
```

Setting this in your `deployment.yaml` or `docker-compose.yml` enables forwarded headers processing with safe defaults applicable to most containerised setups. You still need to ensure your proxy pod's IP is accessible or use the clear-networks approach above.

### nginx Configuration

If nginx is your reverse proxy, your location block needs these headers:

```nginx
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host              $host;
```

Without `proxy_set_header X-Forwarded-Proto $scheme;`, nginx does not forward the original scheme, and the ASP.NET Core middleware has nothing to rewrite.

### AWS Application Load Balancer

ALB sets `X-Forwarded-Proto` automatically. The fix for ALB users is almost always in the ASP.NET Core configuration — specifically `KnownNetworks` and `KnownProxies` not including the ALB's internal IP. Using the clear-networks approach resolves this, provided the Kestrel port is security-group–restricted to ALB only.

## What About Disabling `UseHttpsRedirection` in Production?

Some Stack Overflow answers suggest removing `UseHttpsRedirection` entirely and letting the reverse proxy handle HTTPS enforcement at its level. This is a valid pattern — and a reasonable default when the proxy is configured to enforce HTTPS for all external traffic.

If you go this route, you still want `UseHsts` in your pipeline, because HSTS headers need to be sent on HTTPS responses and are managed at the application level, not the proxy level in most configurations.

However, the forwarded headers approach is preferred for two reasons. First, it preserves `Request.Scheme` correctness throughout the application, which matters for URL generation, redirect URIs in OAuth flows, and content links. Second, it means your application behaves consistently regardless of whether it is accessed directly or via a proxy — useful during staged rollouts or when developers need to test against production-like environments locally.

## Is It Safe to Trust All Forwarded Headers?

Blindly trusting all `X-Forwarded-*` headers is a security risk if Kestrel can be reached by sources other than your trusted proxy. An attacker who can send requests directly to Kestrel can set `X-Forwarded-Proto: https` and `X-Forwarded-For: <any IP>`, bypassing any IP-based access control or rate-limiting logic that reads `HttpContext.Connection.RemoteIpAddress`.

The safe configuration is:

*   Restrict Kestrel's listening port to localhost or an internal network via firewall or security group rules — the proxy should be the only way traffic reaches Kestrel
    
*   Add known proxy IPs or CIDR ranges to `KnownProxies`/`KnownNetworks` instead of clearing all restrictions
    
*   For multi-hop proxy chains (e.g., CDN → ALB → nginx → Kestrel), configure `ForwardLimit` to the number of trusted hops
    

## Multi-Hop Proxy Chains

In enterprise deployments, requests often pass through more than one proxy: a CDN strips and rewrites headers, an API gateway adds more, nginx terminates TLS, and finally Kestrel receives the request. The `X-Forwarded-For` header at that point may contain a comma-separated list of IPs added at each hop.

`ForwardedHeadersMiddleware` processes the right-most IP in the list by default (the most recent hop). You can increase `ForwardLimit` to process multiple hops, but each proxy in the chain must be added to `KnownProxies` or `KnownNetworks`. If any hop in the chain is not trusted, the middleware stops processing at that point.

For CDN scenarios where the originating IP is important (rate limiting, geo-restriction), check whether the CDN sets a separate header like `CF-Connecting-IP` (Cloudflare) or `True-Client-IP` (Akamai/Cloudflare Enterprise). These are more reliable than `X-Forwarded-For` in CDN-fronted deployments.

## Does the Same Problem Affect Request.Host and Generated URLs?

Yes. The forwarded headers problem affects not just `Request.Scheme` but also `Request.Host` and link generation. If your application generates absolute URLs — in OAuth redirect URIs, in `Location` headers from `CreatedAtRoute`, or in email links — and the host is not correctly rewritten from `X-Forwarded-Host`, those URLs will point to the internal address of the container rather than the public domain.

`ForwardedHeadersOptions.ForwardedHeaders` has a `ForwardedHeaders.XForwardedHost` flag as well. Add it when your application generates absolute URLs and you need the host to reflect the original public-facing domain.

```csharp
options.ForwardedHeaders =
    ForwardedHeaders.XForwardedFor |
    ForwardedHeaders.XForwardedProto |
    ForwardedHeaders.XForwardedHost;
```

Only add `XForwardedHost` if your proxy explicitly sets `X-Forwarded-Host` — and verify that the proxy is configured to set it correctly before enabling it in production.

## How to Prevent This from Recurring

The forwarded headers configuration is easy to get right once and then never think about again — but it is also easy to regress. Two practices help:

**Write an integration test that validates the scheme.** Using `WebApplicationFactory`, configure a test middleware that asserts `context.Request.Scheme == "https"` when `X-Forwarded-Proto: https` is sent as a request header. This test will catch any future regression where `UseForwardedHeaders` is removed, reordered, or misconfigured.

**Make forwarded headers configuration environment-aware.** In local development, `X-Forwarded-Proto` is not present and you typically do not need forwarded headers processing. Gating the configuration behind `if (!app.Environment.IsDevelopment())` ensures the middleware only activates in staging and production, reducing noise in dev logs.

* * *

## FAQ

### Why does UseForwardedHeaders work in development but not production?

In development, ASP.NET Core accesses Kestrel directly without a proxy, so `Request.Scheme` is set correctly by the browser connection itself. No proxy header rewriting is needed. In production behind a reverse proxy, TLS is terminated at the proxy layer: Kestrel receives requests over plain HTTP. Without `UseForwardedHeaders` processing `X-Forwarded-Proto: https`, the app never knows the original scheme was HTTPS, and `UseHttpsRedirection` keeps redirecting.

### Is it safe to call `options.KnownNetworks.Clear()` in production?

Clearing `KnownNetworks` and `KnownProxies` removes the IP-based trust guard on forwarded headers. This is safe *only if* your Kestrel port is not reachable from the public internet — for example, when it is bound to a private network interface and protected by a security group or firewall that only allows inbound connections from the proxy. If Kestrel is publicly accessible, always add specific trusted proxy IPs rather than clearing all restrictions.

### Does this apply to Azure App Service and Azure Container Apps?

Yes. Azure App Service and Azure Container Apps both use internal reverse proxies. Both platforms add `X-Forwarded-Proto` headers. In Azure App Service, `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` is often applied via App Settings. In Azure Container Apps, you configure the same environment variable in your container definition. Always verify using the debug middleware approach described above when debugging HTTPS issues on Azure-hosted apps.

### Does disabling `UseHttpsRedirection` solve the problem?

It stops the redirect loop, but it trades one problem for another. Without `UseHttpsRedirection`, your app no longer enforces HTTPS at the application level. If your proxy is ever misconfigured to allow plain HTTP through, requests will be served unencrypted. The forwarded headers approach is more robust because it keeps HTTPS enforcement active while correctly reading the transport-level scheme.

### Why do generated URLs still show `http://` even after fixing the redirect loop?

`UseForwardedHeaders` rewrites `Request.Scheme` — but only for the current request's context. If your application generates absolute URLs before the middleware runs, or if it reads `Request.Scheme` through a cached context, the old scheme might persist. Ensure `UseForwardedHeaders` is called before any middleware that reads or generates URLs, including authentication redirects and `IUrlHelper` calls. Also check whether your proxy is setting `X-Forwarded-Host` and whether you need to include `ForwardedHeaders.XForwardedHost` in your options.

### How do I debug forwarded headers issues in Kubernetes?

In Kubernetes, your proxy (usually nginx-ingress or an ingress controller) sets forwarded headers, but the pod's internal IP is not in `KnownNetworks` by default. Add the pod CIDR range to `KnownNetworks` in your `ForwardedHeadersOptions`, or use the `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` environment variable and ensure the middleware clears default network restrictions as shown above. The most effective diagnostic tool is a temporary endpoint that returns `Request.Scheme`, `Request.Host`, `Request.Headers["X-Forwarded-Proto"]`, and `Connection.RemoteIpAddress` as JSON — deploy it temporarily, hit it from within the cluster, and compare actual values against expected.

### What is the difference between `ForwardedHeaders.All` and specifying each header individually?

`ForwardedHeaders.All` is a convenience flag that includes `XForwardedFor`, `XForwardedHost`, `XForwardedProto`, and also `XForwardedPathBase`. While convenient, enabling `XForwardedHost` without verifying your proxy sets it correctly can lead to unexpected host header values in URL generation. The recommendation is to explicitly specify only the headers your proxy actually sets, rather than enabling all flags and hoping for correct behaviour.
