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

  1. Logging: Register a console logger at Debug and set it as default.
  2. Server: Create a server named ‘Forwarded Headers Demo’.
  3. Middleware: Add forwarded headers with Add-KrForwardedHeader to process X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host.
  4. Trust: For local testing, trust loopback proxies so forwarded headers are honored.
  5. Listener: Expose an HTTP listener on the configured port and IP.
  6. Configuration: Call Enable-KrConfiguration to finalize middleware.
  7. Route: Map /forward to return scheme, host, and remoteIp (after forwarding is applied).
  8. Start: Run Start-KrServer to 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-KrConfiguration so 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/KnownNetworks to trusted proxy addresses or ranges.
  • Supported flags include XForwardedFor, XForwardedProto, XForwardedHost, and XForwardedPathBase.
  • ForwardLimit controls how many X-Forwarded-For entries 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


Previous / Next

Previous: Host Filtering Next: CORS