A security review of the whole repo. The queries were already scoped, the
OIDC flow already did state and nonce and PKCE, the session tokens were
already stored as hashes. What it found was mostly the seam between the
code and the deployment — and one place where the deployment quietly
undid the code.
The one that matters: with any AUTHENTIK_* variable missing, Petal fell
back to resolving every request to the single `local` user. That is right
on a laptop and a catastrophe on a public host, and Phase 16 removed the
Traefik basic-auth gate that used to stand behind the mistake. A typo in
the client secret would have served her journals to the open internet and
said so only in a log line nobody reads. It now refuses to start, guarded
by default for any BASE_URL that isn't loopback.
Then the one that would have been fixed and wasn't: stored images now
serve under `default-src 'none'; sandbox`, so an SVG pasted into a
document can't run as a page on Petal's own origin. Traefik's
customresponseheaders *overwrites*, so the CSP declared in the compose
labels would have silently replaced that per-route policy in production.
The whole header block moved into the binary, where a route can tighten
its own and a test can prove it; only HSTS stays at the edge, where TLS
actually terminates.
The rest, smaller:
- PETAL_ALLOWED_SUBS empty means everyone authentik authenticates, and
authentik here fronts half a dozen applications. Still legal, now
said out loud every boot, and set in both env examples.
- LLM failures relayed err.Error() to the browser, which carries the
address of the inference box on the far side of the VPN. Logged
instead; the client only ever rendered "the helper is resting".
- Exports scheme-check their links. Escaping makes a URL safe to sit
in an attribute and says nothing about following it, and an export
is the one artifact here meant to leave. Writing the test found the
markdown image src, which I'd missed reading it.
- The draft rescue is namespaced per account and cleared on sign-out.
Everything else in localStorage is a preference; this is her unsaved
writing, sitting in a profile two people share.
- /auth/logout is POST-only. With SameSite=Lax a GET route lets any
page on the internet sign her out mid-draft.
- Image uploads get a per-account allowance and the TTS cache a size
cap. Both share the encrypted volume the database is on, and a full
disk is SQLite failing to write, not a feature degrading.
- The session cookie takes the __Host- prefix over https, so nothing
else under parodia.dev can plant one. Old cookies still resolve;
nobody is signed out to get there.
- npm audit: linkify-it and postcss.
Verified: go build, go vet, the full Go suite, tsc, 195 frontend tests,
npm audit clean. The startup guard and both CSPs checked against a
running server rather than only asserted.
Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
100 lines
4.9 KiB
Bash
100 lines
4.9 KiB
Bash
# Petal — production environment for the parodia.dev VPS.
|
|
# Copy to the stack directory as `.env` (docker-compose.yml reads it via
|
|
# env_file) and fill in the model names. Values the image already fixes
|
|
# (PORT, DATABASE_PATH, IMAGE_DIR, TTS_CACHE_DIR, TTS endpoints) are set in
|
|
# docker-compose.yml, not here.
|
|
|
|
# --- Routing -----------------------------------------------------------------
|
|
# Must match the DNS A record and the Traefik Host() rule.
|
|
PETAL_HOST=petal.parodia.dev
|
|
# Absolute origin the app knows itself by. Phase 16's OIDC redirect URI is
|
|
# built from this, so it has to be the real public HTTPS origin.
|
|
BASE_URL=https://petal.parodia.dev
|
|
|
|
# The companion's bedtime nag and the night theme read the container clock.
|
|
TZ=Europe/Lisbon
|
|
|
|
# The container runs as this uid/gid so it can write the ./data bind mount.
|
|
# Set both to the output of `id -u` / `id -g` for the account owning the stack
|
|
# directory. Wrong values show up as "unable to open database file (14)".
|
|
PETAL_UID=1001
|
|
PETAL_GID=1001
|
|
|
|
# (The interim PETAL_BASIC_AUTH edge gate is gone: Petal authenticates for
|
|
# itself now, and the auth block at the bottom of this file is what holds the
|
|
# door. A second password in front of a real login is one more thing to lose.)
|
|
|
|
# --- LLM (millenia, over headscale) ------------------------------------------
|
|
# The only cross-VPN dependency. Petal degrades warmly when it's unreachable:
|
|
# spell check, gloss, garden, search, export and read-aloud all keep working and
|
|
# the status bar shows 小助手在休息 · Petal's helper is resting.
|
|
#
|
|
# 100.64.0.2 is millenia on the headscale network. vLLM must be bound to that
|
|
# interface (NOT 0.0.0.0 — this host is public); see deploy/README.md.
|
|
LLM_BACKEND=vllm
|
|
LLM_ENDPOINT=http://100.64.0.2:8000
|
|
LLM_MODEL=
|
|
LLM_CHAT_MODEL=
|
|
# 30s is the local-network default. Over WAN + VPN, with the voice and
|
|
# collocation passes sending a whole document, that truncates real work — the
|
|
# request is a hard deadline on Complete, and a timeout surfaces as the same
|
|
# warm 502 as an unreachable model. 90s leaves headroom without letting a
|
|
# genuinely wedged backend hang the pass forever.
|
|
LLM_TIMEOUT=90s
|
|
|
|
# --- Read-aloud (Piper sidecars) ---------------------------------------------
|
|
# Endpoints are wired in docker-compose.yml; these pick the voice each sidecar
|
|
# loads. Changing one means recreating that container so it downloads the model.
|
|
#
|
|
# A language is routable only when both halves are set — a TTS_ENDPOINT_XX with
|
|
# no TTS_VOICE_XX reads as "no voice for this language" and the browser's own
|
|
# synthesizer takes over, rather than as an instance that errors on every
|
|
# request. Adding es is a compose service plus a pair of lines here.
|
|
#
|
|
# pt_PT-tugão-medium is the only European Portuguese voice Piper ships; every
|
|
# other pt model in the catalogue is Brazilian. French has the opposite
|
|
# property — every fr voice in the catalogue is fr_FR — so there is no wrong
|
|
# country to land on and no non-ASCII name to trip the downloader.
|
|
TTS_VOICE_EN=en_US-amy-medium
|
|
TTS_VOICE_ZH=zh_CN-huayan-medium
|
|
TTS_VOICE_PT=pt_PT-tugão-medium
|
|
TTS_VOICE_FR=fr_FR-siwis-medium
|
|
TTS_AUDIO_FORMAT=mp3
|
|
TTS_TIMEOUT=15s
|
|
|
|
# --- Auth (Authentik OIDC) ---------------------------------------------------
|
|
# NOT OPTIONAL HERE. Authentik already runs on this host; set all three and
|
|
# Petal authenticates for itself.
|
|
#
|
|
# Leave any of them unset and Petal REFUSES TO START, because the alternative is
|
|
# worse: it would otherwise fall back to resolving every anonymous request to
|
|
# the single `local` user, handing the open internet full read and write over
|
|
# every document in the database. That fallback is right on a laptop and a
|
|
# catastrophe on this host, so the guard is on for any non-loopback BASE_URL.
|
|
# See PETAL_REQUIRE_AUTH below.
|
|
#
|
|
# AUTHENTIK_URL is the provider's issuer, and the redirect URI to register in
|
|
# Authentik is https://petal.parodia.dev/auth/callback.
|
|
AUTHENTIK_URL=https://auth.parodia.dev/application/o/petal/
|
|
AUTHENTIK_CLIENT_ID=petal
|
|
AUTHENTIK_CLIENT_SECRET=
|
|
|
|
# Who may sign in: comma-separated subject ids and/or emails.
|
|
#
|
|
# SET THIS. Empty means everyone Authentik authenticates, and Authentik on this
|
|
# host fronts half a dozen applications — being a valid user there is not the
|
|
# same as belonging in someone's private journal. An empty value is legal (a
|
|
# single-household instance may genuinely want it) and says so loudly in the
|
|
# startup log every boot.
|
|
#
|
|
# An email is knowable in advance; a subject id is an opaque uuid nobody can
|
|
# know before that person's first login. Use emails to invite, subject ids to
|
|
# pin.
|
|
PETAL_ALLOWED_SUBS=her@example.com,me@example.com
|
|
|
|
# The guard itself. Defaulted from BASE_URL — loopback origins run open, real
|
|
# ones demand a login — so it does not normally need setting. Set it to false
|
|
# only for a deployment genuinely reachable from nowhere but a trusted network,
|
|
# and understand that it means anyone who reaches Petal is the `local` user.
|
|
# PETAL_REQUIRE_AUTH=true
|