<@ULVA73B9P> How to enable prefect authentication ...
# marvin-ai
m
@Marvin How to enable prefect authentication in self hosted prefect. • I am using docker compose that runs prefect server, services, db, workpool in separate containers • I want a production grade solution. • I often interact with prefect via API call as well, so how do I secure it?
m
thought for 289.5 seconds
Short answer: Prefect Server (self‑hosted) does not include built‑in authentication or API keys. In production, you secure it by running it behind an authenticated, TLS‑terminating reverse proxy (e.g., Nginx/Traefik/Envoy) and have the proxy enforce auth (OIDC via oauth2‑proxy, mTLS, Basic auth, API gateway, etc.). Prefect clients (CLI, workers, and your code) can attach an Authorization header via
PREFECT_API_AUTH_STRING
to satisfy your proxy. Recommended production pattern - Do not expose Prefect services directly. Put all Prefect containers (API, services, DB, workers) on a private network. Only the reverse proxy is internet‑facing. - Terminate TLS at the proxy (Let’s Encrypt/ACME or your corp certs). Prefer TLS 1.2+. - Enforce authentication/authorization at the proxy: - OIDC/SSO with oauth2‑proxy (Okta, Azure AD, Google, Auth0, etc.) for browser/UI and programmatic access - or mTLS for service‑to‑service/programmatic access - or Basic/JWT auth or an API gateway (Kong/Envoy/NGINX Plus) that validates tokens - Lock down the database (require strong password, optionally TLS), do not expose it. - For internal clients (workers, CLI, your apps), set: -
PREFECT_API_URL
to your external API base (e.g., `https://prefect.example.com/api`) -
PREFECT_API_AUTH_STRING
to the Authorization header your proxy expects (e.g.,
Bearer <jwt>
or
Basic <base64>
) - If you use a private CA, set
PREFECT_API_SSL_CERT_FILE
to the CA bundle path mounted in your containers. - For the UI behind a proxy, if you’re changing host/path, set the UI envs to point to your external URLs. Key settings you’ll use -
PREFECT_API_URL
-
PREFECT_API_AUTH_STRING
(Prefect will forward this verbatim as the HTTP Authorization header) -
PREFECT_API_SSL_CERT_FILE
(trust a private CA) -
PREFECT_UI_URL
and, if needed,
PREFECT_UI_API_URL
(so the browser UI calls the right API over your proxy) Example: secure reverse proxy with oauth2‑proxy (OIDC) - Reverse proxy enforces login and only forwards authenticated requests to Prefect’s API/UI. - Prefect clients/CLI/workers use a bearer token (or a service account flow) and set
PREFECT_API_AUTH_STRING="Bearer <token>"
. Minimal Nginx snippet (conceptual)
Copy code
server {
  listen 443 ssl http2;
  server_name <http://prefect.example.com|prefect.example.com>;

  ssl_certificate /etc/ssl/certs/fullchain.pem;
  ssl_certificate_key /etc/ssl/private/privkey.pem;

  # UI
  location / {
    # If using oauth2-proxy:
    auth_request /oauth2/auth;
    error_page 401 = /oauth2/sign_in;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_pass <http://prefect-server:4200/;>  # Prefect UI/API (adjust to your container/ports)
  }

  # API path if you want to split it
  location /api/ {
    auth_request /oauth2/auth;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_pass <http://prefect-server:4200/api/;>
  }

  # oauth2-proxy endpoints
  location /oauth2/ {
    proxy_pass <http://oauth2-proxy:4180/;>
  }
}
Docker Compose pointers - Keep Prefect API/services/DB on an internal network; expose only the proxy’s 443. - Configure client/worker env vars in Compose:
Copy code
environment:
  - PREFECT_API_URL=<https://prefect.example.com/api>
  - PREFECT_API_AUTH_STRING=Bearer ${PREFECT_SERVICE_TOKEN}
  - PREFECT_API_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt
- If the UI is being proxied on a different host/path, add to the Prefect server/UI container:
Copy code
environment:
  - PREFECT_UI_URL=<https://prefect.example.com>
  - PREFECT_UI_API_URL=<https://prefect.example.com/api>
