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/10.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: 10.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/10.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

  • Place forwarded headers early in the pipeline (before enabling configuration) so it applies to subsequent routing.
  • 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 Middleware registered too late Call Add-KrForwardedHeader before Enable-KrConfiguration and before adding routes.
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: None