Repository navigation
fix: trust ingress forwarded headers - #305
Linxiushen wants to merge 13 commits into
Conversation
|
👋 This PR needs a couple of things fixed before OpenHands can review it:
Push an update once this is addressed and this check re-runs automatically. This is an automated check - no AI was used to generate this comment. |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Summary — Three-line Dockerfile change adding ENV FORWARDED_ALLOW_IPS='*'. The change is correct and does what the linked issue (#245) asks.
Verification performed on head 2268e75:
- uvicorn is pinned at 0.45.0 (
uv.lock).ConfigreadsFORWARDED_ALLOW_IPSfrom the environment whenforwarded_allow_ips is None, andProxyHeadersMiddlewareis installed by default (proxy_headers=True). I confirmed_TrustedHosts('*').always_trust is True, soX-Forwarded-ProtoandX-Forwarded-Forare honored from any peer — the reported/automations307 with anhttp://Locationis fixed. - Docker strips the surrounding single quotes, so the stored value is
*, not the literal'*'. This matters:_TrustedHosts("'*'")yieldsalways_trust=False, i.e. the fix would silently no-op. The existingENV PYTHONPATH='/app'in the same file already depends on quote stripping, andapp.pyimports correctly, which corroborates it. - Searched the service for consumers of forwarded/proxy data. Nothing reads
request.client, the client IP, or the request scheme for a security decision: auth is header/cookie based (auth.py), telemetry ignores the scheme, and every absolute URL the service emits comes fromsettings.resolved_base_urlin config, not from the request. So the wider trust surface does not subvert an existing in-app control.
Non-blocking observations (not merge blockers):
*trusts every peer, which is broader than the "from in-cluster ingress addresses" wording in the PR summary — the in-cluster restriction is left entirely to the network layer. This is exactly the trade-off the linked issue recommends for an image deployed behind an ingress, and on the current head there is no code path where client IP or scheme gates access, so it is not exploitable here. Worth keeping in mind if IP-based rate limiting or audit logging is added later (see theTODOinevent_router.py/webhook_router.py).- Nothing tests this configuration. CI for the head did not execute substantive jobs (only the two
pr-titlechecks completed; theci,Run tests, andDockerruns recorded for this head contain zero jobs), and no unit test asserts the env var or builds the image. A future regression in the var name or quoting would therefore be silent.
✅ APPROVED
2268e75 to
c5a5503
Compare
|
Thanks for the review — both non-blocking points are addressed. Test coverage: commit c5a5503 adds Trust scope: agreed the summary wording is looser than the code. The ingress pod address is not stable, so the in-cluster restriction is deliberately left to the network layer, and I also rebased onto current |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Scope — Belongs in this repository. containers/Dockerfile is documented in AGENTS.md as part of the repo layout, and the change is a deployment-config fix plus its test. No product/architecture decision is needed.
Summary — The head is now c5a5503, which rebases the ENV FORWARDED_ALLOW_IPS='*' fix onto current main and adds tests/test_forwarded_headers.py. The change is correct and the new coverage is real.
Verified on this head:
- uvicorn is pinned at 0.45.0 (
uv.lock).ConfigreadsFORWARDED_ALLOW_IPSonly whenforwarded_allow_ips is None, and installsProxyHeadersMiddlewareby default (proxy_headers=True). With the Dockerfile's value I reproduced the reported behavior directly:X-Forwarded-Proto: httpsfrom a non-loopback peer yields307withLocation: https://…when trusted andLocation: http://…when not — so the/automationsredirect from #245 is fixed by this setting alone. - Docker strips the surrounding single quotes, so the stored value is
*, not the literal'*';_TrustedHosts("'*'").always_trustisFalse, meaning a quoting regression would silently no-op. The test pins exactly that trap. - The new tests bite. I mutated the Dockerfile and confirmed the failures the author describes: deleting the
ENVline fails 3 of 5, and storing"'*'"fails 3 of 5. Unmutated, all 5 pass, as do the adjacenttest_config.py/test_health.pysuites (60 passed). - CI for this head is green:
ci,Docker,Run tests,backend,unit-tests, and the PR-metadata checks all completed successfully (the earlier red runs were recorded against the stale August head and contained no jobs). - I re-searched the service for consumers of forwarded/proxy data. Nothing reads
request.client, the client IP, or the request scheme to make a security decision: auth is header/cookie based (auth.py), telemetry ignores the scheme, CORS keys off request headers rather than peer address, noTrustedHostMiddlewareor IP-based rate limiter exists (only theTODOs inevent_router.py/webhook_router.py), and the onlyRedirectResponseuses a storedtarball_pathrather than a request-derived URL. The wider trust surface therefore does not subvert an existing in-app control.
Non-blocking observations:
*trusts every peer, which is broader than the "from in-cluster ingress addresses" wording in the PR summary; the in-cluster restriction lives entirely at the network layer. This is the trade-off issue #245 recommends for an ingress-fronted image, and per the search above it is not exploitable on the current head. It does meanscope["client"]— and hence uvicorn's default access log — becomes peer-controllable, so it is worth revisiting if IP-based rate limiting or IP-based audit logging is ever added.- The tests assert the
ENVvalue and Uvicorn's resolution of it, but not theCMD. A--forwarded-allow-ipsflag added to theCMDwould override the environment variable and leave all 5 tests passing (I confirmed this by adding--forwarded-allow-ips 127.0.0.1to theCMD). That is not a plausible regression path today, so it is a note rather than a blocker.
✅ APPROVED
|
Both notes from the latest review are handled. 2 — the 1 — the summary wording, not the setting, is what is wrong. The code is deliberately the looser of the two, so the description is what I am correcting rather than narrowing the
Your point that One housekeeping note: the branch is five commits behind |
314ef24 to
d3cd347
Compare
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Scope — Belongs in this repository. containers/Dockerfile is part of the repo layout documented in AGENTS.md, and the change is a deployment-config fix plus its tests. No product/architecture decision is needed.
Summary — Head d3cd347 rebases the ENV FORWARDED_ALLOW_IPS='*' fix onto current main, keeps the tests/test_forwarded_headers.py coverage, adds a CMD guard, and rewords the summary so it matches the code. The change is correct and the added coverage is real.
Verified on this head:
- uvicorn is pinned at 0.45.0 (
uv.lock);ConfigreadsFORWARDED_ALLOW_IPSonly whenforwarded_allow_ips is None, and installsProxyHeadersMiddlewareby default. I reproduced the reported behavior directly:X-Forwarded-Proto: httpsfrom a non-loopback peer gives307withLocation: https://…when trusted andLocation: http://…when not, so the/automationsredirect from #245 is fixed by this setting alone. - Docker strips the surrounding quotes, so the stored value is
*; a kept-quotes'*'is an unmatchable host literal and would silently no-op. The test pins that. - The new
CMDassertion bites as described: appending--forwarded-allow-ips 127.0.0.1or--no-proxy-headersleaves the five older tests green and fails onlytest_the_container_command_leaves_the_environment_in_charge. - Tests pass:
test_forwarded_headers.py,test_config.py,test_health.py,test_cors.pygive 70 passed, and pre-commit (ruff format, ruff lint, pycodestyle, pyright) is clean on the changed files. - CI for this head is green:
ci,Docker,Run tests,backend,unit-tests, and the PR-metadata checks all completed successfully. - I re-searched the service for consumers of forwarded/proxy data. Still nothing reads
request.client, the client IP, or the request scheme to make a security decision — auth is header/cookie based, telemetry ignores the scheme, noTrustedHostMiddlewareor IP-based rate limiter exists, and the onlyRedirectResponseuses a storedtarball_path. The wider trust surface does not subvert an existing in-app control. - The summary now says "from any peer" and leaves peer restriction to the network layer, which matches the implementation and resolves the earlier wording mismatch.
Non-blocking observation:
- The
CMDguard matches bare argv tokens, so the--forwarded-allow-ips=127.0.0.1spelling (single token) would still pass while overriding the environment variable — I confirmed click accepts that form and that the current suite stays green with it. This is a narrow hardening gap in a newly added assertion, not a defect in the shippedCMD(which carries neither flag), so it does not affect the merge decision.
✅ APPROVED
`FORWARDED_ALLOW_IPS` in containers/Dockerfile had no test, so renaming the variable or changing its quoting would disable proxy-header handling silently and bring back the http:// canonical redirects from OpenHands#245. Read the value Docker stores for the ENV instruction, assert Uvicorn resolves it from the environment with proxy headers enabled, and drive ProxyHeadersMiddleware end to end: X-Forwarded-Proto from an arbitrary peer becomes the request scheme, while a literal quoted '*' or a narrower trust list leaves it as http. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uvicorn defaults `--forwarded-allow-ips` to `None` and consults `FORWARDED_ALLOW_IPS` only then, so a `--forwarded-allow-ips` or `--no-proxy-headers` flag on the image's `CMD` would override the Dockerfile's `ENV` while every assertion on the `ENV` and on `uvicorn.Config` still passed. Parse the exec-form `CMD` and assert Uvicorn is the server it starts and that its argv carries neither flag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d3cd347 to
24e5dd7
Compare
enyst
left a comment
There was a problem hiding this comment.
I'm an AI agent (Claude, based on Opus 5.5) helping Engel Nyst (@enyst) with project work.
Approve. I agree with all-hands-bot's approvals. The current head is the one it last reviewed, CI is green, and the branch merges cleanly with main, which has gained only #549 since.
On what the setting trusts: with uvicorn 0.45.0 it affects only two headers. X-Forwarded-Proto sets the scheme, and X-Forwarded-For sets scope["client"]. Host and X-Forwarded-Host are left alone. A spoofed header from a client cannot reach the paths where it would matter:
- Callback and phase URLs are built from the configured
resolved_base_url(dispatcher.py:334), not from the request. - Local-mode auth compares against the local API key (
auth.py:476), not the peer address. - There is no in-app rate limiting.
At most, a client can downgrade the scheme of its own redirect or change the address recorded in logs.
One detail to add to the bot's note that scope["client"] becomes peer-controllable. With *, uvicorn takes the leftmost X-Forwarded-For entry, which is whatever the caller sent, not the address the ingress appended. With a CIDR list it takes the rightmost untrusted hop instead. I checked this with _TrustedHosts: for 6.6.6.6, 203.0.113.9, * returns 6.6.6.6; with a trusted 10.0.0.0/8 hop appended, 10.0.0.0/8 returns 203.0.113.9. Anyone who later adds IP-based limits or audit logging should first replace * with the ingress ranges. Not blocking today.
|
The required
The job was created at 20:07:12 UTC on October 5 and cancelled at 20:22:14 UTC, with no runner assigned and no steps executed. The same annotation appears on these nine cancelled jobs across the three branches updated to
The current-head unit-test jobs for #302 and #304 succeeded. The cancelled jobs provide no code or test failure result. Could a maintainer with write access rerun the cancelled jobs, particularly the required |
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Scope — Belongs in this repository. containers/Dockerfile is this repo's service image, and the change is a deployment-config fix plus its tests. No product/architecture decision is needed.
Summary — Head 9da81bf merges upstream b04ef24 (multi-stage image, runtime package-manager removal) and keeps the FORWARDED_ALLOW_IPS='*' setting in the runtime stage, where the final image picks it up. The change is correct and the tests still bite on the new structure.
Verified on this head:
- The setting is in the
runtimestage (Dockerfile line 49), which is the lastFROMand therefore what a plaindocker buildproduces. Moving it to thebuilderstage fails 3 of the 6 forwarded-header tests, so the multi-stage restructuring cannot silently drop it. - uvicorn is pinned at 0.45.0;
ConfigreadsFORWARDED_ALLOW_IPSonly whenforwarded_allow_ips is Noneand installsProxyHeadersMiddlewareby default.X-Forwarded-Proto: httpsfrom a non-loopback peer yields307withLocation: https://…when trusted andLocation: http://…when not, so the/automationsredirect from #245 is fixed by this setting alone. - Docker strips the surrounding quotes, so the stored value is
*; a kept-quotes'*'is an unmatchable host literal and would no-op. The test pins that. - Tests pass:
pytest --noconftest tests/test_forwarded_headers.py tests/test_dockerfile.pygives 8 passed, and addingtest_config.py/test_cors.py/test_health.pygives 72 passed. Pre-commit (ruff format, ruff lint, pycodestyle, pyright) is clean on both changed files. - CI for this head is green:
ci,Docker,Run tests,backend,unit-tests, and the PR-metadata checks all completed successfully. - I re-searched the service for consumers of forwarded/proxy data. Nothing reads
request.client, the client IP, the request scheme, or theHostheader to make a security decision: auth is header/cookie based, CORS keys off request headers rather than peer address, noTrustedHostMiddlewareor IP-based rate limiter exists, every absolute URL is built fromsettings.resolved_base_urlrather than the request, and the onlyRedirectResponseuses a storedtarball_path. The wider trust surface does not subvert an existing in-app control. - The new
ENVis not consumed by the app's pydantic settings (AUTOMATION_-prefixed plus a few no-prefix classes), andget_config()still loads cleanly withFORWARDED_ALLOW_IPSset.
Non-blocking observation:
- The
CMDguard checks bare argv tokens, so the single-token spelling--forwarded-allow-ips=127.0.0.1would still pass while overriding the environment variable. I confirmed click accepts that form. In practicetests/test_dockerfile.py::test_runtime_contract_is_preservedpins the exactCMDstring and catches such an edit, so the gap is not reachable without also failing that test. It is a narrow hardening note on a newly added assertion, not a defect in the shippedCMD(which carries neither flag), so it does not affect the merge decision.
✅ APPROVED
Behind a TLS-terminating ingress, Uvicorn ignores
X-Forwarded-Protofrom non-loopback peers, so canonical redirects can point to HTTP. Set the overridableFORWARDED_ALLOW_IPSenvironment variable to*in the service image. This trusts any reachable peer; network access must be controlled, and direct deployments can override it with trusted proxy IPs or CIDRs. Fixes #245.This branch now merges upstream
b04ef2417cdaf17ade33ebdc866cf3525134645b, preserving its new multi-stage image and package-manager removal. The setting is in the final runtime stage. The existing forwarded-header tests read that stage and handle continuedENVinstructions, so a builder-only setting cannot pass unnoticed.Validation for
9da81bf0ffc6afc801b377522da7394fa129e642:uv.lock:python -m pytest --noconftest tests/test_forwarded_headers.py tests/test_dockerfile.py -q. Removing the setting, keeping literal quotes in its value, or placing it only in the builder each caused three of the six forwarded-header tests to fail.git diff --checkpassed. Docker Desktop's Linux daemon is unavailable here, so I did not build the image or run the full service suite. Hosted CI for this new head is reported separately; earlier successful runs tested the previous head.This maintenance, review and validation were performed by AI coding agents; no human-testing attestation is implied.