CLI/SDK usage - Configure your local profile once:
Copy code
prefect config set PREFECT_API_URL=<https://prefect.example.com/api>
prefect config set PREFECT_API_AUTH_STRING="Bearer <your-service-token>"
- Or set env vars in your app: ``` import os os.environ["PREFECT_API_URL"] = "https://prefect.example.com/api" os.environ["PREFECT_API_AUTH_STRING"] = "Bearer <your-service-token>"
from prefect.client.orchestration import get_client async def demo(): async with get_client() as client: await client.read_flow_run("<flow-run-id>") ``` What about SSO? - SSO in Prefect Cloud is built in. For self‑hosted, put SSO in front of Prefect via oauth2‑proxy or your API gateway. The Prefect API will not validate tokens itself; your proxy must do it. - If you only need browser SSO, oauth2‑proxy with cookie sessions is common. For programmatic access, use JWT bearer tokens or mTLS at the proxy. Hardening checklist - Only the proxy is public; Prefect API/services/DB are private - TLS everywhere; pin or trust private CA via
PREFECT_API_SSL_CERT_FILE
- Rate limit/throttle at the proxy - Use strict-Allow/Origin policies if you split UI/API hosts - Rotate any service tokens; prefer short‑lived JWTs or mTLS for automation - Back up your PostgreSQL; enable TLS to Postgres if across networks References - Self-hosted via Docker Compose (official): Docker Compose guide - Settings reference (API/UI envs): Settings reference - Security overview and guidance: Security settings - Community reverse proxy example: Compose with reverse proxy example A couple of clarifying questions to tailor this for you: - Which identity provider do you use (Okta, Azure AD, Google, etc.) and do you prefer OIDC with oauth2‑proxy? - Will the API be publicly accessible, or only inside a VPN/private network? - Do you want browser SSO only, or also token-based auth for programmatic access? I can share a concrete Compose + oauth2‑proxy config once I know these details.
m
@Marvin I want basic API authenticataion for API access and oauth2 for UI access. Can u share an example setup
m
thought for 107.5 seconds
Great choice—splitting auth modes is a clean pattern: - UI: OIDC/OAuth2 via oauth2-proxy - API: HTTP Basic for programmatic access Below is a working example using Nginx + oauth2-proxy in front of your existing Prefect Server containers. It publishes two public entry points: - https://prefect.example.com → UI and browser calls to /api protected by OAuth2 (cookie session) - https://api.prefect.example.com → API protected by Basic auth (for CLI/workers/code) Important notes - The Prefect backend does not do auth itself. Nginx enforces OAuth2/Basic and only forwards allowed requests to the backend. - Keep Prefect services on a private Docker network and only expose Nginx. - The UI will call /api on the UI host. Programmatic clients will hit the API host. 1) Add oauth2-proxy and nginx to your Compose This snippet assumes you already run your Prefect server and services on the same Compose network and that the Prefect API is reachable at http://prefect-server:4200 (adjust if different).
Copy code
services:
  oauth2-proxy:
    image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.6.0|quay.io/oauth2-proxy/oauth2-proxy:v7.6.0>
    environment:
      # Replace with your IdP details
      - OAUTH2_PROXY_PROVIDER=oidc
      - OAUTH2_PROXY_CLIENT_ID=${OIDC_CLIENT_ID}
      - OAUTH2_PROXY_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
      - OAUTH2_PROXY_COOKIE_SECRET=${OAUTH2_COOKIE_SECRET}  # 32-byte base64
      - OAUTH2_PROXY_OIDC_ISSUER_URL=${OIDC_ISSUER_URL}     # e.g. <https://login.microsoftonline.com/<tenant>/v2.0>
      - OAUTH2_PROXY_REDIRECT_URL=<https://prefect.example.com/oauth2/callback>
      - OAUTH2_PROXY_WHITELIST_DOMAINS=<http://prefect.example.com|prefect.example.com>
      - OAUTH2_PROXY_EMAIL_DOMAINS=*
      - OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
      # Pass user info to Nginx if needed
      - OAUTH2_PROXY_SET_XAUTHREQUEST=true
    networks:
      - internal

  nginx:
    image: nginx:1.27
    depends_on:
      - oauth2-proxy
      # - prefect-server  # your existing service
    ports:
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./secrets/tls/fullchain.pem:/etc/ssl/certs/fullchain.pem:ro
      - ./secrets/tls/privkey.pem:/etc/ssl/private/privkey.pem:ro
      - ./secrets/htpasswd:/etc/nginx/.htpasswd:ro
    networks:
      - internal

networks:
  internal:
    driver: bridge
