ASPNETCORE_FORWARDEDHEADERS_ENABLED
Turn on forwarded-header processing with one environment variable, know exactly what it trusts, and decide when explicit UseForwardedHeaders is required.
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true- Added in .NET ASP.NET Core 3.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | string= 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
| Option | With the variable | With app.UseForwardedHeaders() defaults |
|---|---|---|
| Headers processed | X-Forwarded-For, X-Forwarded-Proto | None until you set ForwardedHeaders |
| Trusted senders | Anyone (lists cleared) | Loopback only |
ForwardLimit | 1: only the last (rightmost) entry is used | 1 |
| X-Forwarded-Host | Ignored | Only if you add XForwardedHost |
| Position | First 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
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.
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.
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.
### 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