ForwardedHeaders and Reverse Proxies: The Trust Boundary Guide

TL;DR ForwardedHeaders configuration that prevents IP spoofing: KnownNetworks vs KnownProxies, Azure/AWS/Nginx setups, and verification commands.

Your ASP.NET Core app runs behind a reverse proxy. The proxy terminates TLS and sets the real client IP in X-Forwarded-For. But anyone can send that header. Without proper configuration, attackers spoof their IP address.

Common questions this answers

  • Why does my app show the wrong client IP behind a load balancer?
  • What is the difference between KnownNetworks and KnownProxies?
  • How do I configure ForwardedHeaders for Azure App Service?
  • How do I prevent X-Forwarded-For spoofing?
  • Why does my IP show as IPv6 when clients use IPv4?

Definition (what this means in practice)

ForwardedHeaders middleware reads X-Forwarded-* headers set by reverse proxies and updates HttpContext properties. Without it, your app sees the proxy's IP instead of the client's IP.

In practice, this affects rate limiting, audit logging, geolocation, and any logic that depends on client IP. Misconfiguration either breaks these features (wrong IP) or creates security vulnerabilities (spoofed IP).

Terms used

  • Reverse proxy: server that sits between clients and your app (Nginx, Azure App Service, AWS ALB).
  • X-Forwarded-For: header containing the client IP, set by proxies.
  • X-Forwarded-Proto: header indicating the original protocol (http or https).
  • X-Forwarded-Host: header containing the original Host header value.
  • KnownNetworks: IP ranges trusted to set forwarded headers.
  • KnownProxies: specific IP addresses trusted to set forwarded headers.
  • Trust boundary: the point where trusted infrastructure ends and untrusted input begins.

Reader contract

This article is for:

  • Engineers deploying ASP.NET Core behind reverse proxies.
  • Teams troubleshooting incorrect client IP detection.

You will leave with:

  • Understanding of the IP spoofing attack.
  • Configuration patterns for Azure, AWS, Nginx, and Cloudflare.
  • Verification commands to test your setup.

This is not for:

  • Setting up reverse proxy infrastructure.
  • Load balancer architecture decisions.
  • CDN configuration tutorials.

Quick start (10 minutes)

If you deploy to Azure App Service and need correct client IPs:

Verified on: ASP.NET Core (.NET 10).

// Program.cs - First middleware after Build()
var app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks =
    {
        // Azure App Service uses IPv4-mapped IPv6 addresses
        new IPNetwork(IPAddress.Parse("::ffff:10.0.0.0"), 104),     // 10.0.0.0/8
        new IPNetwork(IPAddress.Parse("::ffff:172.16.0.0"), 108),   // 172.16.0.0/12
        new IPNetwork(IPAddress.Parse("::ffff:192.168.0.0"), 112)   // 192.168.0.0/16
    }
});

// All other middleware follows
app.UseHttpsRedirection();

The IP spoofing attack ForwardedHeaders enables

Here is the attack scenario without proper configuration:

  1. Your app trusts any X-Forwarded-For header.
  2. Attacker sends request with X-Forwarded-For: 192.168.1.1.
  3. Your proxy adds the real client IP to the header.
  4. Header now contains: X-Forwarded-For: 192.168.1.1, 203.0.113.50.
  5. Your app reads the leftmost (attacker-controlled) value.
  6. Rate limiting, audit logs, and IP-based rules use the spoofed IP.

The fix: Only trust X-Forwarded-For values from known proxy IP ranges. Strip untrusted values.

ForwardedHeaders fundamentals

The middleware processes three headers by default:

Header Updates Use case
X-Forwarded-For HttpContext.Connection.RemoteIpAddress Client IP for logging, rate limiting
X-Forwarded-Proto HttpContext.Request.Scheme HTTPS detection for redirects, cookies
X-Forwarded-Host HttpContext.Request.Host Original host for URL generation

Basic configuration

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

This configuration has a critical problem: it trusts all proxies by default when both KnownProxies and KnownNetworks are empty. Any client can spoof headers.

Secure configuration

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks = { new IPNetwork(IPAddress.Parse("10.0.0.0"), 8) }
});

Now only requests from 10.0.0.0/8 can set trusted forwarded headers.

KnownNetworks vs KnownProxies decision

Both restrict which sources can set forwarded headers. Choose based on your infrastructure:

Criterion KnownNetworks KnownProxies
IP stability Dynamic or range Static, known IPs
Configuration CIDR notation Individual addresses
Cloud environments Preferred Rarely practical
On-premises Works Works
Maintenance Low (ranges change rarely) High (IPs may change)

