ASPNETCORE_FORWARDEDHEADERS_ENABLED

Turn on forwarded-header processing with one environment variable, know exactly what it trusts, and decide when explicit UseForwardedHeaders is required.

Signature
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
  • Added in .NET ASP.NET Core 3.0

Parameters

ParameterTypeDescription
$valueoptionalstring= false"true" (case-insensitive) turns it on; anything else leaves it off. Host configuration key FORWARDEDHEADERS_ENABLED.

Return value

stringWhen true, the host adds forwarded headers middleware at the start of the pipeline with ForwardedHeaders = XForwardedFor | XForwardedProto and the KnownProxies and known networks lists cleared, so the headers are accepted from any sender.

Errors

Nothing. Headers that do not parse are ignored. Because every sender is trusted, a client that can reach the app directly can set its own IP address and scheme.

Behind a load balancer or ingress, Kestrel sees the proxy's connection: HttpContext.Connection.RemoteIpAddress is the proxy's IP and Request.Scheme is http even when the client used HTTPS. Proxies pass the original values in X-Forwarded-For and X-Forwarded-Proto, and the forwarded headers middleware copies them onto the request. ASPNETCORE_FORWARDEDHEADERS_ENABLED=true switches that middleware on without code. The environment variable must be set before the builder is created, because environment variables are loaded then. The equivalent configuration key FORWARDEDHEADERS_ENABLED is read when the app is built, so setting it through builder.Configuration before Build() also works.

What the variable configures

OptionWith the variableWith app.UseForwardedHeaders() defaults
Headers processedX-Forwarded-For, X-Forwarded-ProtoNone until you set ForwardedHeaders
Trusted sendersAnyone (lists cleared)Loopback only
ForwardLimit1: only the last (rightmost) entry is used1
X-Forwarded-HostIgnoredOnly if you add XForwardedHost
PositionFirst in the pipeline (startup filter)Where you call it; must be before HTTPS redirection, auth and rate limiting

When it is the right choice

Use it when the only way to reach the pod is through a proxy you control: a Kubernetes ingress or cloud load balancer with the pod port not exposed elsewhere, or Azure App Service on Linux. There the "trust everyone" shortcut is safe because everyone is the proxy, and it fixes the two classic bugs at once: a wrong Request.Scheme (which, once an HTTPS port is configured, for example ASPNETCORE_HTTPS_PORT=443, sends UseHttpsRedirection into a redirect loop; with no HTTPS port it only logs a warning), and logs, rate limiters and audit records that show one proxy IP for every customer.

When it is dangerous

If the app is reachable directly (a NodePort, a public VM IP, another pod in a flat network), any client can send X-Forwarded-For: 10.0.0.1 and become that address. Partition keys for rate limiting, IP allow lists and fraud signals then come from the attacker. In that case write code instead: call UseForwardedHeaders with the proxy's addresses in KnownProxies (or its subnet in the known networks list), so only those senders are believed. With two proxies in a chain (CDN then ingress), raise ForwardLimit to 2 and trust both: the middleware unwinds a hop only when the address it came from is trusted, so every hop except the client must be in KnownProxies or the known networks list, or RemoteIpAddress becomes the CDN edge rather than the client. X-Forwarded-Host needs an explicit opt-in and should be paired with AllowedHosts, because it lets the sender choose the host used in generated links.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Stop UseHttpsRedirection with ASPNETCORE_HTTPS_PORT=443 from looping behind a TLS-terminating ingress by enabling forwarded headers with one env var
  • Partition the rate limiter by real client IP instead of the ingress controller's address
  • Replace the variable with explicit KnownProxies after a security review finds the pods reachable through a NodePort

Frequently asked questions

What exactly does ASPNETCORE_FORWARDEDHEADERS_ENABLED change?

It adds UseForwardedHeaders first in the pipeline with X-Forwarded-For and X-Forwarded-Proto enabled and the trusted proxy and network lists cleared. ForwardLimit stays 1 and X-Forwarded-Host is not processed.

Why is RemoteIpAddress still my proxy's IP after calling UseForwardedHeaders()?

With code defaults no headers are processed and only loopback is trusted. Set ForwardedHeaders and add your proxy to KnownProxies or the known networks list, or use the environment variable when the proxy is the only way in.

Is it safe on Azure App Service or behind an ingress?

Yes when the app cannot be reached except through that front end. If clients can connect directly, they can forge the headers.

More examples

Explicit, safer configuration in code

With explicit options only listed networks are believed, so a direct caller cannot spoof <code>X-Forwarded-For</code>. <code>ForwardLimit = 2</code> only helps because the CDN ranges are trusted too: the ingress hop reveals the CDN edge, and the middleware unwinds the next hop only if that edge is trusted. Trust the ingress subnet rather than pod IPs, which change on every reschedule and would silently collapse all clients into one rate-limit partition. Do not also set the environment variable: it registers a second forwarded headers middleware at the start of the pipeline.

Example · csharp
using System.Net;
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    // The ingress pods' CIDR, not single pod IPs: pods get new IPs when rescheduled.
    options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.42.0.0/16"));
    // CDN in front of the ingress: two hops to unwind, and both must be trusted.
    // Use the CDN's published egress ranges (example ranges shown).
    options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("192.0.2.0/24"));
    options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("198.51.100.0/24"));
    options.ForwardLimit = 2;
});

var app = builder.Build();

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

app.MapGet("/whoami", (HttpContext context) => context.Connection.RemoteIpAddress?.ToString());
app.Run();

Enabling it on a Deployment behind an ingress

The Service is <code>ClusterIP</code>, so only in-cluster callers and the ingress reach the pods. Pair it with a NetworkPolicy that admits only the ingress namespace; without one, any pod could send forged headers. Quote <code>"true"</code> because env values must be strings.

Example · yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orders-api
  template:
    metadata:
      labels:
        app: orders-api
    spec:
      containers:
        - name: api
          image: registry.example.com/orders-api:1.8.2
          env:
            - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED
              value: "true"
---
apiVersion: v1
kind: Service
metadata:
  name: orders-api
spec:
  type: ClusterIP
  selector:
    app: orders-api
  ports:
    - port: 80
      targetPort: 8080

Testing that the redirect loop is gone

When an HTTPS port is configured (for example <code>ASPNETCORE_HTTPS_PORT=443</code>) and TLS ends at the ingress, an app without forwarded headers answers the second request with 307 to https, the ingress forwards it as HTTP again, and the browser loops. With no HTTPS port it only logs a warning and serves the request. With forwarded headers, <code>Request.IsHttps</code> is true and the endpoint answers 200 either way.

Example · http
### Through the ingress: the client used HTTPS, the pod sees HTTP plus X-Forwarded-Proto
GET https://api.shop.example.com/orders/1042
Accept: application/json

### Simulating the ingress against a port-forwarded pod
GET http://localhost:8080/orders/1042
X-Forwarded-Proto: https
X-Forwarded-For: 203.0.113.50
Accept: application/json

Discussion

  • Be the first to comment on this lesson.