Utkarsh Jain
08/18/2025, 6:00 AMMarvin
08/18/2025, 6:00 AMMarvin
08/18/2025, 6:04 AM/api
.
Typical approaches
- Traefik + forwardAuth to an auth service that validates JWTs (e.g., oauth2‑proxy in “JWT bearer” mode, Authentik, a small custom verifier).
- Envoy (JWT AuthN filter) or Nginx/OpenResty (lua‑resty‑openidc) if you prefer those stacks.
- Network controls only (VPN/private network) if that’s acceptable. Not glamorous, but effective.
What to protect
- Protect the API paths: /api/*
(the UI makes calls there).
- If you expose the UI at the same domain, the UI will piggyback on your auth. If you split domains, be ready to handle CORS and websocket upgrade rules.
A concrete Traefik example (with oauth2‑proxy validating JWTs)
- oauth2‑proxy in “JWT bearer” mode validates Authorization: Bearer <token>
against your OIDC issuer (via JWKS). No browser redirect, suitable for workers/CLI.
docker‑compose (sketch):
services:
prefect:
image: prefecthq/prefect:3-latest
command: prefect server start --ui
environment:
- PREFECT_API_URL=<https://prefect.example.com/api>
- PREFECT_UI_URL=<https://prefect.example.com>
labels:
- traefik.enable=true
# UI router (no auth; the UI will call /api which is protected)
- traefik.http.routers.prefect-ui.rule=Host(`<http://prefect.example.com|prefect.example.com>`) && PathPrefix(`/`)
- traefik.http.routers.prefect-ui.entrypoints=websecure
- traefik.http.routers.prefect-ui.tls=true
- traefik.http.routers.prefect-ui.service=prefect
# API router (JWT required)
- traefik.http.routers.prefect-api.rule=Host(`<http://prefect.example.com|prefect.example.com>`) && PathPrefix(`/api`)
- traefik.http.routers.prefect-api.entrypoints=websecure
- traefik.http.routers.prefect-api.tls=true
- traefik.http.routers.prefect-api.service=prefect
- traefik.http.routers.prefect-api.middlewares=oauth2-auth@docker
# Service
- traefik.http.services.prefect.loadbalancer.server.port=4200
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7|quay.io/oauth2-proxy/oauth2-proxy:v7>
args:
- --provider=oidc
- --oidc-issuer-url=<https://YOUR_IDP/.well-known/openid-configuration>
- --client-id=unused-for-bearer
- --client-secret=unused-for-bearer
- --jwks-url=<https://YOUR_IDP/jwks>
- --skip-jwt-bearer-tokens=true
- --reverse-proxy=true
- --upstream=static:// # not used for /oauth2/auth
- --set-authorization-header=true
- --pass-access-token=true
environment:
- OAUTH2_PROXY_COOKIE_SECRET=please_set_me
labels:
# Define the forwardAuth middleware Traefik will call
- traefik.http.middlewares.oauth2-auth.forwardauth.address=<http://oauth2-proxy:4180/oauth2/auth>
- traefik.http.middlewares.oauth2-auth.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.oauth2-auth.forwardauth.authResponseHeaders=Authorization,X-Auth-Request-Email
traefik:
image: traefik:v2.11
command:
- --entrypoints.websecure.address=:443
- --providers.docker=true
- --api.dashboard=false
# TLS config omitted for brevity
ports:
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Client/worker auth
- Have your workers/CLI include the same JWT in the Authorization header. Easiest way in Prefect 3.x: set the API URL and reuse the PREFECT_API_KEY
env var to carry your JWT (the client will send it as Authorization: Bearer ...
).
export PREFECT_API_URL=<https://prefect.example.com/api>
export PREFECT_API_KEY=$YOUR_JWT_FROM_IDP
# run your worker / flows as usual
Caveat: there’s no automatic token refresh on the client—use long‑lived service tokens or handle rotation yourself.
Notes and gotchasMarvin
08/18/2025, 6:04 AMUtkarsh Jain
08/18/2025, 8:01 AMMarvin
08/18/2025, 8:01 AMMarvin
08/18/2025, 8:02 AM/
(static UI), Basic Auth for /api/*
.
Caveat you should expect: the UI calls the API. So while hitting /
won’t prompt, the browser will challenge for Basic Auth on the first API request. If you truly want “no prompts ever” for UI users, use an SSO/cookie approach (oauth2-proxy/forward auth) or keep the API private (VPN/internal) and only expose the UI.
Traefik example (two routers, Basic Auth only on /api):
services:
prefect:
image: prefecthq/prefect:3-latest
command: prefect server start --ui
environment:
- PREFECT_API_URL=<https://prefect.example.com/api>
- PREFECT_UI_URL=<https://prefect.example.com>
labels:
- traefik.enable=true
# UI (no auth)
- traefik.http.routers.prefect-ui.rule=Host(`<http://prefect.example.com|prefect.example.com>`) && PathPrefix(`/`)
- traefik.http.routers.prefect-ui.entrypoints=websecure
- traefik.http.routers.prefect-ui.tls=true
- traefik.http.routers.prefect-ui.service=prefect
# API (Basic Auth)
- traefik.http.routers.prefect-api.rule=Host(`<http://prefect.example.com|prefect.example.com>`) && PathPrefix(`/api`)
- traefik.http.routers.prefect-api.entrypoints=websecure
- traefik.http.routers.prefect-api.tls=true
- traefik.http.routers.prefect-api.service=prefect
- traefik.http.routers.prefect-api.middlewares=api-basic
- traefik.http.services.prefect.loadbalancer.server.port=4200
# Basic auth middleware (use a usersfile or hashed password)
- traefik.http.middlewares.api-basic.basicauth.users=user:$$apr1$$9k...$$hashedpass...
Workers/CLI using Basic Auth
- Point to the same URL and provide credentials. In Prefect 3.x you can set:
export PREFECT_API_URL=<https://prefect.example.com/api>
export PREFECT_API_AUTH_STRING=user:pass
# Do not set PREFECT_API_KEY at the same time
The client will send an Authorization header that satisfies the proxy’s Basic Auth.
Notes
- Websockets/log streaming: ensure your proxy preserves Upgrade/Connection headers. Traefik does by default.
- Split domains if you want different policies (e.g., public UI at ui.example.com; private API behind vpn/api.example.com), but the browser still talks to the API domain.
References
- Prefect security settings: Security settings (Prefect 3.x)
- Traefik Basic Auth: Traefik BasicAuth middleware
Quick question to tailor this: is a one-time Basic Auth prompt in the browser acceptable, or do you need the UI entirely frictionless? And are you on Traefik or Nginx? I can drop in the exact config either way.Utkarsh Jain
08/18/2025, 8:11 AMMarvin
08/18/2025, 8:11 AMMarvin
08/18/2025, 8:12 AMservices:
prefect:
image: prefecthq/prefect:3-latest
command: prefect server start --ui
environment:
# UI should talk to the SSO-protected API host
- PREFECT_API_URL=<https://prefect.example.com/api>
- PREFECT_UI_URL=<https://prefect.example.com>
labels:
- traefik.enable=true
# --- UI (SSO) ---
- traefik.http.routers.prefect-ui.rule=Host(`<http://prefect.example.com|prefect.example.com>`) && PathPrefix(`/`)
- traefik.http.routers.prefect-ui.entrypoints=websecure
- traefik.http.routers.prefect-ui.tls=true
- traefik.http.routers.prefect-ui.service=prefect
- traefik.http.routers.prefect-ui.middlewares=sso-auth
# --- API for browser (SSO so the UI works) ---
- traefik.http.routers.prefect-api-browser.rule=Host(`<http://prefect.example.com|prefect.example.com>`) && PathPrefix(`/api`)
- traefik.http.routers.prefect-api-browser.entrypoints=websecure
- traefik.http.routers.prefect-api-browser.tls=true
- traefik.http.routers.prefect-api-browser.service=prefect
- traefik.http.routers.prefect-api-browser.middlewares=sso-auth
# --- API for machines (Basic Auth) on a separate host ---
- traefik.http.routers.prefect-api-m2m.rule=Host(`<http://prefect-m2m.example.com|prefect-m2m.example.com>`) && PathPrefix(`/api`)
- traefik.http.routers.prefect-api-m2m.entrypoints=websecure
- traefik.http.routers.prefect-api-m2m.tls=true
- traefik.http.routers.prefect-api-m2m.service=prefect
- traefik.http.routers.prefect-api-m2m.middlewares=api-basic
# Service port
- traefik.http.services.prefect.loadbalancer.server.port=4200
# Middlewares
# ForwardAuth for SSO (e.g., oauth2-proxy)
- traefik.http.middlewares.sso-auth.forwardauth.address=<http://oauth2-proxy:4180/oauth2/auth>
- traefik.http.middlewares.sso-auth.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.sso-auth.forwardauth.authResponseHeaders=Authorization,X-Auth-Request-Email
# Basic Auth for m2m
- traefik.http.middlewares.api-basic.basicauth.users=user:$$apr1$$9k...$$hashedpass...
oauth2-proxy:
image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7|quay.io/oauth2-proxy/oauth2-proxy:v7>
# ... your SSO config ...
Clients
- UI/browser: uses https://prefect.example.com (SSO). No Basic Auth prompts.
- Workers/CLI (Basic Auth):
export PREFECT_API_URL=<https://prefect-m2m.example.com/api>
export PREFECT_API_AUTH_STRING=user:pass
# Do NOT set PREFECT_API_KEY concurrently
Prefer JWT for machines?
- Swap the m2m Basic Auth middleware for a JWT-validating forwardAuth (e.g., oauth2-proxy in bearer/JWKS mode). Then set:
export PREFECT_API_URL=<https://prefect-m2m.example.com/api>
export PREFECT_API_KEY=$YOUR_JWT
The client will send Authorization: Bearer … and Traefik+oauth2-proxy will validate it.
Notes
- Websockets/logs: ensure Traefik preserves Upgrade/Connection. ForwardAuth only on the SSO routes.
- Avoid cross-origin UI→API if possible; keeping both on prefect.example.com avoids CORS fun.
Refs
- Prefect security settings: Security settings (Prefect 3.x)
- Traefik BasicAuth: Traefik BasicAuth
- Traefik ForwardAuth: Traefik ForwardAuthMarvin
08/18/2025, 8:12 AMUtkarsh Jain
08/19/2025, 2:41 AMMarvin
08/19/2025, 2:41 AMMarvin
08/19/2025, 2:42 AM# External URLs the server/UI should advertise
PREFECT_UI_URL=<https://prefect.example.com>
PREFECT_API_URL=<https://prefect.example.com/api>
Notes:
- Do not set PREFECT_API_AUTH_STRING here. That’s for clients.
- If UI and API are truly on different origins, configure CORS per the docs (only needed if you split origins): https://docs-3.prefect.io/v3/advanced/security-settings
On workers/CLI (machine-to-machine via Basic Auth)
# Point to the Basic-Auth-protected API hostname
PREFECT_API_URL=<https://prefect-m2m.example.com/api>
# Basic auth credentials to satisfy Traefik’s middleware
PREFECT_API_AUTH_STRING=username:password
# Ensure this is NOT set at the same time
unset PREFECT_API_KEY
On user browsers
- Nothing to set. They hit https://prefect.example.com, SSO middleware handles auth, and the UI calls /api on the same host (also behind SSO), so no Basic Auth prompts.
If you currently have Basic Auth on /api at prefect.example.com, the UI will prompt. To keep SSO-only for users, either:
- Move Basic Auth to a second API hostname as above, or
- Protect both /
and /api
on the UI host with the SSO middleware instead of Basic Auth.
References
- Security settings (Prefect 3.x): Prefect security settings
If you share your exact hostnames, I can sanity-check the Traefik rules so the UI keeps using SSO while machines hit Basic Auth.Utkarsh Jain
08/19/2025, 9:25 AMMarvin
08/19/2025, 9:25 AMMarvin
08/19/2025, 9:27 AMPREFECT_API_URL=<https://prefect-m2m.example.com/api>
- PREFECT_API_AUTH_STRING=username:password
- If you’re using a Bearer token instead:
- PREFECT_API_URL=<https://prefect.example.com/api>
- PREFECT_API_KEY=your-token
- Disable prompts
- Either set PREFECT_CLI_PROMPT=false
- Or just pass --no-prompt
to the CLI
Then run:
prefect --no-prompt deploy --name '*'
Notes
- Run it from the directory containing prefect.yaml
.
- --name
accepts patterns; '*'
matches all deployments defined in prefect.yaml. You can scope if needed, e.g. 'my-flow/*'
or '*/staging-*'
.
- Idempotent: rerunning updates existing deployments.
- If you use multiple profiles, add -p your-profile
.
Example GitHub Actions step:
- name: Deploy all Prefect deployments
env:
PREFECT_API_URL: <https://prefect-m2m.example.com/api>
PREFECT_API_AUTH_STRING: ${{ secrets.PREFECT_BASIC_AUTH }}
PREFECT_CLI_PROMPT: "false"
run: |
prefect --no-prompt deploy --name '*'
That’s all the ceremony required. Minimal drama, maximal compliance with your CI’s aversion to human interaction.Utkarsh Jain
08/20/2025, 6:43 AMMarvin
08/20/2025, 6:43 AMMarvin
08/20/2025, 6:45 AM# prefect.toml
PREFECT_API_URL = "<https://prefect-m2m.example.com/api>"
# choose one auth method:
PREFECT_API_AUTH_STRING = "user:pass" # for Basic Auth proxy
# PREFECT_API_KEY = "xxxxx" # or Bearer token, not both
PREFECT_CLI_PROMPT = false # no prompts in CI
3) In CI, ensure the job runs from that directory:
- name: Deploy
working-directory: ./path/to/project
run: prefect --no-prompt deploy --name '*'
4) Make sure nothing overrides it:
- Unset any conflicting environment variables in the CI step if you want the file to win:
unset PREFECT_API_URL PREFECT_API_KEY PREFECT_API_AUTH_STRING
- If you’re using profiles, confirm you’re not switching to one that overrides your values (via --profile
or PREFECT_PROFILE
).
5) Alternative location
- If you prefer pyproject.toml, put settings under:
[tool.prefect]
PREFECT_API_URL = "<https://prefect-m2m.example.com/api>"
PREFECT_API_AUTH_STRING = "user:pass"
PREFECT_CLI_PROMPT = false
Quick sanity checks
- Print the effective value inside your CI step:
python - <<'PY'
from prefect.settings import get_current_settings
s = get_current_settings()
print("API_URL:", s.PREFECT_API_URL)
print("Auth string set:", bool(s.PREFECT_API_AUTH_STRING))
print("API key set:", bool(s.PREFECT_API_KEY))
print("Prompt:", s.PREFECT_CLI_PROMPT)
PY
If those don’t match your file, you’re in the wrong directory or something is overriding them.
Docs
- Settings and profiles: Settings and profiles
If you paste your prefect.toml and where you’re running the command from, I’ll point out the exact mismatch. I promise not to judge. Much.Utkarsh Jain
08/20/2025, 6:52 AMtest-{deployment name}-{date}-{time}
Marvin
08/20/2025, 6:53 AMMarvin
08/20/2025, 6:55 AMfrom prefect import flow
@flow(
name="test",
flow_run_name="test-{deployment_name}-{now:%Y%m%d}-{now:%H%M%S}",
)
def my_flow():
...
Notes:
- {deployment_name} resolves when the run comes from a deployment; for ad‑hoc runs it may be empty, so you can use {flow_name} instead if you prefer.
- {now:%Y%m%d} and {now:%H%M%S} use strftime formatting.
- Other common tokens: {flow_name}, {parameters}, {flow_run_id}.
Per‑deployment override (if your CLI supports it)
- Some Prefect 3.x versions let you set a run name template per deployment via CLI and prefect.yaml.
- Check with:
prefect deploy --help
If you see a flag like --flow-run-name
, you can use:
prefect --no-prompt deploy --name 'my-deployment' --flow-run-name 'test-{deployment_name}-{now:%Y%m%d-%H%M%S}'
Or in prefect.yaml under the deployment:
deployments:
- name: my-deployment
entrypoint: flows/my.py:my_flow
flow_run_name: "test-{deployment_name}-{now:%Y%m%d-%H%M%S}"
work_pool:
name: my-pool
If that option isn’t available in your version, stick with the @flow(flow_run_name=...) approach—it applies uniformly to runs created by the deployment.
Docs
- Settings and templating basics: Settings & profiles
- (Sanity check your CLI options with `prefect deploy --help`; Prefect 3.x removed the old 2.x deployment build flow.)Utkarsh Jain
08/20/2025, 6:59 AMflow_run_name: "test-mm-japan-{now:%Y%m%d}-{now:%H%M%S}"
to my deployment but yet the flow run comes up with a random name. I am using prefect v3Marvin
08/20/2025, 6:59 AMMarvin
08/20/2025, 7:01 AMflow_run_name
set in the deployment (prefect.yaml) is not applied. That was a 2.x pattern. Use the flow decorator instead; runs created by the deployment will adopt the flow’s template when they start.
Do this:
from prefect import flow
@flow(flow_run_name="test-mm-japan-{now:%Y%m%d}-{now:%H%M%S}")
def my_flow(...):
...
# deploy as usual; no run-name config needed in prefect.yaml
Notes:
- The UI initially shows a placeholder name when the run is scheduled/queued. It’s updated to your template when the flow actually starts on a worker. Refresh once it’s Running.
- If you need per-deployment variation, make it a parameter and include it in the template:
@flow(flow_run_name="test-{region}-{now:%Y%m%d}-{now:%H%M%S}")
def my_flow(region: str = "mm-japan"):
...
If you really must set names outside code, you’d need to call the API to update the run at start-up, which is more work and not worth it unless you enjoy pain.