my prefect server setup prefect-metrics.waow.tech
python orchestration
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: capture read-only public UI architecture notes

documents the plan for moving auth from Prefect's built-in BasicAuth to
Traefik IngressRoute (priority-based routing: public GETs + safe POSTs
unauthenticated, write routes behind Traefik BasicAuth middleware).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

zzstoatzz e19e75f5 9bca8005

+102
+102
notes/read-only-ui.md
··· 1 + # read-only public UI: thinking it through 2 + 3 + ## what we want 4 + 5 + anyone can open `prefect-server.waow.tech` and browse the UI, see flow runs, logs, deployments. they cannot trigger runs, delete anything, or write. admin users still have full access by supplying credentials. 6 + 7 + ## where the first plan broke down 8 + 9 + the initial plan was: keep Prefect's own BasicAuth, use Traefik IngressRoute to inject admin credentials on the server's behalf for read-only routes, and require user-supplied credentials for write routes. 10 + 11 + **this doesn't work.** here's why: 12 + 13 + the Prefect UI is a browser SPA. one of its first calls is `GET /api/ui-settings`, which returns: 14 + 15 + ```json 16 + {"api_url": "...", "csrf_enabled": false, "auth": "BASIC"} 17 + ``` 18 + 19 + when the UI sees `auth: "BASIC"`, it **shows a login prompt immediately**, regardless of what Traefik injects on the network level. the UI doesn't know Traefik is handling auth on its behalf — from the browser's perspective, the server says "I require BasicAuth" and shows the prompt. credential injection at the proxy layer is invisible to the SPA. 20 + 21 + so even though all the data calls would succeed (because Traefik injects creds), the UI would still block public users with a login dialog. dead end. 22 + 23 + ## the correct architecture 24 + 25 + to get a truly public UI, we have to **disable Prefect's built-in BasicAuth** and move all auth to Traefik: 26 + 27 + ``` 28 + ┌──────────────────────────────────────────────────────┐ 29 + internet ──→ Traefik IngressRoute │ 30 + │ rule: GETs + safe read POSTs → no middleware │ 31 + │ rule: everything else → BasicAuth middleware │ 32 + └──────────────────┬───────────────────────────────────┘ 33 + 34 + Prefect server (no auth) 35 + ``` 36 + 37 + with `basicAuth.enabled: false` in prefect-values.yaml, Prefect's `/api/ui-settings` returns `"auth": null`. the UI skips the login prompt and renders normally for public users. 38 + 39 + admin users hit write endpoints → Traefik's BasicAuth middleware prompts for credentials → browser sends them → Traefik validates → passes request to Prefect. 40 + 41 + ## trade-off: loss of defense-in-depth 42 + 43 + Prefect's own auth was a backstop — even if Traefik was bypassed (e.g., from within the cluster), the server required credentials. without it, any pod in the cluster can call the Prefect API directly with no auth. 44 + 45 + in practice: the worker pod already calls Prefect directly as part of normal operation. the threat model for cluster-internal access is: a compromised worker or job pod could make write API calls. for this use case (personal infra, single-node k3s), this risk is acceptable. 46 + 47 + ## what Traefik actually needs 48 + 49 + **not standard Ingress**: we drop `ingress.enabled: true` and instead use: 50 + 51 + 1. **a cert-manager `Certificate` CR** (not the Ingress annotation) — tells cert-manager to request a cert for the domain and store it in a named Secret. IngressRoute references that Secret. this is the clean, explicit alternative to the "keep Ingress for cert-manager" hack. 52 + 53 + 2. **Traefik `Middleware` CRDs**: 54 + - `prefect-admin-auth` (BasicAuth type) — requires htpasswd-format credentials in a k8s Secret 55 + - note: Traefik BasicAuth middleware expects `user:bcrypt_hash` (not plaintext `user:pass`). justfile needs to hash the password before creating the secret. 56 + 57 + 3. **Two Traefik `IngressRoute` CRDs** (priority-based): 58 + - `prefect-public` (priority 20): GETs + explicit safe POST list → no middleware 59 + - `prefect-admin` (priority 10): catch-all → `prefect-admin-auth` middleware 60 + 61 + ## things to check before implementing 62 + 63 + - **k3s version on the server**: determines Traefik version (v2 = `traefik.containo.us/v1alpha1`, v3 = `traefik.io/v1alpha1`). run `kubectl version` or `k3s --version` to check. 64 + - **is CSRF enabled?** default is false, but if someone enabled it, POST filter endpoints need CSRF tokens from public users. check with `curl https://$DOMAIN/api/ui-settings | jq .csrf_enabled`. 65 + - **does the Prefect UI gracefully handle write-path 401s?** when a public user clicks "run deployment", Traefik returns 401. the browser shows a BasicAuth prompt (native browser dialog for 401 on XHR). this is... ok but not great UX. fine for now. 66 + - **htpasswd availability**: macOS has `openssl passwd` available for generating bcrypt hashes. or use `htpasswd` if apache2-utils is installed. 67 + 68 + ## files to create/modify 69 + 70 + | file | change | 71 + |------|--------| 72 + | `deploy/prefect-values.yaml` | `basicAuth.enabled: false`, `ingress.enabled: false` | 73 + | `deploy/prefect-certificate.yaml` | new — cert-manager Certificate CR | 74 + | `deploy/prefect-ingress-route.yaml` | new — Middleware + 2x IngressRoute | 75 + | `justfile` | add recipe to create htpasswd secret + apply new yamls | 76 + 77 + ## what the safe POST allowlist looks like 78 + 79 + explicit paths (no regex, no wildcards): 80 + - `/api/flows/filter|count|paginate` 81 + - `/api/flow_runs/filter|count|paginate|history|lateness` 82 + - `/api/task_runs/filter|count|paginate|history` 83 + - `/api/deployments/filter|count|paginate` 84 + - `/api/artifacts/filter|count`, `/api/artifacts/latest/filter|count` 85 + - `/api/work_pools/filter|count`, `/api/work_queues/filter` 86 + - `/api/logs/filter` 87 + - `/api/events/filter`, `/api/events/count-by/{day,event_type,resource_id}` 88 + - `/api/variables/filter|count` 89 + - `/api/automations/filter|count` 90 + - `/api/block_types/filter`, `/api/block_schemas/filter`, `/api/block_documents/filter` 91 + - `/api/concurrency_limits/filter`, `/api/v2/concurrency_limits/filter` 92 + 93 + this is ~35 explicit paths. verbose but provably correct — if a path isn't on this list, it hits the catch-all admin route. 94 + 95 + ## open question: UX for write-blocked public users 96 + 97 + when a logged-out user tries to run a deployment, they get a native browser BasicAuth dialog (401). this is... jarring. alternatives: 98 + 1. accept it — it's clear "this requires login" 99 + 2. add a custom error page in Traefik for 401s on write routes 100 + 3. modify the Prefect UI to handle 401 on write routes gracefully (fork territory) 101 + 102 + option 1 is fine for now.