Use KnownNetworks when

  • Proxies have dynamic IPs within a known range.
  • Cloud provider uses private IP ranges.
  • You cannot predict exact proxy IPs.

Use KnownProxies when

  • You have a fixed set of proxy servers.
  • IPs never change.
  • You want explicit allowlisting.

Combining both

You can use both simultaneously:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks = { new IPNetwork(IPAddress.Parse("10.0.0.0"), 8) },
    KnownProxies = { IPAddress.Parse("192.168.1.100") }
});

Azure App Service configuration

Azure App Service runs Kestrel behind Azure's load balancer. The load balancer uses private IP ranges.

Critical detail: Azure presents IPv4 addresses as IPv4-mapped IPv6 addresses. You must configure networks accordingly.

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks =
    {
        // RFC 1918 private ranges as IPv4-mapped IPv6
        new IPNetwork(IPAddress.Parse("::ffff:10.0.0.0"), 104),     // 10.0.0.0/8
        new IPNetwork(IPAddress.Parse("::ffff:172.16.0.0"), 108),   // 172.16.0.0/12
        new IPNetwork(IPAddress.Parse("::ffff:192.168.0.0"), 112)   // 192.168.0.0/16
    }
});

IPv4 to IPv6-mapped prefix length conversion

IPv4 CIDR prefix + 96 = IPv6-mapped prefix.

IPv4 range IPv4 prefix IPv6 prefix
10.0.0.0/8 8 104
172.16.0.0/12 12 108
192.168.0.0/16 16 112

App Service environment variables

Azure sets these headers automatically. No additional App Service configuration needed.

AWS ALB/ELB configuration

AWS Application Load Balancer sets X-Forwarded-For. Configure your app to trust the ALB's VPC CIDR.

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks =
    {
        // Your VPC CIDR - replace with actual range
        new IPNetwork(IPAddress.Parse("10.0.0.0"), 16)  // Example: 10.0.0.0/16
    }
});

Check your VPC configuration for the exact CIDR range.

Nginx reverse proxy configuration

Nginx requires explicit header configuration on both sides.

Nginx configuration

location / {
    proxy_pass http://localhost:5000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

ASP.NET Core configuration

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownProxies = { IPAddress.Parse("127.0.0.1") }  // Nginx on same machine
});

For Nginx on a different server:

KnownProxies = { IPAddress.Parse("192.168.1.50") }  // Nginx server IP

Cloudflare configuration

Cloudflare adds X-Forwarded-For and also provides CF-Connecting-IP with just the client IP.

Option 1: Trust Cloudflare IP ranges

Cloudflare publishes their IP ranges. You can configure them as KnownNetworks, but the list changes.

// Cloudflare IPv4 ranges (check cloudflare.com/ips for current list)
KnownNetworks =
{
    new IPNetwork(IPAddress.Parse("173.245.48.0"), 20),
    new IPNetwork(IPAddress.Parse("103.21.244.0"), 22),
    // ... add all Cloudflare ranges
}

Option 2: Use CF-Connecting-IP header

Cloudflare's CF-Connecting-IP contains only the client IP. Configure ForwardedHeaders to read it:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    ForwardedForHeaderName = "CF-Connecting-IP",
    ForwardLimit = 1
});

This approach requires ensuring requests actually come through Cloudflare (firewall rules).

Middleware ordering requirements

ForwardedHeaders must run first. It modifies HttpContext properties that other middleware depends on.

var app = builder.Build();

// 1. ForwardedHeaders FIRST
app.UseForwardedHeaders(options);

// 2. Then everything else
app.UseHttpsRedirection();  // Uses Scheme from ForwardedHeaders
app.UseHsts();
app.UseRouting();
app.UseRateLimiter();       // Uses RemoteIpAddress from ForwardedHeaders
app.UseAuthorization();

If ForwardedHeaders runs after UseHttpsRedirection, the redirect check uses the wrong scheme.

IPv4-mapped IPv6 addresses

Modern .NET and Linux often represent IPv4 addresses as IPv4-mapped IPv6 addresses.

IPv4 IPv4-mapped IPv6
10.0.0.1 ::ffff:10.0.0.1
192.168.1.1 ::ffff:192.168.1.1

When this matters

  • Azure App Service uses IPv4-mapped IPv6 internally.
  • Kestrel on Linux may report IPv4-mapped IPv6.
  • KnownNetworks must match the actual format.

Checking which format you receive

