Forwarded Headers
Honor X-Forwarded-* headers from a reverse proxy to reflect the original client IP, scheme, and host.
Full source
File: pwsh/tutorial/examples/15.7-Forwarded-Header.ps1
<#
Sample Kestrun Server - Forwarded Headers
This script demonstrates enabling Forwarded Headers middleware
and exposing a diagnostic route that returns key request fields
(scheme, host, and remote IP) to show header effects.
FileName: 15.7-Forwarded-Header.ps1
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# (Optional) Configure console logging so we can see events
New-KrLogger | Set-KrLoggerLevel -Value Debug |
Add-KrSinkConsole |
Register-KrLogger -Name 'console' -SetAsDefault | Out-Null
# Create a new Kestrun server
New-KrServer -Name 'Forwarded Headers Demo'
# Add a listener on the configured port and IP address
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# Enable Forwarded Headers middleware. Trust loopback so tests/local can pass headers.
# Process X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host.
# Limit to 1 forward (the first value in a comma-delimited list).
# Trust local reverse proxies (loopback)
Add-KrForwardedHeader -XForwardedFor -XForwardedProto -XForwardedHost -XForwardedPrefix `
-KnownProxies '127.0.0.1' -ForwardLimit 1
# Enable Kestrun configuration
Enable-KrConfiguration
# Map a diagnostic route that reveals forwarded effects
Add-KrMapRoute -Verbs Get -Pattern '/forward' -ScriptBlock {
# 1) Effective host & hostPort (prefer HostString; fallback to raw Host header)
$hs = $Context.Request.Host
$hostOnly = $hs.Host
$hostPort = $hs.Port # nullable int
if ([string]::IsNullOrEmpty($hostOnly)) {
$rawHost = [string]$Context.Request.Headers['Host']
if ($rawHost) {
$hs = [Microsoft.AspNetCore.Http.HostString]::FromUriComponent($rawHost)
$hostOnly = $hs.Host
$hostPort = $hs.Port
}
}
$scheme = $Context.Request.Scheme
$basePath = $Context.Request.PathBase.ToString()
$path = $Context.Request.Path.ToString()
# 2) Connection info (what Kestrel is bound to, and the peer)
if ($null -eq $Context.Connection.LocalIpAddress) {
$localIp = ''
} else {
$localIp = $Context.Connection.LocalIpAddress.ToString()
}
$localPort = $Context.Connection.LocalPort # Kestrun alias may be .Port
if ($null -eq $Context.Connection.RemoteIpAddress) {
$remoteIp = ''
} else {
$remoteIp = $Context.Connection.RemoteIpAddress.ToString()
}
$remotePort = $Context.Connection.RemotePort
# 3) Host-with-port for display: include port only if non-default for scheme
$isDefaultPort = ($scheme -eq 'https' -and $hostPort -eq 443) -or
($scheme -eq 'http' -and $hostPort -eq 80)
if ($hostPort -and -not $isDefaultPort) {
$hostWithPort = "$hostOnly`:$hostPort"
} else {
$hostWithPort = $hostOnly
}
if ($null -eq $hostPort) {
$hostPort = ''
}
Write-KrJsonResponse @{
scheme = $scheme
host = $hostOnly
hostPort = $hostPort # from Host header (may be null)
hostWithPort = $hostWithPort
basePath = $basePath
path = $path
fullPath = "$basePath$path"
# Connection (socket) facts
localIp = $localIp
localPort = $localPort # what Kestrel/listener is using
remoteIp = $remoteIp # respects X-Forwarded-For if trusted
remotePort = $remotePort
}
}
# Initial informational log
Write-KrLog -Level Information -Message 'Server {Name} configured.' -Values 'Forwarded Headers Demo'
# Start the server and close all the loggers when the server stops
# This is equivalent to calling Close-KrLogger after Start-KrServer
Start-KrServer -CloseLogsOnExit
Step-by-step
- Logging: Register a console logger at Debug and set it as default.
- Server: Create a server named ‘Forwarded Headers Demo’.
- Middleware: Add forwarded headers with
Add-KrForwardedHeaderto processX-Forwarded-For,X-Forwarded-Proto, andX-Forwarded-Host. - Trust: For local testing, trust loopback proxies so forwarded headers are honored.
- Listener: Expose an HTTP listener on the configured port and IP.
- Configuration: Call
Enable-KrConfigurationto finalize middleware. - Route: Map
/forwardto returnscheme,host, andremoteIp(after forwarding is applied). - Start: Run
Start-KrServerto start the server.
Try it
Start the sample (default port 5000):
pwsh ./docs/_includes/examples/pwsh/15.7-Forwarded-Header.ps1
Send a request simulating a reverse proxy:
curl -s http://127.0.0.1:5000/forward \
-H "X-Forwarded-For: 203.0.113.9" \
-H "X-Forwarded-Proto: https" \
-H "X-Forwarded-Host: proxy.example.test" | jq
PowerShell equivalent:
Invoke-WebRequest http://127.0.0.1:5000/forward -Headers @{
'X-Forwarded-For'='203.0.113.9'; 'X-Forwarded-Proto'='https'; 'X-Forwarded-Host'='proxy.example.test'
} | ForEach-Object { $_.Content }
Expected JSON (values depend on headers):
{
"scheme": "https",
"host": "proxy.example.test",
"remoteIp": "203.0.113.9"
}
Key Points
- Register forwarded headers before
Enable-KrConfigurationso Kestrun includes them in the built-in middleware pipeline. - Kestrun currently applies forwarded headers after its routing and CORS middleware have selected a route, but before route handlers and any later user middleware execute. Route handlers therefore see the corrected values, while earlier components such as initial logging, diagnostics, or exception handling still see the original connection data.
- In production, restrict
KnownProxies/KnownNetworksto trusted proxy addresses or ranges. - Supported flags include
XForwardedFor,XForwardedProto,XForwardedHost, andXForwardedPathBase. ForwardLimitcontrols how manyX-Forwarded-Forentries are considered.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Request.Scheme stays http | Headers are missing, the proxy is not trusted, or you are checking the value before Kestrun applies forwarded headers | Call Add-KrForwardedHeader before Enable-KrConfiguration, verify -KnownProxies/-KnownNetworks, and check the effective values in the route or later middleware. |
Host remains localhost:port | Proxy not trusted or host rewriting disabled | Ensure loopback or your proxy IPs are in -KnownProxies/-KnownNetworks. Include X-Forwarded-Host in -ForwardedHeaders. |
RemoteIpAddress is $null | X-Forwarded-For not considered or exceeded ForwardLimit | Include XForwardedFor flag and set -ForwardLimit 1 (or appropriate value). |
| Works with curl but fails in browser | Browser/proxy strips headers | Verify actual headers on the wire; use devtools or a proxy tool. |
| Mixed proxy chains | Multiple entries in X-Forwarded-For | Increase -ForwardLimit and enumerate KnownNetworks/KnownProxies accordingly. |
References
- Guide: Forwarded Headers
- Add-KrForwardedHeader
- Enable-KrConfiguration
- Add-KrMapRoute
- Start-KrServer
- Write-KrJsonResponse
- New-KrLogger
- Set-KrLoggerLevel
- Add-KrSinkConsole
- Register-KrLogger
- RoutingTopic
Previous / Next
Previous: Host Filtering Next: CORS