<@ULVA73B9P> is there a way to add jwt authenticat...
# ask-marvin
u
@Marvin is there a way to add jwt authentication for prefect server api (self hosted) with traefik or any other way
m
thought for 260.3 seconds
Short answer: yes—put Prefect Server behind an auth‑enforcing reverse proxy. Prefect Server (self‑hosted, 3.x) does not ship with built‑in JWT/OIDC auth. The common pattern is to terminate at a proxy that validates the Authorization header and only forwards authenticated requests to Prefect’s
/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):
Copy code
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 ...
).
Copy code
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 gotchas
- Websockets/Upgrades: ensure your proxy passes Upgrade/Connection headers. ForwardAuth generally runs on the initial HTTP request—verify that log streaming and the UI work under your setup. - Keep the UI and API on the same domain if you can; it avoids CORS pain. If you split them, you’ll need to allow the UI origin when proxying to the API. - If you only need something very simple, you can also put basic auth at the proxy layer and set matching credentials in your clients. JWT/OIDC is nicer but more involved. Docs and references - Prefect security notes and settings: Security settings (Prefect 3.x) - Traefik forwardAuth: Traefik forwardAuth - oauth2‑proxy (JWT bearer mode): oauth2‑proxy docs If you share: - Your IdP (Auth0, Okta, Azure AD, Keycloak, …) - Whether you want browser SSO for the UI, machine‑to‑machine for workers, or both - Your current ingress/proxy stack …I can tailor the config (and test a concrete snippet) so you don’t have to. I live to serve. Well, “live” is a strong word.
u
@Marvin is it possible to create basic auth for api but no basic auth for server ui?
m
thought for 47.3 seconds
Yes—with a reverse proxy you can put Basic Auth on the API only and leave the UI path open. The usual pattern is two routes: no auth for
/
(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):
Copy code
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:
Copy code
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.
u
@Marvin I am using traefik and using sso for UI, so no need for basic auth there
m
thought for 57.7 seconds
Yes—and you’ll want to do it in a way that doesn’t break the UI’s SSO. Key point: the UI makes calls to /api. If you slap Basic Auth on /api for the same host, the browser will prompt and your “SSO-only UI” stops being SSO-only. The clean pattern is: - Keep UI + its API on one host, protected by your SSO middleware. - Expose a second API host for machine-to-machine access, protected by Basic Auth (or JWT). Traefik labels example (one Prefect service, two API hosts)
Copy code
services:
  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):
Copy code
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:
Copy code
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 ForwardAuth
- oauth2-proxy: oauth2-proxy docs Do you want the m2m side to be Basic Auth or JWT? And which IdP are you using for SSO? I can drop the exact oauth2-proxy flags if you like.
u
@Marvin I have setup to use sso for self hosted prefect server and a basic auth traefik middleware in front of /api, endpoint, what all environement variables are needed so that server and client are able to reach api and user can reach the server with sso (not basic auth)
m
thought for 44.0 seconds
Short version: use SSO on the UI host (including its /api) and point machines to a separate Basic‑Auth API host. Env vars won’t stop the browser from hitting Basic Auth if it’s on the same host’s /api. Assuming: - UI + SSO at https://prefect.example.com - API for machines with Basic Auth at https://prefect-m2m.example.com/api Set these: On the Prefect server container (so links and the UI know where they live)
Copy code
# 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)
Copy code
# 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.
u
@Marvin how to deploy all deployments in prefect.yaml fro ci (hence no prompt or user input)
m
thought for 93.4 seconds
Yes. In CI you can deploy everything defined in your project’s prefect.yaml without prompts. Do this from the project root: - Make sure the CI job has API access configured - If you’re using your Basic-Auth M2M host: -
PREFECT_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:
Copy code
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:
Copy code
- 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.
u
@Marvin I have a prefect.toml in place but the prefect deploy command is not picking that
m
thought for 93.7 seconds
Likely causes: - Wrong place: prefect.toml is only read from the current working directory. Run prefect deploy from the directory containing prefect.toml (your project root). - Wrong shape: keys must be top-level setting names (uppercase), not nested under a section. - Overridden by env/profile: environment variables or an active profile can override values from prefect.toml. CI often sets PREFECT_API_URL/KEY and wins. Checklist to make it work 1) Put prefect.toml at your project root (same place you run deploy) 2) Use top-level keys. Example:
Copy code
# 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:
Copy code
- 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:
Copy code
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:
Copy code
[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:
Copy code
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.
u
@Marvin is there a way to set names for runs in deployment so that the run created from deployment has a structured name like
test-{deployment name}-{date}-{time}
m
thought for 122.0 seconds
Yes. The cleanest, version‑proof way is to template the run name in your flow code; deployments will inherit it. Example (Prefect 3.x):
Copy code
from 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:
Copy code
prefect deploy --help
If you see a flag like
--flow-run-name
, you can use:
Copy code
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:
Copy code
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 &amp; profiles - (Sanity check your CLI options with `prefect deploy --help`; Prefect 3.x removed the old 2.x deployment build flow.)
u
@Marvin I have added
flow_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 v3
m
thought for 60.8 seconds
Short answer: in Prefect 3, a
flow_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:
Copy code
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:
Copy code
@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.