diff --git a/deploy/README.md b/deploy/README.md index 904facb..5370e03 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -49,7 +49,9 @@ Then edit `.env`: - `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 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. ```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 (`frame-ancestors 'self'`, HSTS, nosniff, `Referrer-Policy: same-origin`). -`/api/health` is deliberately on its own higher-priority router with no -middleware: a monitoring probe must not need a credential, and the endpoint -carries no user data. +There is no auth middleware at the edge: Petal does its own (§4). `/api/health` +and `/api/version` sit outside Petal's own auth for the same reason they always +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 `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, -and what makes the interim edge gate below still necessary until this is -configured. +and what every deployment did before this landed. A host serving the public +must have them set. ### 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 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 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 = ''\"" ``` -### Interim edge gate (delete once the above is configured) +### The edge gate is gone -Until `AUTHENTIK_*` is filled in, Petal authenticates nobody — `StaticResolver` -hands every request the same `local` user. On a public host that means anyone who -finds the hostname can read and write documents and fill the disk with image -uploads, so Traefik holds the door with basic auth. +Until Phase 16 there was a Traefik basic-auth middleware in front of everything, +because Petal authenticated nobody and a public hostname was a public API. It +was removed when OIDC went live on 2026-07-27, together with the separate +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 -htpasswd -nbB petal 'your-password' # or any bcrypt htpasswd generator +```yaml +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`. - -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. +with `htpasswd -nbB petal 'your-password'` in `.env` as `PETAL_BASIC_AUTH`. --- diff --git a/docker-compose.yml b/docker-compose.yml index dfba15d..2972363 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,23 +79,14 @@ services: traefik.http.routers.petal.tls: "true" traefik.http.routers.petal.tls.certResolver: default 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" - # 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. 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