Tier 1 of "secure behind a reverse proxy". STRICTLY opt-in so direct/LAN installs are byte-for-byte unchanged. - core/security/reverse_proxy.py: apply_reverse_proxy_mode(app, config_get) — a no-op unless security.trust_reverse_proxy=true. When OFF (default), the app is untouched: no ProxyFix, X-Forwarded-* stays UNtrusted (a direct client can't spoof its IP/scheme), session cookie keeps Flask defaults. When ON (operator is behind nginx/Caddy/Traefik with TLS): trust one proxy hop's X-Forwarded-*, and mark the session cookie Secure + SameSite=Lax. Any config error → safe no-op, never breaks startup. - Wired once at app init. - Support/REVERSE-PROXY.md: nginx (with the Socket.IO Upgrade headers people always miss) / Caddy / Traefik configs, the setting, and the "put auth in front (Authelia/Authentik/oauth2-proxy)" recommendation + the off-for-plain-HTTP note. Tests: off (and missing-key, and a config exception) is a strict no-op — not ProxyFix-wrapped, cookie defaults intact; on wraps ProxyFix + secures the cookie; and the real web_server app is NOT in proxy mode by default. 5 tests pass.
44 lines
1.8 KiB
Python
44 lines
1.8 KiB
Python
"""Opt-in reverse-proxy mode.
|
|
|
|
Default OFF. When off this is a strict no-op: the Flask app is left exactly as it
|
|
was, ``X-Forwarded-*`` headers are NOT trusted (so a direct client can't spoof its
|
|
IP/scheme), and the session cookie keeps Flask's defaults. So a normal direct /
|
|
LAN install is byte-for-byte unchanged.
|
|
|
|
Only when the operator explicitly sets ``security.trust_reverse_proxy: true`` —
|
|
they're running behind nginx / Caddy / Traefik that terminates TLS — do we:
|
|
- trust the proxy's ``X-Forwarded-For/Proto/Host/Port`` (correct client IP,
|
|
HTTPS detection, redirects), and
|
|
- mark the session cookie ``Secure`` (HTTPS-only) + ``SameSite=Lax``.
|
|
|
|
Gated this way the security/UX change is scoped strictly to people who turned it
|
|
on; everyone else is untouched.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
CONFIG_KEY = "security.trust_reverse_proxy"
|
|
|
|
|
|
def apply_reverse_proxy_mode(app, config_get) -> bool:
|
|
"""Apply reverse-proxy hardening to ``app`` iff the operator enabled it.
|
|
|
|
``config_get`` is a ``config_manager.get``-style callable ``(key, default)``.
|
|
Returns True if proxy mode was enabled, False (no-op) otherwise. Never raises
|
|
out — a failure to enable falls back to the safe no-op behaviour.
|
|
"""
|
|
try:
|
|
if not config_get(CONFIG_KEY, False):
|
|
return False
|
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
# Trust exactly one proxy hop for each forwarded header.
|
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
|
app.config["SESSION_COOKIE_SECURE"] = True
|
|
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
|
return True
|
|
except Exception:
|
|
# If anything goes wrong, behave like off — never break startup over this.
|
|
return False
|
|
|
|
|
|
__all__ = ["apply_reverse_proxy_mode", "CONFIG_KEY"]
|