ASP.NET Core UseForwardedHeaders behind a proxy

ASP.NET Core ships its own answer to the client-IP problem: the forwarded-headers middleware. It works well, it's free, and it fails silently in production for one specific reason that catches nearly everyone. Here's how to configure it properly, and where it stops helping.

Short answer. KnownProxies is the list of proxy IP addresses ASP.NET Core will accept X-Forwarded-For from. It defaults to loopback only (::1), so the middleware silently does nothing in production until you add your real proxy. Call UseForwardedHeaders first in the pipeline.

It rewrites HttpContext.Connection.RemoteIpAddress so your application sees the real client. It does not change the c-ip field in the IIS W3C logs, which IIS writes independently of your app.

What does UseForwardedHeaders actually do?

It reads the X-Forwarded-For and X-Forwarded-Proto headers sent by a trusted proxy and overwrites HttpContext.Connection.RemoteIpAddress and Request.Scheme with the original client's values, before the rest of the pipeline runs.

When your app sits behind a reverse proxy, Kestrel sees the connection coming from that proxy, so HttpContext.Connection.RemoteIpAddress is the proxy's address and Request.Scheme is often http even though the client used HTTPS. The forwarded-headers middleware reads X-Forwarded-For and X-Forwarded-Proto and rewrites those properties before the rest of your pipeline runs.

Everything downstream (your logging, rate limiting, authorisation policies, audit records, Request.IsHttps) then sees the real client rather than the proxy.

The configuration

var builder = WebApplication.CreateBuilder(args);

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

    // The defaults trust loopback only. Add your actual proxy.
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.5"));

    // Or, for a range of proxies:
    // options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 24));
});

var app = builder.Build();

// Must run before anything that reads the client IP or scheme.
app.UseForwardedHeaders();

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

On the older Startup.cs layout the same two halves live in ConfigureServices (the Configure<ForwardedHeadersOptions> call) and Configure (the app.UseForwardedHeaders() call). Nothing else changes.

What is the difference between KnownProxies and KnownNetworks?

KnownProxies holds individual proxy addresses and matches them exactly. KnownNetworks holds CIDR ranges and matches any address inside them. The middleware compares the address that opened the TCP connection against both lists, and processes the forwarded headers only if one of them matches.

Option Type Default Use it when
KnownProxies IList<IPAddress> One entry: IPAddress.IPv6Loopback (::1) You have a handful of proxies at fixed addresses.
KnownNetworks IList<IPNetwork> One entry: 127.0.0.0/8 Your proxies sit in a subnet, or their addresses change.
ForwardLimit int? 1 More than one trusted hop sits in front of the app.
ForwardedForHeaderName string X-Forwarded-For Your proxy sends a different header, such as CF-Connecting-IP.

Both defaults exist so that a developer machine works out of the box. Neither is any use in production, where the proxy is a different host. Note that the two defaults are not symmetric: KnownProxies ships the IPv6 loopback and KnownNetworks ships the IPv4 loopback range.

Naming individual proxies

The most common case. Add each proxy address; adding one does not remove the loopback defaults, so local development keeps working.

options.KnownProxies.Add(IPAddress.Parse("10.0.0.5"));
options.KnownProxies.Add(IPAddress.Parse("10.0.0.6"));

Trusting a whole subnet

When the load balancer pool can grow or its addresses are assigned dynamically, name the network instead of chasing individual addresses. The second argument is the CIDR prefix length, so this trusts 10.0.0.0/24:

options.KnownNetworks.Add(
    new IPNetwork(IPAddress.Parse("10.0.0.0"), 24));

Loading the list from configuration

Hard-coding addresses means a rebuild every time the network changes. Reading them from appsettings.json keeps the trust list an operational setting rather than a code change, which matters when the same build is deployed to several environments:

// appsettings.json
// "TrustedProxies": [ "10.0.0.5", "10.0.0.6" ]

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

    foreach (var ip in builder.Configuration
                 .GetSection("TrustedProxies").Get<string[]>() ?? [])
    {
        options.KnownProxies.Add(IPAddress.Parse(ip));
    }
});

Why does KnownProxies still not match after adding the right address?

Because on a dual-stack socket the connection arrives as an IPv4-mapped IPv6 address. Your proxy at 10.0.0.5 shows up as ::ffff:10.0.0.5, and KnownProxies matching is exact. This is the second trap, and it looks identical to the first: no error, no rewrite, address still wrong.

Microsoft's guidance is to check what the server is actually seeing rather than guess. Log HttpContext.Connection.RemoteIpAddress on a real request through the proxy and add the address in whatever form it appears:

