Repository navigation
[Feature] Optional forwarded-headers support for running behind a TLS-terminating reverse proxy #283
Description
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 2docs/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.IsHttpsis alwaysfalse, soUseHsts()never addsStrict-Transport-Security(HSTS is only sent on HTTPS requests).UseHttpsRedirection()finds no HTTPS port (no HTTPS endpoint, noHTTPS_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.RemoteIpAddressis 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
httpand the internal host name.
- The only built-in escape hatch, the environment variable
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true, is undocumented here, processes onlyX-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 EnabledfalseProcess 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).ForwardLimit1Maximum number of proxy hops processed (range 1–10). AllowedHosts[]Host names accepted from X-Forwarded-Host.X-Forwarded-Hostis processed only when this list is not empty. -
When
Enabledistrue:X-Forwarded-ForandX-Forwarded-Protoare processed, andX-Forwarded-Hosttoo whenAllowedHostsis set; headers are honored only when the immediate sender's address is inKnownProxies/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: httpsis treated as HTTPS: outsideDevelopmentthe response carriesStrict-Transport-Security, and HTTPS redirection does not redirect it (no redirect loop). - A request forwarded with
X-Forwarded-Proto: httpis 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: truerequires at least one entry inKnownProxiesorKnownNetworks, 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
KnownProxiesentry must parse as an IP address and everyKnownNetworksentry as CIDR;ForwardLimitmust 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.
-
Developmentkeeps 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: trueand the sender inKnownNetworks, a request withX-Forwarded-Proto: httpsgetsStrict-Transport-Security(non-Development) and is not redirected. - With
Enabled: true, a request withX-Forwarded-Proto: httpand a configured HTTPS port is redirected tohttps://{forwarded host}:{port}/…. - Forwarded headers from a sender outside
KnownProxies/KnownNetworksare ignored (scheme, host and remote IP stay as received). -
X-Forwarded-Hostis ignored unlessAllowedHostscontains the host. - Validation rejects
Enabled: truewithout proxies/networks, unparsable addresses/CIDRs, andForwardLimitoutside 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 aDefaultHttpContext, e.g.ForwardedHeadersTrustedProxyHttpsProtoSendsHsts,ForwardedHeadersUntrustedSenderIgnoresHeaders.
- Coverage gate passes.
-
appsettings.jsoncontains the section with defaults; the README configuration reference documents every key (key, environment variable form, default);INSTALL.mdgets a short "behind a reverse proxy" note (enable, setKnownNetworksto the proxy's Docker network, setASPNETCORE_HTTPS_PORT). -
docs/ARCHITECTURE.md(Configuration model, Security posture) updated. - The area document
docs/areas/hosting.mddescribes 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), propertyForwardedHeadersonDockerUpdateGuardOptions, validation inDockerUpdateGuardOptionsValidator(Configuration/DockerUpdateGuardOptionsValidator.cs). - Configure
ForwardedHeadersOptionsinAddDockerUpdateGuardHost(src/DockerUpdateGuard/ServiceCollectionExtensions.cs) from the startup-boundapplicationOptions(as is done for the database and HTTP clients there). On .NET 10 useKnownIPNetworks(System.Net.IPNetwork);ForwardedHeadersOptions.KnownNetworksis obsolete (ASPDEPR005) and would raise an analyzer diagnostic. - Call
app.UseForwardedHeaders()as the first middleware inProgram.cs, only when enabled. Keeping the decision in a small extension method (e.g.UseDockerUpdateGuardForwardedHeaders) keepsProgram.Mainthin and makes it testable. - Do not use or document
ASPNETCORE_FORWARDEDHEADERS_ENABLEDas the solution — it trusts all senders. - Test pitfall:
HstsMiddlewarenever sends the header forlocalhost,127.0.0.1and[::1](defaultExcludedHosts); tests must use a non-loopback host such asdug.example. - HTTPS port: rely on the platform's
ASPNETCORE_HTTPS_PORT/HTTPS_PORTinstead 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 HTTP8080;Developmentbehavior; 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.mdConfiguration model.
Related
- PR [Docs] Describe the current behavior by area #263 (area documents)
Generated by Claude Code
Activity