2) Nginx config (two vhosts: UI+OAuth2 and API+Basic) Put this at ./nginx/nginx.conf. ``` worker_processes 1; events { worker_connections 1024; } http { # Basic hardening, tune as needed sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; upstream prefect_backend { # Match your container name and port server prefect-server:4200; } # ---------- UI host (OAuth2 for browser + cookie session) ---------- server { listen 443 ssl http2; server_name prefect.example.com; ssl_certificate /etc/ssl/certs/fullchain.pem; ssl_certificate_key /etc/ssl/private/privkey.pem; # oauth2-proxy endpoints location /oauth2/ { proxy_pass http://oauth2-proxy:4180/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme https; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Host $host; } # Auth gate (subrequest to oauth2-proxy) location = /oauth2/auth { proxy_pass http://oauth2-proxy:4180/oauth2/auth; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Original-URI $request_uri; proxy_set_header Content-Length ""; proxy_pass_request_body off; } # UI and API for browser location / { auth_request /oauth2/auth; error_page 401 = /oauth2/sign_in; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Upgrade headers help with SSE/long-polling
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_pass http://prefect_backend/; } # If you prefer to split explicitly: # location /api/ { ...same as above... proxy_pass http://prefect_backend/api/; } } # ---------- API host (Basic auth for programmatic access) ---------- server { listen 443 ssl http2; server_name api.prefect.example.com; ssl_certificate /etc/ssl/certs/fullchain.pem; ssl_certificate_key /etc/ssl/private/privkey.pem; # HTTP Basic auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; # API path only location /api/ { proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_pass http://prefect_backend/api/; } } }
Copy code
3) Configure Prefect UI to point at your external URLs
Set these on your Prefect server container so the UI renders the correct endpoints:
PREFECT_UI_URL=https://prefect.example.com PREFECT_UI_API_URL=https://prefect.example.com/api
Copy code
4) Create Basic auth credentials
Create the htpasswd file used by the API host:

- With Docker (no local htpasswd needed):
docker run --rm httpd:2-alpine htpasswd -nbB myuser 'mypassword' > ./secrets/htpasswd
Copy code
- Or on your machine:
htpasswd -cB ./secrets/htpasswd myuser # you will be prompted for a password
Copy code
5) Programmatic clients (CLI/workers/app code)
Point them at the Basic-protected API host and include the Basic header via PREFECT_API_AUTH_STRING.

- One-time CLI profile:
prefect config set PREFECT_API_URL=https://api.prefect.example.com/api prefect config set PREFECT_API_AUTH_STRING="Basic $(printf '%s' 'myuser:mypassword' | base64)"
Copy code
- In code:
import os os.environ["PREFECT_API_URL"] = "https://api.prefect.example.com/api" os.environ["PREFECT_API_AUTH_STRING"] = "Basic <base64 user:password>" from prefect.client.orchestration import get_client async def main(): async with get_client() as client: flows = await client.read_flows() print(len(flows)) ``` Notes and tips - Keep Prefect backend and DB on the internal network; only expose Nginx:443. - If you use a private CA, distribute it and set
PREFECT_API_SSL_CERT_FILE
on workers/clients. - If you put API and UI on different hosts (as above), the UI will call its own host’s /api path (OAuth2 cookie), while programmatic access uses the API host (Basic). No CORS needed for the UI path. - Rotate Basic credentials regularly; consider mTLS or short-lived JWTs if you later need stronger controls for automation. References - Security settings: Security guidance - Settings reference (API/UI envs): Settings reference - Docker Compose self-hosting: Self-hosted via Docker Compose - oauth2-proxy: oauth2-proxy docs If you share your IdP (Okta/Azure AD/Google/etc.), I can fill in the exact oauth2-proxy environment variables for that provider and add any required scopes/claims.
m
@Marvin I built an app that has a DB and stores users creds. It providers endpoints to sign in with username and password and returns refresh and access tokens. Additionally, an admin in this app can set an API key that users can use to authenticate with instead of JWT tokens. How do I use this app as an authentication mechanism for prefect? I have full control over this app as well. I am open to use any additional layers nginx, .. or whatever
m
thought for 287.3 seconds
Great question — and you’ve got the right instincts. Here’s the lay of the land and a few patterns that work well. First, an important distinction - Prefect Cloud: Auth is handled by Prefect (API keys + optional SSO). You cannot plug in a custom identity provider for Cloud. See API keys and SSO. - Self-hosted Prefect Server: There is no built-in user auth/RBAC. You protect the API/UI by putting an auth layer (NGINX, oauth2-proxy, etc.) in front. Prefect clients (workers/flows/CLI) can be configured to send an Authorization header for that proxy via settings. See Security settings. Since you control your auth app, you have a few solid options for self-hosting: Option A — NGINX + your auth service (auth_request) - Idea: Put NGINX in front of Prefect and have NGINX call your app to validate tokens/API keys on every request. - Works for: - Browser/UI users: They hit NGINX, which validates via your app and then proxies to Prefect UI/API. - Machine clients (workers/flows/CLI): They send a header (e.g., Authorization: Basic ... or Bearer ...), which your app validates. - Setup outline: 1) Run Prefect Server on an internal address (e.g., 127.0.0.1:4200 or a private network). 2) Expose NGINX externally and protect both the UI and API with
auth_request
. 3) Have NGINX pass the Authorization or X-API-Key header to your auth app’s validation endpoint. Example NGINX snippet (proxying everything to your Prefect Server):
Copy code
# Upstreams
upstream prefect_api {
    server 127.0.0.1:4200;  # wherever `prefect server start` exposes the API/UI
}

upstream auth_backend {
    server 127.0.0.1:9000;  # your auth service that validates tokens/API keys
}

