Deploy: drop the interim basic-auth gate now that Petal signs people in

The gate existed because Petal authenticated nobody and a public hostname
was therefore a public, writable API. It no longer is: every /api route
answers 401 without a session, so the only thing an anonymous visitor
reaches is the app shell and its redirect to Authentik. The separate
unauthenticated /api/health router goes with it — it only existed to escape
the middleware. A second password in front of a real login is one more
thing to lose.

Also records the two problems this deployment actually hit, since both fail
before the login page appears and neither is obvious from the error: the
issuer's trailing slash is significant, and a provider created through the
API rather than the admin UI comes up with an empty grant_types.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 07:33:01 -07:00
parent 6d71276513
commit e2f967c92b
2 changed files with 41 additions and 35 deletions
+34 -19
View File
@@ -49,7 +49,9 @@ Then edit `.env`:
- `PETAL_UID` / `PETAL_GID``id -u` / `id -g` for this account. `./data` is a - `PETAL_UID` / `PETAL_GID``id -u` / `id -g` for this account. `./data` is a
bind mount, so the image's own `petal` user has no claim on it; a mismatch bind mount, so the image's own `petal` user has no claim on it; a mismatch
shows up as `unable to open database file (14)` and a restart loop. shows up as `unable to open database file (14)` and a restart loop.
- `PETAL_BASIC_AUTH` — the interim edge gate, see §4. - `AUTHENTIK_URL` / `AUTHENTIK_CLIENT_ID` / `AUTHENTIK_CLIENT_SECRET` /
`PETAL_ALLOWED_SUBS` — sign-in, see §4. Without them Petal runs as the single
`local` user and must not be exposed.
- `LLM_MODEL` / `LLM_CHAT_MODEL` — see §3. - `LLM_MODEL` / `LLM_CHAT_MODEL` — see §3.
```bash ```bash
@@ -77,9 +79,10 @@ Labels follow the convention the other services on this box use: the external
`compression@file`. Petal adds its own response-header middleware `compression@file`. Petal adds its own response-header middleware
(`frame-ancestors 'self'`, HSTS, nosniff, `Referrer-Policy: same-origin`). (`frame-ancestors 'self'`, HSTS, nosniff, `Referrer-Policy: same-origin`).
`/api/health` is deliberately on its own higher-priority router with no There is no auth middleware at the edge: Petal does its own (§4). `/api/health`
middleware: a monitoring probe must not need a credential, and the endpoint and `/api/version` sit outside Petal's own auth for the same reason they always
carries no user data. did — a monitoring probe must not need a session, and neither carries user
data.
--- ---
@@ -135,8 +138,8 @@ unreachable for that to be safe.
Login turns on only when `AUTHENTIK_URL`, `AUTHENTIK_CLIENT_ID` and Login turns on only when `AUTHENTIK_URL`, `AUTHENTIK_CLIENT_ID` and
`AUTHENTIK_CLIENT_SECRET` are all set. With any of them missing Petal falls back `AUTHENTIK_CLIENT_SECRET` are all set. With any of them missing Petal falls back
to the single hardcoded `local` user — which is what local development wants, to the single hardcoded `local` user — which is what local development wants,
and what makes the interim edge gate below still necessary until this is and what every deployment did before this landed. A host serving the public
configured. must have them set.
### Register Petal in Authentik ### Register Petal in Authentik
@@ -185,6 +188,18 @@ docker compose logs petal | grep '^.*auth:' # issuer + redirect at
The startup log prints the redirect URI it will use; if Authentik rejects the The startup log prints the redirect URI it will use; if Authentik rejects the
login with a redirect-uri mismatch, compare that line against what's registered. login with a redirect-uri mismatch, compare that line against what's registered.
Two things bit this deployment, both worth checking first if a login dies early:
- **The issuer's trailing slash is significant.** Authentik's is
`…/application/o/petal/`, OIDC requires the discovered issuer to match the
configured one byte-for-byte, and normalising the slash away makes discovery
fail with `did not match the issuer URL returned by provider`.
- **A provider created through the API or `ak shell` has an empty
`grant_types`**, which authentik reads as "no grant type is permitted here"
and answers with `invalid_request` / *The request is otherwise malformed*
before the login page ever appears. The admin UI fills the list in for you;
scripted creation must set it (`authorization_code`, `refresh_token`).
Discovery is lazy and retried, so an Authentik outage blocks *new* logins but Discovery is lazy and retried, so an Authentik outage blocks *new* logins but
leaves existing sessions working — those only need Petal's own database. leaves existing sessions working — those only need Petal's own database.
@@ -203,24 +218,24 @@ docker compose exec petal sh -c \
"sqlite3 /data/petal.db \"DELETE FROM sessions WHERE user_id = '<sub>'\"" "sqlite3 /data/petal.db \"DELETE FROM sessions WHERE user_id = '<sub>'\""
``` ```
### Interim edge gate (delete once the above is configured) ### The edge gate is gone
Until `AUTHENTIK_*` is filled in, Petal authenticates nobody — `StaticResolver` Until Phase 16 there was a Traefik basic-auth middleware in front of everything,
hands every request the same `local` user. On a public host that means anyone who because Petal authenticated nobody and a public hostname was a public API. It
finds the hostname can read and write documents and fill the disk with image was removed when OIDC went live on 2026-07-27, together with the separate
uploads, so Traefik holds the door with basic auth. unauthenticated `/api/health` router that existed only to escape it: every `/api`
route now answers 401 without a session, and the only thing an anonymous visitor
gets is the app shell and a redirect to sign in.
Generate a credential: If you ever run this stack *without* `AUTHENTIK_*` configured — Petal then falls
back to the single `local` user — put the gate back before pointing DNS at it:
```bash ```yaml
htpasswd -nbB petal 'your-password' # or any bcrypt htpasswd generator traefik.http.routers.petal.middlewares: compression@file,petal-headers,petal-auth
traefik.http.middlewares.petal-auth.basicauth.users: ${PETAL_BASIC_AUTH:?}
``` ```
and put the resulting `user:hash` pair in `.env` as `PETAL_BASIC_AUTH`. with `htpasswd -nbB petal 'your-password'` in `.env` as `PETAL_BASIC_AUTH`.
Once OIDC is configured and a real login works, delete the `petal-auth`
middleware label, the `petal-health` router labels, and this subsection. Keeping
both is harmless but means two passwords to get to one editor.
--- ---
+7 -16
View File
@@ -79,23 +79,14 @@ services:
traefik.http.routers.petal.tls: "true" traefik.http.routers.petal.tls: "true"
traefik.http.routers.petal.tls.certResolver: default traefik.http.routers.petal.tls.certResolver: default
traefik.http.routers.petal.service: petal traefik.http.routers.petal.service: petal
traefik.http.routers.petal.middlewares: compression@file,petal-headers,petal-auth # No edge gate: Petal authenticates for itself now (Authentik OIDC), so
# every /api route answers 401 without a session and the only thing served
# to an anonymous visitor is the app shell and its sign-in redirect. The
# basic-auth middleware that stood here until Phase 16 — plus the separate
# unauthenticated router /api/health needed to escape it — is gone; a
# second password in front of a real login is just one more thing to lose.
traefik.http.routers.petal.middlewares: compression@file,petal-headers
traefik.http.services.petal.loadbalancer.server.port: "8080" traefik.http.services.petal.loadbalancer.server.port: "8080"
# INTERIM — delete this middleware and the petal-health router when Phase
# 16's OIDC login lands. Petal has no authentication of its own yet
# (StaticResolver hands every request the same local user), so without a
# gate at the edge anyone who finds the hostname can read and write
# documents and upload images. PETAL_BASIC_AUTH is a user:bcrypt-hash
# pair; see deploy/README.md for generating it.
traefik.http.middlewares.petal-auth.basicauth.users: ${PETAL_BASIC_AUTH:?set PETAL_BASIC_AUTH in .env}
# /api/health stays open on its own higher-priority router: a monitoring
# probe must not need a credential, and the endpoint carries no user data.
traefik.http.routers.petal-health.rule: Host(`${PETAL_HOST:-petal.parodia.dev}`) && Path(`/api/health`)
traefik.http.routers.petal-health.priority: "100"
traefik.http.routers.petal-health.entrypoints: web-secure
traefik.http.routers.petal-health.tls: "true"
traefik.http.routers.petal-health.tls.certResolver: default
traefik.http.routers.petal-health.service: petal
# Petal is a private writing space: no framing, no sniffing, HSTS on. # Petal is a private writing space: no framing, no sniffing, HSTS on.
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Content-Security-Policy: frame-ancestors 'self' traefik.http.middlewares.petal-headers.headers.customresponseheaders.Content-Security-Policy: frame-ancestors 'self'
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Strict-Transport-Security: max-age=31536000; includeSubDomains traefik.http.middlewares.petal-headers.headers.customresponseheaders.Strict-Transport-Security: max-age=31536000; includeSubDomains