feat(6866): the proxy hop count is an admin setting, and the server shows what each count reads
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m47s
CI & Build / integration (push) Successful in 2m1s
CI & Build / Build & push image (push) Successful in 20s

An admin can save how many reverse proxies stand in front of Grimoire.
A saved count wins over TRUSTED_PROXY_HOPS, which stays the starting
value. Each worker loads it at startup (under a deadline) and re-reads
it every 30s; the worker that saves applies it at once.

GET/PUT /api/admin/proxy returns the count, where it came from, and a
readout of the admin's own request: the socket peer, the forwarding
chains, what counts 0-3 would read, and the count that reads
Cloudflare's visitor when Cloudflare's headers are present. Saves are
audit-logged. A "too many attempts" reply now says where to look when
everyone is being blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 09:55:42 -05:00
co-authored by Claude Opus 5.5
parent 49b5ceee41
commit 753a6ccb31
10 changed files with 595 additions and 17 deletions
+11 -3
View File
@@ -85,9 +85,9 @@ def create_app() -> Quart:
if os.environ.get("TRUST_PROXY_HEADERS") and not os.environ.get("TRUSTED_PROXY_HOPS"):
logger.warning(
"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.",
"TRUST_PROXY_HEADERS is not used (the starting proxy hop count is %d). "
"Remove it; set the count in Settings → Config, or TRUSTED_PROXY_HOPS "
"to change the starting value.",
Config.TRUSTED_PROXY_HOPS,
)
@@ -202,6 +202,14 @@ def create_app() -> Quart:
start_notification_loop()
start_auth_token_retention_loop()
# The proxy hop count saved in Settings decides every request's client
# address, so it is loaded before serving, under a deadline (a failed
# or slow read leaves the environment's count in force), and re-read
# on an interval so a save in another worker reaches this one.
from grimoire.services import proxy_settings
await proxy_settings.load_at_startup()
proxy_settings.start_refresh_loop()
# Write down any shipped default that moved in this release.
#
# INLINE AND AWAITED, deliberately, against the instinct the block