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:
- Your app trusts any X-Forwarded-For header.
- Attacker sends request with
X-Forwarded-For: 192.168.1.1. - Your proxy adds the real client IP to the header.
- Header now contains:
X-Forwarded-For: 192.168.1.1, 203.0.113.50. - Your app reads the leftmost (attacker-controlled) value.
- 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
- Configure ASP.NET Core to work with proxy servers and load balancers
- Configure an ASP.NET Core app for Azure App Service (ForwardedHeaders on Azure)
- Host ASP.NET Core on Linux with Nginx
- Forwarded Headers Middleware ignores X-Forwarded-* headers from unknown proxies
- ForwardedHeadersOptions Class
- IPNetwork Class
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.