feat(6838): read the client address and HTTPS from the trusted proxy hop
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Failing after 1m20s
CI & Build / integration (push) Successful in 2m27s
CI & Build / Build & push image (push) Skipped

grimoire/proxy.py is the one reader of X-Forwarded-For and X-Forwarded-Proto.
It takes the entry TRUSTED_PROXY_HOPS in from the right (default 1; a shorter
header reads its leftmost entry; 0 trusts no proxy), so an entry a client puts
left of the proxy's is never believed. Before this, with TRUST_PROXY_HEADERS
on, the leftmost X-Forwarded-For entry keyed the login, register, forgot and
reset rate limits and the audit log, and a client could choose it.

The session cookie is Secure, and HSTS (max-age=31536000) is sent, exactly
when the request is HTTPS by the trusted proxy's word. Plain HTTP is
untouched and nothing redirects. SECURE_COOKIES is gone (a startup warning says
so when it is still set); TRUST_PROXY_HEADERS=false still maps to 0 hops, with
a warning to move to TRUSTED_PROXY_HOPS. Admin audit lines now use the same
client address instead of the socket peer.

The placeholder-key refusal keyed on SECURE_COOKIES, so it now refuses either
placeholder key outright; the instance makes its own key when none is set.

Family idea #5004 (IsHTTPS, trusted-hop X-Forwarded-Proto), canon v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 19:32:20 -05:00
co-authored by Claude Opus 5.5
parent b87178d261
commit dbd6d5b8b5
12 changed files with 369 additions and 82 deletions
+13 -10
View File
@@ -6,7 +6,8 @@ from pathlib import Path
from quart import Quart, g, jsonify, make_response, request, send_from_directory
from grimoire.config import PLACEHOLDER_SECRET_KEYS, SECRET_KEY_FILENAME, Config
from grimoire.config import SECRET_KEY_FILENAME, Config
from grimoire.proxy import PerRequestSecureSession, apply_https_headers
from grimoire.routes.admin import admin_bp
from grimoire.routes.api import api
from grimoire.routes.auth import auth_bp
@@ -61,13 +62,13 @@ def create_app() -> Quart:
app.secret_key = Config.SECRET_KEY
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = Config.SECURE_COOKIES
# Secure is decided per request, from the trusted proxy's X-Forwarded-Proto.
app.session_interface = PerRequestSecureSession()
if Config.SECRET_KEY in PLACEHOLDER_SECRET_KEYS:
if os.environ.get("SECURE_COOKIES"):
logger.warning(
"SECRET_KEY is set to a public placeholder, so session cookies are insecure. "
"Remove it to let the instance keep its own key in %s, or set a real one.",
Config.DATA_DIR,
"SECURE_COOKIES is not used: the session cookie is Secure on every request "
"the trusted proxy reports as HTTPS. Remove it from the environment."
)
if Config.SECRET_KEY_SOURCE == "ephemeral":
logger.warning(
@@ -82,11 +83,12 @@ def create_app() -> Quart:
Config.DATA_DIR, SECRET_KEY_FILENAME,
)
if not Config.TRUST_PROXY_HEADERS:
if os.environ.get("TRUST_PROXY_HEADERS") and not os.environ.get("TRUSTED_PROXY_HOPS"):
logger.warning(
"TRUST_PROXY_HEADERS is not set. If this instance is behind a reverse proxy "
"(nginx, Caddy, Traefik) set TRUST_PROXY_HEADERS=true so rate limiting uses "
"real client IPs rather than the proxy IP."
"TRUST_PROXY_HEADERS is replaced by TRUSTED_PROXY_HOPS (now %d). Set "
"TRUSTED_PROXY_HOPS to the number of proxies in front of Grimoire and "
"remove TRUST_PROXY_HEADERS.",
Config.TRUSTED_PROXY_HOPS,
)
app.register_blueprint(admin_bp)
@@ -159,6 +161,7 @@ def create_app() -> Quart:
except Exception:
logger.debug("Failed to log usage", exc_info=True)
apply_https_headers(response)
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "DENY")
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")