server {
    listen 443 ssl;
    server_name <http://prefect.example.com|prefect.example.com>;

    # TLS config omitted for brevity

    # Subrequest that validates auth via your app
    location = /_auth {
        internal;
        proxy_pass <http://auth_backend/validate;>     # your app's validation endpoint
        proxy_set_header Authorization $http_authorization;
        proxy_set_header X-API-Key $http_x_api_key;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
    }

    # Protect everything (UI + API)
    location / {
        auth_request /_auth;

        proxy_pass <http://prefect_api>;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Helpful for long-running/streaming endpoints
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # If you see issues with event streams, consider:
        # proxy_buffering off;
        # proxy_set_header Upgrade $http_upgrade;
        # proxy_set_header Connection "upgrade";
    }
}
Notes: - Prefect Server itself does not read your auth headers — the proxy enforces auth. - Keep the UI and API on the same origin/host to avoid CORS headaches; the UI will call the API via the same host by default. Prefect clients (workers/flows/CLI) with your proxy - For non-interactive clients, you want them to include an Authorization header that your proxy enforces. Prefect provides a setting for basic auth: - `PREFECT_API_URL`: point clients to your proxy’s API URL - `PREFECT_API_AUTH_STRING`: a
username:password
string; the client will send
Authorization: Basic ...
- Example:
Copy code
export PREFECT_API_URL="<https://prefect.example.com/api>"
export PREFECT_API_AUTH_STRING="svc:YOUR_API_KEY_OR_PASSWORD"
or persist it to a profile
Copy code
prefect config set PREFECT_API_URL=<https://prefect.example.com/api>
prefect config set PREFECT_API_AUTH_STRING="svc:YOUR_API_KEY"
Your NGINX auth subrequest can accept Basic auth (or translate Basic -> your own validation). If you require Bearer/JWT instead of Basic: - Prefect clients don’t have a first-class setting to send arbitrary Bearer tokens to self-hosted Server. Two common workarounds: 1) Keep using Basic at the client but have your proxy/auth service accept Basic and internally map/validate it against your API key/JWT via your auth endpoint. 2) Put a very small sidecar or gateway in front of Prefect that injects the Bearer token and forwards to Prefect; clients hit the gateway with Basic or a custom header your gateway understands. Option B — oauth2-proxy in front of Prefect - If your app can act as an OIDC provider (or you wrap it with one), you can put oauth2-proxy in front of Prefect to handle browser SSO and issue a session cookie. - Pros: user-friendly UI access, SSO flows, group claims, etc. - Considerations: machine clients (workers/flows/CLI) don’t do interactive logins; you’ll typically pair this with: - bypass rules for service accounts hitting the API with a static header validated by your auth service, or - a separate non-interactive token validation path (oauth2-proxy supports validating JWTs from trusted issuers). Other notes and tradeoffs - RBAC/User accounts: Self-hosted Prefect Server is single-tenant — once a request is allowed, it has full access to that workspace. If you need per-user roles and resource scoping, Prefect Cloud is the path. - Helm/Ingress: If you deploy via Helm/Ingress, you can use built-in annotations for external auth (e.g., `nginx.ingress.kubernetes.io/auth-url`) and TLS. See Server Helm. - Security settings: See Security settings for settings like
PREFECT_API_URL
,
PREFECT_API_AUTH_STRING
, CSRF protection, etc. - Community reference (reverse proxy + Prefect): example repo (community-maintained, but useful for patterns). A few clarifying questions to tailor a concrete setup: - Are you using Prefect Cloud or self-hosting Prefect Server? - For self-hosting: Do you want the UI accessible to end users (with your sign-in) or is it primarily machine-to-machine? - Can your auth app expose a synchronous “validate token/key” HTTP endpoint that NGINX or oauth2-proxy can call? - Do you want clients to use Basic auth to the proxy, or must they use your Bearer/JWT format directly? If you can share those, I can provide an exact NGINX (or oauth2-proxy) config and the precise Prefect settings to use.
m
@Marvin I decided to move on with keycloak, how do I use to secure access to prefect UI. While I will keep access to prefect API in a private network
m
thought for 74.1 seconds
Great choice. For self-hosted Prefect Server, the common pattern is: - Keep Prefect Server (UI + API) on a private network. - Put a reverse proxy on the public network that: - Handles Keycloak login (OIDC) - Proxies both UI and API to the private Prefect Server - Only allows requests from authenticated users This way the API is not directly exposed to the internet; it’s only reachable through the proxy after Keycloak auth. The Prefect UI needs to call the API, so proxying both through the same host is the simplest and avoids CORS trouble. Recommended architecture - Prefect Server: private subnet (e.g., http://10.0.0.20:4200) - oauth2-proxy: public or DMZ, configured with Keycloak (OIDC) - NGINX: public or DMZ, using
auth_request
to oauth2-proxy, and proxying to Prefect Server Step 1) Create a Keycloak client for the UI - Realm: your choice (e.g., myrealm) - Client type: OpenID Connect (confidential) - Redirect URI: https://prefect.example.com/oauth2/callback - Standard Flow: Enabled - Save client ID and secret Step 2) Deploy oauth2-proxy (pointed at Keycloak) Example (Docker-like config; adapt to your platform):
Copy code
# env: .env.oauth2-proxy
OAUTH2_PROXY_CLIENT_SECRET=...                    # from Keycloak client
OAUTH2_PROXY_COOKIE_SECRET=...                    # 32-byte base64 (e.g., `openssl rand -base64 32`)

