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.

feat: move Prefect auth to Traefik IngressRoute for public read-only UI

disables Prefect's built-in BasicAuth so /api/ui-settings returns
"auth": null — the UI renders without a login prompt for public users.

auth is now enforced by Traefik at the HTTP layer:
- GETs and explicit read POST paths (/api/*/filter, /count, etc.) → no middleware
- all other routes (write POSTs, PATCH, DELETE) → BasicAuth middleware

new files:
- deploy/prefect-certificate.yaml: cert-manager Certificate CR (replaces ingress annotation)
- deploy/prefect-ingress-route.yaml: Middleware + prefect-public (priority 20) + prefect-admin (priority 10)

justfile:
- replaces prefect-auth secret with htpasswd-format prefect-traefik-auth secret
- adds certificate + ingress route apply steps to deploy recipe
- adds public-ingress recipe for re-applying without full deploy

rollback: git reset --hard e19e75f

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

zzstoatzz 3561a7dd e19e75f5

+129 -14
+12
deploy/prefect-certificate.yaml
··· 1 + apiVersion: cert-manager.io/v1 2 + kind: Certificate 3 + metadata: 4 + name: prefect-server-tls 5 + namespace: prefect 6 + spec: 7 + secretName: prefect-server-tls 8 + issuerRef: 9 + name: letsencrypt-prod 10 + kind: ClusterIssuer 11 + dnsNames: 12 + - DOMAIN_PLACEHOLDER
+79
deploy/prefect-ingress-route.yaml
··· 1 + --- 2 + # BasicAuth middleware — credentials from prefect-traefik-auth secret (htpasswd format) 3 + apiVersion: traefik.io/v1alpha1 4 + kind: Middleware 5 + metadata: 6 + name: prefect-admin-auth 7 + namespace: prefect 8 + spec: 9 + basicAuth: 10 + secret: prefect-traefik-auth 11 + 12 + --- 13 + # Public IngressRoute — GETs and explicit safe read POSTs, no auth required (priority 20) 14 + apiVersion: traefik.io/v1alpha1 15 + kind: IngressRoute 16 + metadata: 17 + name: prefect-public 18 + namespace: prefect 19 + spec: 20 + entryPoints: 21 + - websecure 22 + routes: 23 + - match: Host(`DOMAIN_PLACEHOLDER`) && Method(`GET`) 24 + kind: Rule 25 + priority: 20 26 + services: 27 + - name: prefect-server 28 + port: 4200 29 + - match: >- 30 + Host(`DOMAIN_PLACEHOLDER`) && Method(`POST`) && ( 31 + Path(`/api/flows/filter`) || Path(`/api/flows/count`) || Path(`/api/flows/paginate`) || 32 + Path(`/api/flow_runs/filter`) || Path(`/api/flow_runs/count`) || Path(`/api/flow_runs/paginate`) || 33 + Path(`/api/flow_runs/history`) || Path(`/api/flow_runs/lateness`) || 34 + Path(`/api/task_runs/filter`) || Path(`/api/task_runs/count`) || Path(`/api/task_runs/paginate`) || 35 + Path(`/api/task_runs/history`) || 36 + Path(`/api/deployments/filter`) || Path(`/api/deployments/count`) || Path(`/api/deployments/paginate`) || 37 + Path(`/api/artifacts/filter`) || Path(`/api/artifacts/count`) || 38 + Path(`/api/artifacts/latest/filter`) || Path(`/api/artifacts/latest/count`) || 39 + Path(`/api/work_pools/filter`) || Path(`/api/work_pools/count`) || 40 + Path(`/api/work_queues/filter`) || 41 + Path(`/api/logs/filter`) || 42 + Path(`/api/events/filter`) || Path(`/api/events/count-by/day`) || 43 + Path(`/api/events/count-by/event_type`) || Path(`/api/events/count-by/resource_id`) || 44 + Path(`/api/variables/filter`) || Path(`/api/variables/count`) || 45 + Path(`/api/automations/filter`) || Path(`/api/automations/count`) || 46 + Path(`/api/block_types/filter`) || Path(`/api/block_schemas/filter`) || 47 + Path(`/api/block_documents/filter`) || 48 + Path(`/api/concurrency_limits/filter`) || 49 + Path(`/api/v2/concurrency_limits/filter`) 50 + ) 51 + kind: Rule 52 + priority: 20 53 + services: 54 + - name: prefect-server 55 + port: 4200 56 + tls: 57 + secretName: prefect-server-tls 58 + 59 + --- 60 + # Admin IngressRoute — catch-all, BasicAuth required (priority 10) 61 + apiVersion: traefik.io/v1alpha1 62 + kind: IngressRoute 63 + metadata: 64 + name: prefect-admin 65 + namespace: prefect 66 + spec: 67 + entryPoints: 68 + - websecure 69 + routes: 70 + - match: Host(`DOMAIN_PLACEHOLDER`) 71 + kind: Rule 72 + priority: 10 73 + middlewares: 74 + - name: prefect-admin-auth 75 + services: 76 + - name: prefect-server 77 + port: 4200 78 + tls: 79 + secretName: prefect-server-tls
+2 -11
deploy/prefect-values.yaml
··· 2 2 replicaCount: 1 3 3 4 4 basicAuth: 5 - enabled: true 6 - existingSecret: prefect-auth 5 + enabled: false # auth moves to Traefik IngressRoute 7 6 8 7 uiConfig: 9 8 prefectUiApiUrl: "https://DOMAIN_PLACEHOLDER/api" ··· 31 30 architecture: standalone 32 31 33 32 ingress: 34 - enabled: true 35 - className: traefik 36 - host: 37 - hostname: DOMAIN_PLACEHOLDER 38 - path: / 39 - pathType: Prefix 40 - annotations: 41 - cert-manager.io/cluster-issuer: letsencrypt-prod 42 - tls: true 33 + enabled: false # replaced by IngressRoute CRDs + Certificate CR in deploy/prefect-ingress-route.yaml
+36 -3
justfile
··· 86 86 sed "s|LETSENCRYPT_EMAIL_PLACEHOLDER|$LETSENCRYPT_EMAIL|g" deploy/cluster-issuer.yaml \ 87 87 | kubectl apply -f - 88 88 89 - echo "==> creating prefect auth secret" 90 - kubectl create secret generic prefect-auth \ 89 + echo "==> creating prefect traefik auth secret (htpasswd format)" 90 + USER=$(echo "$AUTH_STRING" | cut -d: -f1) 91 + PASS=$(echo "$AUTH_STRING" | cut -d: -f2-) 92 + HASH=$(openssl passwd -apr1 "$PASS") 93 + kubectl create secret generic prefect-traefik-auth \ 91 94 --namespace prefect \ 92 - --from-literal=auth-string="$AUTH_STRING" \ 95 + --from-literal=users="${USER}:${HASH}" \ 93 96 --dry-run=client -o yaml | kubectl apply -f - 94 97 95 98 echo "==> installing prefect server" ··· 99 102 --values - \ 100 103 --set postgresql.auth.password="$POSTGRES_PASSWORD" \ 101 104 --wait --timeout 5m 105 + 106 + echo "==> applying prefect certificate" 107 + sed "s|DOMAIN_PLACEHOLDER|$DOMAIN|g" deploy/prefect-certificate.yaml \ 108 + | kubectl apply -f - 109 + 110 + echo "==> applying prefect ingress routes" 111 + sed "s|DOMAIN_PLACEHOLDER|$DOMAIN|g" deploy/prefect-ingress-route.yaml \ 112 + | kubectl apply -f - 102 113 103 114 echo "==> installing monitoring stack" 104 115 sed "s|GRAFANA_DOMAIN_PLACEHOLDER|$GRAFANA_DOMAIN|g" deploy/monitoring-values.yaml \ ··· 139 150 # deploy the kubernetes worker to the cluster 140 151 worker: 141 152 kubectl apply -f deploy/worker.yaml 153 + 154 + # re-apply public ingress config without full deploy (certificate + ingress routes + auth secret) 155 + public-ingress: 156 + #!/usr/bin/env bash 157 + set -euo pipefail 158 + : "${DOMAIN:?set DOMAIN}" 159 + : "${AUTH_STRING:?set AUTH_STRING}" 160 + USER=$(echo "$AUTH_STRING" | cut -d: -f1) 161 + PASS=$(echo "$AUTH_STRING" | cut -d: -f2-) 162 + HASH=$(openssl passwd -apr1 "$PASS") 163 + kubectl create secret generic prefect-traefik-auth \ 164 + --namespace prefect \ 165 + --from-literal=users="${USER}:${HASH}" \ 166 + --dry-run=client -o yaml | kubectl apply -f - 167 + sed "s|DOMAIN_PLACEHOLDER|$DOMAIN|g" deploy/prefect-certificate.yaml \ 168 + | kubectl apply -f - 169 + sed "s|DOMAIN_PLACEHOLDER|$DOMAIN|g" deploy/prefect-ingress-route.yaml \ 170 + | kubectl apply -f - 171 + echo "done — verify:" 172 + echo " curl -sf https://$DOMAIN/api/health | jq ." 173 + echo " curl -sf -X POST https://$DOMAIN/api/flow_runs/filter -H 'Content-Type: application/json' -d '{}' | jq 'length'" 174 + echo " curl -o /dev/null -w '%{http_code}' -X POST https://$DOMAIN/api/flow_runs/ -H 'Content-Type: application/json' -d '{}' # expect 401" 142 175 143 176 # register flow deployments (run locally with PREFECT_API_URL + PREFECT_API_AUTH_STRING) 144 177 register-flows: