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:
+34
-19
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user