Skip to content

[Feature] Optional forwarded-headers support for running behind a TLS-terminating reverse proxy #283

Open

Description

@LarsLaskowski

Summary

DockerUpdateGuard has no authentication and is meant to run behind a trusted perimeter, typically a reverse proxy that terminates TLS (and often authenticates users). The application does not process X-Forwarded-For, X-Forwarded-Proto or X-Forwarded-Host, so behind such a proxy every request looks like plain HTTP from the proxy's address: HSTS is never sent, HTTPS redirection cannot work, and logs/traces show the proxy instead of the client. This feature adds optional, explicitly configured forwarded-headers support so the application sees the original scheme, host and client address.

Area

  • docs/areas/hosting.md (added in [Docs] Describe the current behavior by area #263) — HTTP pipeline, Security posture, Open question 2
  • docs/areas/telemetry.md (client address of incoming-request spans/logs; informational)
  • Security area (.squad/project.md): TLS and transport relaxations — HSTS/HTTPS redirection behavior changes, and trusting forwarded headers is a trust decision that must stay opt-in.

Current behavior

  • src/DockerUpdateGuard/Program.cs:30-35:
if (app.Environment.IsDevelopment() == false)
{
    app.UseExceptionHandler("/error", createScopeForErrors: true);
    app.UseHsts();
    app.UseHttpsRedirection();
}

There is no UseForwardedHeaders and no ForwardedHeadersOptions anywhere in src/.

  • The container listens on plain HTTP only: src/DockerUpdateGuard/Dockerfile:44-45 (ENV ASPNETCORE_URLS=http://+:8080).
  • Consequences behind a TLS-terminating proxy (proxy → http://app:8080):
    • HttpRequest.IsHttps is always false, so UseHsts() never adds Strict-Transport-Security (HSTS is only sent on HTTPS requests).
    • UseHttpsRedirection() finds no HTTPS port (no HTTPS endpoint, no HTTPS_PORT/ASPNETCORE_HTTPS_PORT), logs "Failed to determine the https port for redirect" and does not redirect; a plain-HTTP request that reaches the proxy is never upgraded by the application.
    • HttpContext.Connection.RemoteIpAddress is the proxy's address, so the ASP.NET Core instrumentation and request logs record the proxy, not the client.
    • Absolute URLs generated by the app use http and the internal host name.
  • The only built-in escape hatch, the environment variable ASPNETCORE_FORWARDEDHEADERS_ENABLED=true, is undocumented here, processes only X-Forwarded-For/X-Forwarded-Proto, and clears the known proxies and networks, i.e. it trusts forwarded headers from any sender — not acceptable as the supported configuration.

Expected behavior

Decision (product manager): forwarded-headers support is added as an optional feature, off by default, with configurable known proxies/networks.

  • New configuration section DockerUpdateGuard:ForwardedHeaders:

    Key Default Meaning
    Enabled false Process forwarded headers. When false, the pipeline is exactly as today.
    KnownProxies [] IP addresses of trusted proxies (IPv4/IPv6).
    KnownNetworks [] Trusted proxy networks in CIDR notation (e.g. 172.18.0.0/16).
    ForwardLimit 1 Maximum number of proxy hops processed (range 1–10).
    AllowedHosts [] Host names accepted from X-Forwarded-Host. X-Forwarded-Host is processed only when this list is not empty.
  • When Enabled is true:

    • X-Forwarded-For and X-Forwarded-Proto are processed, and X-Forwarded-Host too when AllowedHosts is set; headers are honored only when the immediate sender's address is in KnownProxies/KnownNetworks (loopback stays trusted as in the platform default). Headers from any other sender are ignored.
    • Forwarded-headers processing runs first in the pipeline, before the exception handler, HSTS, HTTPS redirection, status-code pages and anti-forgery.
    • A request forwarded with X-Forwarded-Proto: https is treated as HTTPS: outside Development the response carries Strict-Transport-Security, and HTTPS redirection does not redirect it (no redirect loop).
    • A request forwarded with X-Forwarded-Proto: http is redirected to HTTPS by HTTPS redirection when an HTTPS port is configured through the platform's standard setting (ASPNETCORE_HTTPS_PORT / HTTPS_PORT, e.g. 443); the redirect target uses the forwarded host. Without a configured HTTPS port the behavior is as today (no redirect, platform warning).
    • The client address of incoming requests (logs, ASP.NET Core spans) is the forwarded client address.
  • Validation (startup and reload, same validator):

    • Enabled: true requires at least one entry in KnownProxies or KnownNetworks, so the feature can never trust every sender; the message names the key ('DockerUpdateGuard:ForwardedHeaders:KnownProxies' or 'DockerUpdateGuard:ForwardedHeaders:KnownNetworks' must be configured when forwarded headers are enabled).
    • Every KnownProxies entry must parse as an IP address and every KnownNetworks entry as CIDR; ForwardLimit must be 1–10. Messages name the key, not the value.
  • The section is read once at startup (pipeline configuration); changing it requires a restart, which the area document lists.

  • Development keeps its current behavior (no HSTS, no HTTPS redirection); forwarded-headers processing, when enabled, applies in every environment.

Acceptance criteria

  • With Enabled: false (default) the middleware pipeline and responses are unchanged (no forwarded-header processing).
  • With Enabled: true and the sender in KnownNetworks, a request with X-Forwarded-Proto: https gets Strict-Transport-Security (non-Development) and is not redirected.
  • With Enabled: true, a request with X-Forwarded-Proto: http and a configured HTTPS port is redirected to https://{forwarded host}:{port}/….
  • Forwarded headers from a sender outside KnownProxies/KnownNetworks are ignored (scheme, host and remote IP stay as received).
  • X-Forwarded-Host is ignored unless AllowedHosts contains the host.
  • Validation rejects Enabled: true without proxies/networks, unparsable addresses/CIDRs, and ForwardLimit outside 1–10, with key-naming messages.
  • Unit tests (MSTest, no new package needed — the middlewares can be invoked directly on a DefaultHttpContext):
    • DockerUpdateGuardOptionsValidatorTests: DockerUpdateGuardOptionsValidatorForwardedHeadersEnabledWithoutProxiesFails, …InvalidKnownNetworkFails, …ForwardLimitOutOfRangeFails, …ValidForwardedHeadersSucceeds
    • tests for the options mapping, e.g. ForwardedHeadersConfigurationKnownNetworksMapToKnownIPNetworks, ForwardedHeadersConfigurationWithoutAllowedHostsDoesNotForwardHost
    • tests running ForwardedHeadersMiddleware + HSTS/HTTPS redirection on a DefaultHttpContext, e.g. ForwardedHeadersTrustedProxyHttpsProtoSendsHsts, ForwardedHeadersUntrustedSenderIgnoresHeaders.
  • Coverage gate passes.
  • appsettings.json contains the section with defaults; the README configuration reference documents every key (key, environment variable form, default); INSTALL.md gets a short "behind a reverse proxy" note (enable, set KnownNetworks to the proxy's Docker network, set ASPNETCORE_HTTPS_PORT).
  • docs/ARCHITECTURE.md (Configuration model, Security posture) updated.
  • The area document docs/areas/hosting.md describes the new behavior and its Known gaps entry for this issue is removed.

Implementation notes

  • New options class src/DockerUpdateGuard/Configuration/ForwardedHeadersSettings.cs (or …Options), property ForwardedHeaders on DockerUpdateGuardOptions, validation in DockerUpdateGuardOptionsValidator (Configuration/DockerUpdateGuardOptionsValidator.cs).
  • Configure ForwardedHeadersOptions in AddDockerUpdateGuardHost (src/DockerUpdateGuard/ServiceCollectionExtensions.cs) from the startup-bound applicationOptions (as is done for the database and HTTP clients there). On .NET 10 use KnownIPNetworks (System.Net.IPNetwork); ForwardedHeadersOptions.KnownNetworks is obsolete (ASPDEPR005) and would raise an analyzer diagnostic.
  • Call app.UseForwardedHeaders() as the first middleware in Program.cs, only when enabled. Keeping the decision in a small extension method (e.g. UseDockerUpdateGuardForwardedHeaders) keeps Program.Main thin and makes it testable.
  • Do not use or document ASPNETCORE_FORWARDEDHEADERS_ENABLED as the solution — it trusts all senders.
  • Test pitfall: HstsMiddleware never sends the header for localhost, 127.0.0.1 and [::1] (default ExcludedHosts); tests must use a non-loopback host such as dug.example.
  • HTTPS port: rely on the platform's ASPNETCORE_HTTPS_PORT/HTTPS_PORT instead of a new key; document it.
  • Blazor Server uses a WebSocket/long-polling connection: verify that the circuit still connects behind the proxy with the feature on (manual check, note in the PR).
  • Must not change: no authentication is added (guarantee in .squad/project.md); the container still listens on plain HTTP 8080; Development behavior; error/not-found pages.
  • Integration surface for a new configuration option (.squad/project.md): options class + validation, appsettings*.json, README configuration reference, INSTALL.md/compose notes, docs/ARCHITECTURE.md Configuration model.

Related


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions