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>
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>
With no SECRET_KEY set, Grimoire fell back to a fixed placeholder. The
quickstart compose substituted change-me-in-production, so the README's
own update step (pull, up -d) recreated the container on that placeholder,
and every credential sealed under the key the user started with stopped
opening.
- config: with neither SECRET_KEY nor SECRET_KEY_FILE, the first start
writes a random key to DATA_DIR/secret_key (mode 0600, published with a
hard link so concurrent starts agree), and later starts read it back.
An explicit key wins and leaves the file alone. A missing
SECRET_KEY_FILE or an empty key file stops the start instead of
switching keys. With no data directory (a run outside the container)
the process gets a temporary key and says so.
- app: warns on a public placeholder key, on a temporary key, and when
/data is not a mounted volume.
- Dockerfile creates /data.
- docker-compose.yml is now the deploy compose (published image, both
volumes, nothing to set). The build-from-source compose is
docker-compose.dev.yml, and docker-compose.quickstart.yml is gone. The
Swarm stack file gains the /data volume and keeps its explicit key,
because a Swarm can move the app away from a node-local volume.
- README: a Deploy section with the compose inline, SECRET_KEY optional,
"keep both volumes", and updates that keep the key. A test holds the
README's copy equal to the file.
- Installs that ran on the change-me-in-production placeholder get a key
of their own on the new compose; Settings then asks once for saved
forge tokens and the backup secret.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
theme.css loaded Fraunces, Inter and JetBrains Mono from Google Fonts. That sent
every visitor's address to Google on every page load, and an instance with no
internet drew in fallback type. All three families are now vendored under
frontend/src/assets/fonts with every subset Google publishes for them, plus their
OFL licences. The Content-Security-Policy no longer lists fonts.googleapis.com or
fonts.gstatic.com.
A new guard test fails if any stylesheet, page or component links a remote asset
or preconnects to one, or if the CSP names another host.
The theme.css header now describes the sheet as it is instead of how it used to
read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 2 of the Scribe -> Grimoire rename.
- src/scribe -> src/grimoire, and every import, patch target, module path,
hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
each overridable from .env, so an install created as Scribe keeps its data
by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
Archives named scribe-* with the scribe-backup-tar manifest still restore
and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
carry the new name.
Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.
Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>