# docker compose-style service (or run_args)
oauth2-proxy:
  image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.6.0|quay.io/oauth2-proxy/oauth2-proxy:v7.6.0>
  env_file: .env.oauth2-proxy
  ports:
    - "4180:4180"
  command:
    - --provider=oidc
    - --oidc-issuer-url=<https://keycloak.example.com/realms/myrealm>
    - --client-id=prefect-ui
    - --client-secret=${OAUTH2_PROXY_CLIENT_SECRET}
    - --redirect-url=<https://prefect.example.com/oauth2/callback>
    - --cookie-secret=${OAUTH2_PROXY_COOKIE_SECRET}
    - --scope=openid email profile
    - --http-address=0.0.0.0:4180
    - --reverse-proxy=true
    - --cookie-secure=true
    - --cookie-samesite=lax
    - --email-domain=*
    - --upstream=<static://200>
Step 3) Configure NGINX to enforce Keycloak and proxy to Prefect This keeps the actual Prefect API private; only NGINX can reach it, and only after login. ``` upstream prefect_backend { server 10.0.0.20:4200; # Prefect Server (private) } upstream oauth2_proxy { server 127.0.0.1:4180; # oauth2-proxy } server { listen 443 ssl; server_name prefect.example.com; # TLS config omitted # oauth2-proxy endpoints location /oauth2/ { proxy_pass http://oauth2_proxy; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_set_header X-Auth-Request-Redirect $request_uri; } # Subrequest endpoint for auth checks location = /oauth2/auth { proxy_pass http://oauth2_proxy; proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; } # Protect everything (UI + API) with Keycloak location / { auth_request /oauth2/auth; error_page 401 = /oauth2/start?rd=$request_uri; # Optional: forward user info headers to Prefect (not required) auth_request_set $user $upstream_http_x_auth_request_user; proxy_set_header X-Auth-Request-User $user; proxy_set_header X-Forwarded-User $user; proxy_pass http://prefect_backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Prefect benefits from long timeouts and streaming proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; # helpful for log/event streams } # If you want unauthenticated health checks, you can exempt specific paths # location /api/health { # proxy_pass http://prefect_backend;
# } } ``` Step 4) Prefect settings - Run Prefect Server privately (e.g.,
prefect server start
behind your firewall or in a private subnet). - You generally do not need special settings for the UI if everything is served on the same host and proxied together. - Optionally set these so links and redirects use the public URL: -
PREFECT_UI_URL=<https://prefect.example.com>
-
PREFECT_API_URL=<https://prefect.example.com/api>
(only if you want generated links to use the public URL; clients on the private network can still use the private API URL) Key points and caveats - The Prefect UI must be able to talk to the API. By proxying both via NGINX on the same host, the browser uses a relative
/api
path and stays on the same origin—no CORS issues. - The API is not directly exposed to the internet; only the proxy can reach it, and the proxy enforces Keycloak auth. - If you’re on Kubernetes, you can do the same with Ingress-NGINX and oauth2-proxy (via
auth-url
annotations). The pattern is identical. - If you see issues with streams or logs in the UI, ensure
proxy_buffering off
and long read timeouts for the API paths. References - Prefect security settings: Security settings - Prefect Server Helm (useful if you’re on K8s): Server Helm - oauth2-proxy docs: oauth2-proxy If you share: - Your deployment target (VMs vs Kubernetes) - Desired Keycloak group restrictions (who should get access) - Your current Prefect Server endpoint(s) …I can tailor the exact manifests/config you need (including K8s Ingress annotations or a full docker-compose stack).
m
@Marvin I resorted to these configs:
Copy code
name: prefect-wizard

