PST-095: Container hardening: gunicorn keeps its own trusted-proxy list, and services run without read-only root, dropped capabilities or memory limits #824
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#824
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the security audit of 2026-10-10 (
SECURITY-AUDIT-2026-10-10.md), run againstmainatc30851bfrom 8 to 10 October 2026, as PST-095. Severity: Informational.The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.
docker/Dockerfile(363-375);docker/compose.yml(22-36)Who can do it, and what they need. A peer that gunicorn's
forwarded_allow_ips(default 127.0.0.1/::1) trusts but that the operator has removed from POSTULO_TRUSTED_PROXIES, or the reverse. No path for a remote stranger under either default.How it works.
wsgi.url_scheme=httpsfromX-Forwarded-Proto: https,X-Forwarded-Protocol: sslorX-Forwarded-Ssl: onwhen the peer is in its ownforwarded_allow_ips, before Django runs.X-Forwarded-Proto/For/Host/PortandForwardedfrom untrusted peers;X-Forwarded-ProtocolandX-Forwarded-Sslare not on its list, and stripping cannot undo what gunicorn already decided.request.schemefalls back towsgi.url_schemewhen SECURE_PROXY_SSL_HEADER's header is absent, so a loopback peer can mark a request secure even if the operator narrowed POSTULO_TRUSTED_PROXIES to exclude loopback.Impact. The two trust lists agree under both defaults (loopback), so there is no attack today; the gap only opens if an operator narrows one and not the other. Recorded so the two settings are documented together.
Verification (verdict: confirmed; severity after verification: Informational). Keep it informational. Cited location: docker/Dockerfile:369-375 (CMD, no --forwarded-allow-ips), src/postulo/core/proxy.py:51-57, and prod.py:56-63 where the comment says 'Changing one of those two without the other undoes both' but names only two lists. Corrections to the write-up: (1) the middleware's header list is not the gap, because gunicorn decides the scheme before Django runs; (2) the realistic divergence is an operator-set FORWARDED_ALLOW_IPS=*, not a narrowed POSTULO_TRUSTED_PROXIES, and it lets a direct peer make is_secure() True, which gains the stranger nothing beyond skipping the SSL redirect for their own request. …
Suggested remediation. Pass
--forwarded-allow-ipsderived from POSTULO_TRUSTED_PROXIES in the entrypoint (or set it to an empty list and rely solely on Postulo's middleware), and addHTTP_X_FORWARDED_PROTOCOL/HTTP_X_FORWARDED_SSLto FORWARDING_HEADERS for completeness.Also covered by this issue.
docker/compose.yml(22-36); Informational). Self-inflicted or post-compromise denial of service and a larger blast radius for any in-container compromise. DoS is out of scope per the threat model; recorded as a hardening gap because the Dockerfile anticipates the …Fixed in
240e347deon main.