app.MapGet("/debug/ip", (HttpContext context) =>
{
    var ip = context.Connection.RemoteIpAddress;
    return new
    {
        Raw = ip?.ToString(),
        IsIPv4MappedToIPv6 = ip?.IsIPv4MappedToIPv6,
        MapToIPv4 = ip?.IsIPv4MappedToIPv6 == true ? ip.MapToIPv4().ToString() : null
    };
});

Verification and testing

Check current configuration

app.MapGet("/debug/headers", (HttpContext context) =>
{
    return new
    {
        RemoteIpAddress = context.Connection.RemoteIpAddress?.ToString(),
        Scheme = context.Request.Scheme,
        Host = context.Request.Host.ToString(),
        XForwardedFor = context.Request.Headers["X-Forwarded-For"].ToString(),
        XForwardedProto = context.Request.Headers["X-Forwarded-Proto"].ToString()
    };
});

Test spoofing protection

From outside your network:

# This should NOT change the reported IP
curl -H "X-Forwarded-For: 1.2.3.4" https://yourapp.com/debug/headers

If RemoteIpAddress shows 1.2.3.4, your configuration is vulnerable.

Verify HTTPS detection

# Behind proxy with HTTPS termination
curl https://yourapp.com/debug/headers
# Scheme should be "https"

Copy/paste artifact: ForwardedHeaders configurations

Azure App Service

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks =
    {
        new IPNetwork(IPAddress.Parse("::ffff:10.0.0.0"), 104),
        new IPNetwork(IPAddress.Parse("::ffff:172.16.0.0"), 108),
        new IPNetwork(IPAddress.Parse("::ffff:192.168.0.0"), 112)
    }
});

AWS with VPC

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownNetworks =
    {
        new IPNetwork(IPAddress.Parse("10.0.0.0"), 16)  // Replace with your VPC CIDR
    }
});

Local Nginx

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
    KnownProxies = { IPAddress.Parse("127.0.0.1") }
});

Development (trust all - NEVER in production)

if (app.Environment.IsDevelopment())
{
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.All
    });
}

Common failure modes

Symptom Likely cause
IP always shows proxy IP ForwardedHeaders not configured or wrong position
IP shows spoofed value KnownNetworks/KnownProxies not configured
HTTPS redirects loop X-Forwarded-Proto not processed
KnownNetworks not matching IPv4 vs IPv4-mapped IPv6 mismatch
Works locally, fails in prod Different proxy infrastructure

Checklist

  • ForwardedHeaders middleware is first after Build().
  • KnownNetworks or KnownProxies configured for your infrastructure.
  • IPv4-mapped IPv6 addresses used if required (Azure App Service).
  • X-Forwarded-Proto included for HTTPS detection.
  • Spoofing protection verified with external curl test.
  • Debug endpoint removed before production deployment.

FAQ

Should I use ForwardedHeaders in development?

Only if you are testing proxy behavior. For local development without a proxy, ForwardedHeaders is unnecessary and may cause confusion.

What if my proxy IP changes?

Use KnownNetworks with the IP range instead of KnownProxies with specific IPs.

Can I trust X-Forwarded-For from the internet?

No. Only trust it from known proxy infrastructure within your network boundary.

Why does Azure use IPv4-mapped IPv6?

Azure App Service runs on Linux with dual-stack networking. IPv4 connections are represented as IPv4-mapped IPv6 addresses internally.

How many proxy hops does ForwardedHeaders support?

By default, ForwardLimit is 1 (trusts one proxy). Increase it if you have multiple proxies in sequence, but each proxy must be in KnownNetworks/KnownProxies.

What to do next

Review your ForwardedHeaders configuration. If KnownNetworks and KnownProxies are both empty, you are vulnerable to IP spoofing. Add the appropriate network ranges for your infrastructure.

Test spoofing protection by sending a request with a fake X-Forwarded-For header from outside your network.

For more on middleware ordering, read Middleware Pipeline Order.

If you want help securing your proxy configuration, reach out via Contact.

References

Author notes

Decisions:

  • Emphasize KnownNetworks over KnownProxies. Rationale: cloud environments rarely have static proxy IPs.
  • Include IPv4-mapped IPv6 conversion table. Rationale: this is the most common misconfiguration for Azure App Service.
  • Show the spoofing attack first. Rationale: understanding the threat motivates proper configuration.

Observations:

  • Teams often copy ForwardedHeaders examples without KnownNetworks, leaving the app vulnerable.
  • Azure App Service IPv4-mapped IPv6 is the most common configuration mistake.
  • Debug endpoints are frequently left in production, exposing internal information.