// If RemoteIpAddress logs as ::ffff:10.0.0.5, add that form:
options.KnownProxies.Add(
    IPAddress.Parse("10.0.0.5").MapToIPv6());

Adding both forms is harmless and saves a redeploy if the socket configuration changes later. The same applies to KnownNetworks: an IPv4 CIDR range will not match an IPv4-mapped IPv6 connection address.

Has KnownNetworks changed in .NET 10?

Yes. In .NET 10, ForwardedHeadersOptions.KnownNetworks and Microsoft.AspNetCore.HttpOverrides.IPNetwork are obsolete. Building against them raises warning ASPDEPR005. The replacements are KnownIPNetworks and the framework's own System.Net.IPNetwork:

// .NET 10 and later
options.KnownIPNetworks.Add(
    System.Net.IPNetwork.Parse("10.0.0.0/24"));

// .NET 9 and earlier
options.KnownNetworks.Add(
    new IPNetwork(IPAddress.Parse("10.0.0.0"), 24));

KnownProxies is unaffected and works the same on every version. If your editor reports IPNetwork as an ambiguous reference, that is the two types colliding: System.Net.IPNetwork and the older Microsoft.AspNetCore.HttpOverrides.IPNetwork are both in scope. Qualify the one you mean, or move to KnownIPNetworks and drop the HttpOverrides using directive.

What if the proxy does not send X-Forwarded-For?

Point the middleware at the header it does send. Cloudflare uses CF-Connecting-IP, some appliances use True-Client-IP, and older Java stacks use Proxy-Client-IP. Set the name and the rest of the configuration is unchanged:

options.ForwardedForHeaderName = "CF-Connecting-IP";

Only one header name can be read at a time, so if traffic can arrive through two different proxies you will need to normalise upstream. Our reference of which client IP header each proxy sends lists the common ones.

Turning it on without code: ASPNETCORE_FORWARDEDHEADERS_ENABLED

There is a second way to enable the middleware, and it is the one most people meet first in containers and on Azure App Service. Set the environment variable and the host wires the middleware up for you, with no Program.cs change at all:

ASPNETCORE_FORWARDEDHEADERS_ENABLED=true

That switches on XForwardedFor | XForwardedProto exactly as the code above does. What is much less obvious is the rest of what it does: because you have opted in explicitly, it also clears KnownProxies and KnownNetworks. That is the same trust-everything posture the previous section warns against, arrived at by setting one environment variable rather than by writing Clear().

In a container whose only reachable ingress is a load balancer you control, that is usually acceptable, and it is why the shortcut exists. It stops being acceptable the moment anything else can open a connection to the app directly, because then any caller can assert whatever X-Forwarded-For they like. If your app is reachable beyond the proxy, configure ForwardedHeadersOptions in code and name your proxies instead.

The three things that go wrong

1. KnownProxies defaults to loopback

This is the one that catches almost everyone. Out of the box KnownProxies and KnownNetworks contain only 127.0.0.1 / ::1. It works perfectly on a developer machine where the proxy is local, then does nothing at all in production, where the proxy is a different host; the middleware sees an untrusted forwarder and declines to rewrite anything. There's no error; the address is simply still wrong.

The fix is to add the proxy's real address. The temptation, widely repeated in forum answers, is to clear both collections instead:

// Don't do this.
options.KnownProxies.Clear();
options.KnownNetworks.Clear();

That makes it work by trusting any forwarder, which means anyone who can reach your app can set X-Forwarded-For to whatever they like and your logs, rate limits and audit trail will believe them. It converts a silent no-op into a silent spoofing hole.

2. Middleware order

UseForwardedHeaders has to run before any middleware that depends on the client address or the scheme. Placed after UseAuthentication, UseHttpsRedirection or your logging middleware, those components have already made decisions using the proxy's address.

3. ForwardLimit with multiple proxies

ForwardLimit defaults to 1, meaning only the last entry in the chain is processed. Behind two hops (a CDN in front of a load balancer, say) you'll resolve to the intermediate proxy rather than the client. Raise it to match the number of trusted hops, and trust every one of them explicitly. Don't set it to null (unlimited) unless you have a clear reason.

Does UseForwardedHeaders fix the client IP in IIS logs?

No. The middleware only changes what your application sees. IIS writes the c-ip field in its W3C log from the TCP connection it accepted, before your application runs and independently of it, so the log keeps showing the proxy.

The middleware fixes your application's view of the client. It doesn't touch the IIS logs. IIS writes the W3C log entry (including the c-ip field) from the TCP connection it accepted, independently of whatever your application later concludes. So after configuring forwarded headers correctly you can be in the position where your app logs the right address and C:\inetpub\logs\LogFiles\ still shows the proxy for every request.

