Skip to content

Forwarded Headers & Reverse-Proxy Hosting

Key Points

  • When ASP.NET Core runs behind a reverse proxy (nginx, IIS, Azure Front Door, AGW, ALB, Traefik), HttpContext.Connection.RemoteIpAddress shows the proxy, not the client. Request.Scheme is http, not https.
  • UseForwardedHeaders restores client info from X-Forwarded-For / -Proto / -Host headers — must run before authentication, redirection, and IP-based logging.
  • Trust boundary matters: don't blindly trust forwarded headers from the internet. Configure KnownProxies / KnownNetworks to allow only proxies you control.
  • App Service / Functions / Container Apps — Microsoft's reverse-proxy environments emit forwarded headers and ASPNETCORE_FORWARDEDHEADERS_ENABLED=true does the right thing automatically.
  • Without this: HTTPS redirect loops, broken auth (cookie marked secure but request looks insecure), wrong client IPs in audit logs and rate-limit policies.

Concepts (deep dive)

What forwarded headers carry

X-Forwarded-For:    1.2.3.4, 10.0.0.1                — original client + intermediate proxies
X-Forwarded-Proto:  https                              — original scheme
X-Forwarded-Host:   api.acme.com                       — original Host header
X-Forwarded-Port:   443                                — original port (rare)
Forwarded:          for=1.2.3.4;proto=https;host=...   — RFC 7239 (less common)

The proxy adds these so the upstream app can act as if the client connected directly.

Why it matters

// Without UseForwardedHeaders:
ctx.Connection.RemoteIpAddress  // 10.0.0.1 (proxy)
ctx.Request.Scheme              // "http" (proxy → app over HTTP)
ctx.Request.IsHttps             // false → HTTPS redirect loops
ctx.Request.Host                // app's internal hostname

// With UseForwardedHeaders:
ctx.Connection.RemoteIpAddress  // 1.2.3.4 (real client)
ctx.Request.Scheme              // "https"
ctx.Request.IsHttps             // true
ctx.Request.Host                // "api.acme.com"

Configuration

builder.Services.Configure<ForwardedHeadersOptions>(opts =>
{
    opts.ForwardedHeaders = ForwardedHeaders.XForwardedFor
                          | ForwardedHeaders.XForwardedProto
                          | ForwardedHeaders.XForwardedHost;

    // Trust only your proxies
    opts.KnownNetworks.Clear();
    opts.KnownProxies.Clear();
    opts.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
    opts.KnownProxies.Add(IPAddress.Parse("172.16.0.1"));

    // Optional: how many forwards deep to honor
    opts.ForwardLimit = 2;
});

var app = builder.Build();
app.UseForwardedHeaders();   // FIRST — before auth, redirect, logging

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

Trust boundary

Without KnownProxies / KnownNetworks, anyone can spoof X-Forwarded-For and bypass IP-based rules. The defaults trust loopback only.

Public client ─► [Public LB] ─► [Internal proxy] ─► App
                               trust this hop;
                               clear forwarded headers
                               that came from clients

Configure your proxy to strip any inbound X-Forwarded-* and write fresh values from the actual connection. Then ASP.NET Core trusts only what comes from the proxy IP range.

ForwardedHeaders.All is dangerous

opts.ForwardedHeaders = ForwardedHeaders.All;  // includes XForwardedHost — host header injection risk

Set headers explicitly. Allow XForwardedHost only if your proxy enforces it (otherwise an attacker rewrites Request.Host and breaks Host-based routing or SSRF protections).

Order matters

✅ Correct middleware order:

UseForwardedHeaders          ← first; rewrites Connection/Scheme/Host
UseHsts
UseHttpsRedirection           ← now sees real Scheme
UseSerilogRequestLogging      ← logs real client IP
UseRouting
UseAuthentication             ← cookies marked Secure work over real HTTPS
UseAuthorization
UseEndpoints

If UseForwardedHeaders runs after UseAuthentication, login cookies are computed from the wrong scheme/host and may not be sent on subsequent requests.

App Service / Container Apps shortcut

For Microsoft-managed reverse proxies, set:

ASPNETCORE_FORWARDEDHEADERS_ENABLED=true

The runtime applies safe defaults (trusts the platform's proxy network). Manual UseForwardedHeaders not required.

IIS in-process vs out-of-process

  • In-process (default): IIS hosts CLR; no proxy hop in your sense, but HttpRequest.Scheme reflects the W3SVC binding which may need patching.
  • Out-of-process: IIS reverse-proxies to Kestrel; standard forwarded-headers story applies.

UseIISIntegration() (called by WebApplication.CreateBuilder) wires this automatically.

Behind multiple proxies

Client ──► CDN ──► API Gateway ──► App

X-Forwarded-For becomes a comma-list: 1.2.3.4, 10.0.0.5, 172.16.0.3. ASP.NET Core walks from rightmost back, accepting hops in KnownProxies until it hits the first untrusted address — that's the "real" client. Configure ForwardLimit to bound the walk.

YARP scenario

YARP (Microsoft.ReverseProxy) is itself ASP.NET Core. It writes X-Forwarded-For etc. when routing upstream. The upstream app uses the same UseForwardedHeaders configuration.

// In YARP cluster config:
{
  "ClusterId": "backend",
  "Destinations": { "d1": { "Address": "http://backend1:5000/" } },
  "HttpRequest": { "ActivityTimeout": "00:01:00" },
  "Metadata": { "ForwardedHeaders": "Append" }
}

Code: correct vs wrong

❌ Wrong: middleware order

app.UseHttpsRedirection();         // sees scheme=http; redirects to https; loop
app.UseForwardedHeaders();

✅ Correct

app.UseForwardedHeaders();
app.UseHttpsRedirection();

❌ Wrong: trusting all proxies

opts.ForwardedHeaders = ForwardedHeaders.All;
opts.KnownProxies.Add(IPAddress.Any);   // anybody on the internet can spoof

✅ Correct: explicit trusted ranges

opts.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8));