services:
  keycloak:
    image: <http://quay.io/keycloak/keycloak:24.0|quay.io/keycloak/keycloak:24.0>
    command: start-dev
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KEYCLOAK_HOSTNAME: "<http://localhost:8080/realms/wizard>"
    ports:
      - "8080:8080"
    volumes:
      - keycloak_data:/opt/keycloak/data

  oauth2-proxy:
    image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.5.1|quay.io/oauth2-proxy/oauth2-proxy:v7.5.1>
    extra_hosts:
      - "host.docker.internal:host-gateway"
    ports:
      - "4180:4180"
    environment:
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_OIDC_ISSUER_URL: <http://host.docker.internal:8080/realms/wizard>
      OAUTH2_PROXY_CLIENT_ID: wizard-oauth
      OAUTH2_PROXY_CLIENT_SECRET: "olcWi1lGcyrU82MtdvNgK8Nm7x4C8ieA"
      OAUTH2_PROXY_COOKIE_SECRET: "bHEyY2E0dEZHSTlnY2hJa0FRWnRERURxaDg4Qm5qY3k="
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
      OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
      OAUTH2_PROXY_UPSTREAMS: "<http://server:4200>"
      OAUTH2_PROXY_REDIRECT_URL: "<http://host.docker.internal:4180/oauth2/callback>"
    depends_on:
      - keycloak
      - server

  api:
    build:
      context: .
      dockerfile: docker/api/nonprod/Dockerfile
    ports:
      - "9832:9832"
    environment:
      PREFECT_API_URL: <http://server:4200/api>
    volumes:
      - ./api:/app
      - ./autoupdate:/app/autoupdate
      - ./core:/app/core
    extra_hosts:
      - "host.docker.internal:host-gateway"
    env_file:
      - .env
    restart: always
    healthcheck:
      test: [ "CMD-SHELL", "curl -f <http://localhost:9832/health> || exit 1" ]
      interval: 5s
      timeout: 5s
      retries: 5

  db:
    image: postgres:14
    environment:
      POSTGRES_USER: prefect
      POSTGRES_PASSWORD: prefect
      POSTGRES_DB: prefect
    ports:
      - "5466:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U prefect" ]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7
    volumes:
      - redis_data:/data
    healthcheck:
      test: [ "CMD-SHELL", "redis-cli ping" ]
      interval: 5s
      timeout: 5s
      retries: 5

  server:
    image: prefecthq/prefect:3.4.13-python3.11
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@db:5432/prefect>
      PREFECT_SERVER_API_HOST: 0.0.0.0
      PREFECT_UI_API_URL: "<http://localhost:4200/api>"
      PREFECT_MESSAGING_BROKER: prefect_redis.messaging
      PREFECT_MESSAGING_CACHE: prefect_redis.messaging
      PREFECT_REDIS_MESSAGING_HOST: redis
      PREFECT_REDIS_MESSAGING_PORT: 6379
      PREFECT_REDIS_MESSAGING_DB: 0
      PREFECT_LOGGING_LEVEL: DEBUG
      PREFECT_LOGGING_LOG_PRINTS: True
      PREFECT_SERVER_LOGGING_LEVEL: DEBUG
      PREFECT_SERVER_LOGGING_LOG_PRINTS: True
      #PREFECT_SERVER_API_AUTH_STRING: "admin:admin"
    command: prefect server start --no-services
    ports:
      - "4200:4200"
    healthcheck:
      test: [ "CMD-SHELL", "python -c 'import json,urllib.request,sys; sys.exit(0 if json.loads(urllib.request.urlopen(\"<http://localhost:4200/api/health>\", timeout=1).read()) is True else 1)' " ]
      interval: 5s
      timeout: 3s
      retries: 30

  services:
    image: prefecthq/prefect:3.4.13-python3.11
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      PREFECT_API_DATABASE_CONNECTION_URL: <postgresql+asyncpg://prefect:prefect@db:5432/prefect>
      PREFECT_API_URL: <http://server:4200/api>
      PREFECT_MESSAGING_BROKER: prefect_redis.messaging
      PREFECT_MESSAGING_CACHE: prefect_redis.messaging
      PREFECT_REDIS_MESSAGING_HOST: redis
      PREFECT_REDIS_MESSAGING_PORT: 6379
      PREFECT_REDIS_MESSAGING_DB: 0
    command: prefect server services start

  create-deployment:
    image: prefect-workpool
    build:
      context: .
      dockerfile: docker/autoupdate/Dockerfile
    depends_on:
      - server
    environment:
      PREFECT_API_URL: <http://server:4200/api>
    # command: prefect deployment build autoupdate/pipeline/deployment.py:autoupdate_pipeline --name autoupdate-pipeline --cron "0 0 * * *" --timezone "America/New_York" --infra prefect-wizard-infra --storage-block prefect-s3-block
    command: bash -c "cd /opt/prefect/autoupdate && prefect --no-prompt deploy --name autoupdate-deployment"
    volumes:
      - ./autoupdate:/opt/prefect/autoupdate
      - ./core:/opt/prefect/autoupdate/core
    env_file:
      - .env

  autoupdate-worker:
    image: prefect-workpool
    depends_on:
      server:
        condition: service_healthy
      services:
        condition: service_started
      create-deployment:
        condition: service_completed_successfully
    environment:
      PREFECT_API_URL: <http://server:4200/api>
      DOCKER_HOST: <tcp://host.docker.internal:2375>
      PREFECT_LOCAL_STORAGE_PATH: /logs/storage
      # DOCKER_TLS_VERIFY: ${DOCKER_TLS_VERIFY:-}
      # DOCKER_CERT_PATH: ${DOCKER_CERT_PATH:-}
    restart: always
    volumes:
      - ./autoupdate:/opt/prefect/autoupdate
      - ./core:/opt/prefect/autoupdate/core
      - ${HOME}/.wizard:/logs
    extra_hosts:
      - "host.docker.internal:host-gateway"
    env_file:
      - .env

volumes:
  postgres_data:
  redis_data:
  keycloak_data:
