Skip to main content

Command Palette

Search for a command to run...

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

Updated
β€’12 min read

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, 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 covers β€” including the Docker and nginx configuration that makes it all click.

ASP.NET Core Web API: Zero to Production

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:

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

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

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:

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:

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.

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.

More from this blog

C

Coding Droplets

244 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.