Whether that matters depends on what reads your logs:

What you need Right tool Why
The app to see the real client: auth, rate limiting, app-level logging UseForwardedHeaders Built into ASP.NET Core, no licence, no extra component. If this is all you need, you're done.
IIS W3C logs correct: SIEM, compliance, log analysers, anything reading c-ip An ISAPI filter such as X-Forwarded-For for IIS The middleware can't write to the IIS log. A filter rewrites c-ip before IIS records it, so downstream tooling needs no changes.
Both, or a mix of .NET Core and other apps on the same servers Both, together They operate at different layers and don't conflict. Classic ASP.NET, PHP and static sites on the same IIS instance get nothing from the middleware.

If you own the application code, only care about the application's behaviour, and every app on the server is ASP.NET Core, the built-in middleware is the right answer and there's nothing to buy. The filter earns its place when the log file itself has to be correct, which is usually the case where a SIEM, a compliance requirement or an existing log-analysis pipeline is involved, none of which are going to be rewritten to read your application's logs instead.

Frequently asked questions

Why does UseForwardedHeaders work locally but not in production?

Because KnownProxies and KnownNetworks default to loopback only. Locally the proxy is on the same machine so it's trusted; in production it's a different host, the middleware treats the forwarder as untrusted, and silently leaves the address unchanged. Add the production proxy's address explicitly.

Is it safe to clear KnownProxies and KnownNetworks?

No. Clearing them trusts any source of the X-Forwarded-For header, so anyone able to reach your application can forge a client address. It's a common suggestion in forum answers and it turns a configuration problem into a security one. Add your specific proxies instead.

What does ASPNETCORE_FORWARDEDHEADERS_ENABLED do?

Setting it to true enables the forwarded-headers middleware without any code change, which is why it is the usual approach in containers and on Azure App Service. It turns on XForwardedFor and XForwardedProto, and it also clears KnownProxies and KnownNetworks, so every forwarder is trusted. That is fine when the app can only be reached through a proxy you control, and unsafe when it can be reached directly. If in doubt, configure ForwardedHeadersOptions in code and list your proxies.

Where should UseForwardedHeaders go in the pipeline?

As early as possible, and definitely before authentication, HTTPS redirection and any request logging. Anything running before it sees the proxy's address rather than the client's.

Does this fix the client IP in my IIS logs?

No. IIS writes the c-ip field from the connection it accepted, before and independently of your application. The middleware only changes what your application sees. Correcting the IIS log itself needs a filter that rewrites the field as IIS records it.

What does ForwardLimit do?

It caps how many entries in the forwarded-header chain the middleware will process, defaulting to 1. With more than one trusted proxy in front of the app, raise it to the number of hops and add each of them to the trusted collections, so the middleware walks back to the genuine client rather than stopping at an intermediate proxy.

What is the difference between KnownProxies and KnownNetworks?

KnownProxies is a list of individual IP addresses matched exactly. KnownNetworks is a list of CIDR ranges matched by containment. Use KnownProxies for a few proxies at fixed addresses, and KnownNetworks when the proxies sit in a subnet or their addresses change.

What is the default value of KnownProxies?

A single entry, IPAddress.IPv6Loopback (::1). KnownNetworks separately defaults to a single entry covering 127.0.0.0/8. Together they trust the local machine only, which is why the middleware works in development and appears to do nothing once a real proxy is in front of it.

Why does KnownProxies not match my proxy's IP address?

Most often because the server is using a dual-stack socket, so the proxy arrives as an IPv4-mapped IPv6 address: 10.0.0.5 presents as ::ffff:10.0.0.5, and matching is exact. Log HttpContext.Connection.RemoteIpAddress on a real proxied request and add the address in the form it actually appears.

What is KnownIPNetworks, and what is warning ASPDEPR005?

KnownIPNetworks is the .NET 10 replacement for KnownNetworks, using System.Net.IPNetwork instead of the older Microsoft.AspNetCore.HttpOverrides.IPNetwork. Building against the old API raises warning ASPDEPR005. KnownProxies is unaffected and is unchanged across versions.

How do I use forwarded headers with Cloudflare or another non-standard header?

Set ForwardedForHeaderName to the header the proxy actually sends, such as CF-Connecting-IP for Cloudflare or True-Client-IP on some appliances. Everything else, including the KnownProxies trust check, behaves the same. Only one header name can be read at a time.

When the IIS log itself has to be right

X-Forwarded-For for IIS writes the real client address into the standard c-ip field, so SIEM, compliance and log-analysis tooling keep working unchanged, alongside the forwarded-headers middleware rather than instead of it. IIS 10 on Windows Server 2016–2025.

View the product   or request a download →