Now there's an issue with networking, i am having DNS split brains problem, docker network sees oauth and keycloak differently than how browser view them from local host
m
thought for 208.4 seconds
Thanks for sharing the compose. The “split brain” you’re seeing is because different actors (the browser, containers on the Docker bridge network, and Keycloak itself) are using different hostnames for the same services. For OIDC to work, the Issuer URL, browser callback URL, and Keycloak’s advertised hostname must all be consistent and resolvable from the right places. Key issues I see: - Keycloak hostname: you’re setting
KEYCLOAK_HOSTNAME
to a full realm URL with http + path. For Keycloak 24, you should use
KC_HOSTNAME
and it should be just the host (and optional port), not include
/realms/...
. - Issuer mismatch:
OAUTH2_PROXY_OIDC_ISSUER_URL
uses
<http://host.docker.internal:8080/realms/wizard>
but Keycloak may be advertising a different issuer (due to the hostname setting above). OAuth2 Proxy requires the discovered
issuer
to exactly match what you configured. - Browser vs container names: The browser cannot resolve Docker service names like `server`; it needs a browser-visible URL. Meanwhile containers should use Docker DNS (service names). - UI calling the API: Your
PREFECT_UI_API_URL
is currently
"<http://localhost:4200/api>"
, which bypasses your proxy. If you want the UI (in the browser) to go through oauth2-proxy, point this to the proxy’s external URL (and make oauth2-proxy forward
/api
to the Prefect server). A solid way to fix this - Pick one canonical external URL for Keycloak that both the browser and oauth2-proxy will use (e.g., auth.localtest.me:8080localtest.me resolves to 127.0.0.1, no hosts file needed). - Configure Keycloak to advertise that URL. - Configure oauth2-proxy to use that exact issuer and a redirect URL that the browser will reach. - Configure Prefect UI to call the API via the proxy. Concrete changes 1) Keycloak (use KC_* vars in v24 and set a single hostname)
Copy code
keycloak:
    image: <http://quay.io/keycloak/keycloak:24.0|quay.io/keycloak/keycloak:24.0>
    command: ["start-dev", "--http-port=8080"]
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      KC_HOSTNAME: "<http://auth.localtest.me|auth.localtest.me>"          # no path, just host (and optional port)
      KC_HOSTNAME_STRICT: "false"               # dev convenience
      KC_HOSTNAME_STRICT_HTTPS: "false"         # dev convenience
      KC_PROXY: "edge"                          # recommended when behind a proxy
    ports:
      - "8080:8080"
    volumes:
      - keycloak_data:/opt/keycloak/data
- In your Keycloak realm “wizard,” the well-known config should then show issuer as
<http://auth.localtest.me:8080/realms/wizard>
. 2) OAuth2 Proxy (match issuer, use browser-visible callback)
Copy code
oauth2-proxy:
    image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.5.1|quay.io/oauth2-proxy/oauth2-proxy:v7.5.1>
    ports:
      - "4180:4180"
    environment:
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_OIDC_ISSUER_URL: "<http://auth.localtest.me:8080/realms/wizard>"
      OAUTH2_PROXY_CLIENT_ID: wizard-oauth
      OAUTH2_PROXY_CLIENT_SECRET: "olcWi1lGcyrU82MtdvNgK8Nm7x4C8ieA"
      OAUTH2_PROXY_COOKIE_SECRET: "bHEyY2E0dEZHSTlnY2hJa0FRWnRERURxaDg4Qm5qY3k="  # 32B base64
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
      OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
      OAUTH2_PROXY_REDIRECT_URL: "<http://localhost:4180/oauth2/callback>"  # must match Keycloak client redirect
      OAUTH2_PROXY_UPSTREAMS: "<http://server:4200>"