❌ Wrong: writing header logic by hand

var ip = ctx.Request.Headers["X-Forwarded-For"].ToString().Split(',').First();

Skips trust validation, ignores spoofing.

✅ Correct: rely on the middleware

var ip = ctx.Connection.RemoteIpAddress;   // already substituted

Design patterns for this topic

Pattern 1 — Production-grade forwarded-headers config

builder.Services.Configure<ForwardedHeadersOptions>(opts =>
{
    opts.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    opts.KnownNetworks.Clear();
    opts.KnownProxies.Clear();
    foreach (var range in builder.Configuration.GetSection("ProxyTrust:Networks").Get<string[]>() ?? [])
    {
        var parts = range.Split('/');
        opts.KnownNetworks.Add(new IPNetwork(IPAddress.Parse(parts[0]), int.Parse(parts[1])));
    }
    opts.ForwardLimit = 2;
});

Pull trust boundaries from configuration; deploy-time pin.

Pattern 2 — Real-IP-aware rate limiter

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("per-ip", ctx =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            factory: _ => new FixedWindowRateLimiterOptions { Window = TimeSpan.FromMinutes(1), PermitLimit = 100 }));
});

After UseForwardedHeaders, RemoteIpAddress is the real client.

Pattern 3 — Defensive base middleware

app.Use(async (ctx, next) =>
{
    if (ctx.Request.Scheme != "https" && !env.IsDevelopment())
        ctx.Response.Headers["Strict-Transport-Security"] = "max-age=31536000";
    await next();
});

Once Scheme is correct, enforce HSTS without redirect surprises.

Pattern 4 — Audit logger using real client IP

_logger.LogInformation("Request from {ClientIp} {Method} {Path}",
    ctx.Connection.RemoteIpAddress, ctx.Request.Method, ctx.Request.Path);

Pros & cons / trade-offs

Setup Pros Cons
Default (no proxy) Simple Wrong info behind proxy
UseForwardedHeaders + trust list Correct, secure Must keep trust list current
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true One-liner for App Service Less control
Hand-parsing X-Forwarded-For Maximum control Easy to misuse, security risk

When to use / when to avoid

  • Always when running behind nginx, IIS out-of-process, AGW, ALB, AFD, CloudFront, Container Apps, App Service.
  • Skip when running plain Kestrel publicly (rare, single-instance dev).
  • Avoid trusting X-Forwarded-Host unless your proxy enforces it.

Interview Q&A

Q1. Why does Request.IsHttps return false behind nginx? A. nginx terminates TLS and proxies to Kestrel over plaintext. Kestrel sees an HTTP connection. UseForwardedHeaders reads X-Forwarded-Proto: https and rewrites Request.Scheme to "https".

Q2. Why must UseForwardedHeaders run before UseAuthentication? A. Authentication issues cookies marked Secure and validates them based on Request.Scheme. If the scheme is wrong (still http), cookies aren't sent on subsequent HTTPS requests, breaking sign-in.

Q3. What's the security risk of ForwardedHeaders.All + IPAddress.Any trust? A. Any external attacker can spoof X-Forwarded-For to bypass IP-based rules, or X-Forwarded-Host to impersonate any host. Always explicitly list trusted networks.

Q4. How do you handle multiple proxy hops? A. Configure ForwardLimit (default 1). The middleware walks X-Forwarded-For from rightmost backward, accepting addresses in KnownProxies/KnownNetworks until it hits the first untrusted — that's the client.

Q5. What's ASPNETCORE_FORWARDEDHEADERS_ENABLED? A. Environment variable Microsoft platforms set so the runtime auto-applies safe forwarded-headers config without your code calling UseForwardedHeaders. Useful for App Service / Container Apps.

Q6. Difference between KnownProxies and KnownNetworks? A. KnownProxies lists single IPs; KnownNetworks lists CIDR ranges. Functionally equivalent; use whichever matches your trust definition.

Q7. What does YARP write? A. X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Prefix — controllable per-cluster. Upstream app applies UseForwardedHeaders to consume them.

Q8. Why might HTTPS redirect loop? A. App receives http (proxy already terminated TLS), redirects to https, browser hits proxy with https, proxy forwards as http to app, app redirects again. Fix: UseForwardedHeaders so app sees https.


Gotchas / common mistakes

  • UseForwardedHeaders after UseHttpsRedirection — redirect loops.
  • UseForwardedHeaders after UseAuthentication — cookies broken.
  • KnownProxies.Clear() not called — defaults trust loopback only, your real proxy IP is rejected.
  • Trusting X-Forwarded-Host without proxy enforcement — host injection.
  • Reading X-Forwarded-For directly in user code instead of using RemoteIpAddress.
  • Forgetting App Service env var — manual middleware works fine but ASPNETCORE_FORWARDEDHEADERS_ENABLED=true is simpler.
  • ALB / NLB writing different forwarded styles — verify which header your proxy emits.
  • IIS in-process and adding UseIISIntegration manually — already wired by CreateBuilder.

Further reading