- Update your Keycloak client “wizard-oauth” to include
<http://localhost:4180/oauth2/callback>
as a valid redirect URI. - If you want a nicer hostname than localhost:4180 for the browser, you can use something like
<http://prefect.localtest.me>
and set cookie/domain options accordingly. 3) Prefect server behind the proxy (make the UI call the API through oauth2-proxy) - Proxy both “/” (the UI) and “/api” through oauth2-proxy to
server:4200
. - Then set: ``` server: image: prefecthq/prefect:3.4.13-python3.11 environment:
PREFECT_API_DATABASE_CONNECTION_URL: postgresql+asyncpg://prefect:prefect@db:5432/prefect PREFECT_SERVER_API_HOST: 0.0.0.0 PREFECT_UI_API_URL: "http://localhost:4180/api" # externally visible URL the browser should use PREFECT_MESSAGING_BROKER: prefect_redis.messaging PREFECT_MESSAGING_CACHE: prefect_redis.messaging PREFECT_REDIS_MESSAGING_HOST: redis PREFECT_REDIS_MESSAGING_PORT: 6379 PREFECT_REDIS_MESSAGING_DB: 0 ``` - Alternatively, if you mount oauth2-proxy at a host like
<http://prefect.localtest.me>
, use
PREFECT_UI_API_URL: "<http://prefect.localtest.me/api>"
. The key is: this must be the browser-visible address, not the Docker service name or port 4200 directly. 4) Avoid host.docker.internal unless you must - On Mac/Windows it works, but on Linux it often causes confusion. Prefer a single canonical hostname that your browser resolves (like localtest.me) and let containers talk to each other by service name on the Docker network (e.g.,
server
,
keycloak
). - If you must use
host.docker.internal
, ensure every container that references it has the
extra_hosts
mapping and that the browser can resolve that name too. Quick checklist to validate - Visit http://auth.localtest.me:8080/realms/wizard/.well-known/openid-configuration in your browser. The
issuer
value must exactly match
OAUTH2_PROXY_OIDC_ISSUER_URL
. - In oauth2-proxy logs, there should be no “issuer mismatch” warnings. - The Keycloak client redirect URI must exactly match
OAUTH2_PROXY_REDIRECT_URL
. - The browser DevTools network tab should show the UI calling
<http://localhost:4180/api/>...
(or your chosen host), not
<http://localhost:4200>
. If you’d like, I can help you wire oauth2-proxy to route
/api
to
<http://server:4200/api>
and everything else to
<http://server:4200/>
explicitly. Also, let me know: - Are you on Linux, macOS, or Windows? - Do you want the API protected by oauth2-proxy as well, or just the UI? References - Prefect 3.x security settings - Example: Prefect Server behind a reverse proxy
m
@Marvin oauth2 proxy container can't resolve this canonical url you suggested:
Copy code
[2025/08/30 19:40:53] [main.go:60] ERROR: Failed to initialise OAuth2 Proxy: error initialising provider: could not create provider data: error building OIDC ProviderVerifier: could not get verifier builder: error while discovery OIDC configuration: failed to discover OIDC configuration: error performing request: Get "<http://auth.localtest.me:8080/realms/wizard/.well-known/openid-configuration>": dial tcp 127.0.0.1:8080: connect: connection refused
m
thought for 77.7 seconds
Right now
<http://auth.localtest.me|auth.localtest.me>
resolves to 127.0.0.1 inside the oauth2-proxy container, so it’s trying to hit itself on 8080. That’s why you see “connect: connection refused.” You need that hostname to resolve differently depending on who is asking: - In the browser: to 127.0.0.1 (so it reaches the host’s published port 8080 → Keycloak) - In containers: to the Keycloak container IP (so they can reach Keycloak directly) Recommended fix: give Keycloak a Docker network alias that matches the public hostname, and set Keycloak to advertise that same hostname. Changes 1) Keycloak: switch to KC_* vars and add a network alias
Copy code
keycloak:
    image: <http://quay.io/keycloak/keycloak:24.0|quay.io/keycloak/keycloak:24.0>
    command: ["start-dev", "--http-port=8080"]
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      KC_HOSTNAME: "<http://auth.localtest.me|auth.localtest.me>"     # no path
      KC_HOSTNAME_STRICT: "false"
      KC_HOSTNAME_STRICT_HTTPS: "false"
      KC_PROXY: "edge"
    ports:
      - "8080:8080"
    networks:
      default:
        aliases:
          - <http://auth.localtest.me|auth.localtest.me>
2) oauth2-proxy: point issuer to that hostname (no extra_hosts needed)
Copy code
oauth2-proxy:
    image: <http://quay.io/oauth2-proxy/oauth2-proxy:v7.5.1|quay.io/oauth2-proxy/oauth2-proxy:v7.5.1>
    ports:
      - "4180:4180"
    environment:
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_OIDC_ISSUER_URL: "<http://auth.localtest.me:8080/realms/wizard>"
      OAUTH2_PROXY_CLIENT_ID: wizard-oauth
      OAUTH2_PROXY_CLIENT_SECRET: "olcWi1lGcyrU82MtdvNgK8Nm7x4C8ieA"
      OAUTH2_PROXY_COOKIE_SECRET: "bHEyY2E0dEZHSTlnY2hJa0FRWnRERURxaDg4Qm5qY3k="
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
      OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
      OAUTH2_PROXY_REDIRECT_URL: "<http://localhost:4180/oauth2/callback>"
      OAUTH2_PROXY_UPSTREAMS: "<http://server:4200>"
Why this works - Containers on the compose network will resolve
<http://auth.localtest.me|auth.localtest.me>
via Docker’s embedded DNS to the Keycloak container IP. - Your browser will resolve
<http://auth.localtest.me|auth.localtest.me>
to 127.0.0.1 (thanks to localtest.me), which maps to the host’s published 8080 and then to the Keycloak container. - Keycloak advertises
<http://auth.localtest.me|auth.localtest.me>
in the discovery document, so oauth2-proxy’s configured issuer matches exactly. After changing network aliases, bring the stack down and back up so Docker recreates the network:
Copy code
docker compose down
docker compose up -d --build
Validate from within the oauth2-proxy container:
Copy code
docker compose exec oauth2-proxy getent hosts <http://auth.localtest.me|auth.localtest.me>
docker compose exec oauth2-proxy curl -s <http://auth.localtest.me:8080/realms/wizard/.well-known/openid-configuration>
Alternative if you don’t want a network alias - Map the hostname to the host gateway for oauth2-proxy:
Copy code
oauth2-proxy:
    extra_hosts:
      - "<http://auth.localtest.me:host-gateway|auth.localtest.me:host-gateway>"
This makes oauth2-proxy reach Keycloak via the host’s published port. Still set
KC_HOSTNAME=<http://auth.localtest.me|auth.localtest.me>
so the issuer matches. Reminder for Prefect UI - If you’re protecting the API via oauth2-proxy, set
PREFECT_UI_API_URL
to the browser-visible proxy URL, e.g.:
Copy code
PREFECT_UI_API_URL: "<http://localhost:4180/api>"
If you share your OS (Linux/macOS/Windows), I can tailor which of the two approaches (alias vs host-gateway) is the smoother fit for your setup.