Brady Mitchell
08/15/2025, 10:03 PMPREFECT_API_KEY
environment variable, but I'm still able to make requests without passing the api key. How do I require prefect to reject api calls that don't include the api key?Marvin
08/15/2025, 10:03 PMMarvin
08/15/2025, 10:05 PMPREFECT_API_KEY
only affects Prefect clients talking to Prefect Cloud. It does not make a self‑hosted Prefect Server enforce authentication. So yes—you can still hit your self‑hosted API without a key, because Prefect Server doesn’t validate it.
What to do instead
- If you’re on Prefect Cloud: the Cloud API already requires auth for workspace resources. If you’re seeing unauthenticated calls succeed, you’re likely hitting public endpoints (e.g., health) or a local Server by mistake. Share the exact endpoint if you want me to sanity‑check it.
- If you’re self‑hosting Prefect Server: there’s no built‑in request auth in 3.4.13. Put the API behind an authenticating reverse proxy (Basic Auth, OAuth/OIDC via oauth2‑proxy, etc.) or restrict access via network controls.
Example: NGINX Basic Auth in front of the API
1) Create users:
htpasswd -c /etc/nginx/.htpasswd admin
2) NGINX config:
server {
listen 443 ssl;
server_name <http://prefect.example.com|prefect.example.com>;
# ssl certs...
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass <http://prefect-api:4200;> # your Prefect API service
}
}
If you really want a “bearer-style” shared secret, you can have the proxy reject requests missing the Authorization: Bearer <secret>
header, but note the Prefect UI won’t add that header from the browser unless you customize it—Basic Auth or OIDC is usually friendlier for the UI.
Docs for reference
- Security guidance (reverse proxy, TLS, CORS): Security settings
- Cloud API keys (client-side usage): Cloud API keys
If you can confirm whether you’re on Cloud or self-hosted Server (and which endpoint you’re calling), I can give you a tighter config. I’d say “thrilling,” but it’s auth.Brady Mitchell
08/15/2025, 10:06 PMMarvin
08/15/2025, 10:06 PMMarvin
08/15/2025, 10:07 PMPREFECT_API_KEY
is only used by clients when talking to Prefect Cloud. The Server won’t validate it, so unauthenticated requests will still work unless you put something in front of it.
How to require auth
Pick one of these common patterns and put it in front of the API (and UI):
- Basic Auth via reverse proxy (simple, works fine with the UI)
- OIDC/OAuth2 (via oauth2-proxy, NGINX Ingress auth, Traefik ForwardAuth, Cloudflare Access)
- Private network/VPN (no public ingress)
- Shared secret header enforced by the proxy (for service-to-service)
Example: NGINX Basic Auth
1) Create credentials:
htpasswd -c /etc/nginx/.htpasswd admin
2) Minimal NGINX config in front of your Prefect API:
server {
listen 443 ssl;
server_name <http://prefect.example.com|prefect.example.com>;
# ssl certs...
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass <http://prefect-api:4200;> # your Prefect API service
}
}
Example: Require a Bearer token header at the proxy
map $http_authorization $is_auth {
default 0;
"~^Bearer SUPERSECRET123$" 1;
}
server {
listen 443 ssl;
server_name <http://prefect.example.com|prefect.example.com>;
location / {
if ($is_auth = 0) { return 401; }
proxy_pass <http://prefect-api:4200;>
}
}
Note: browsers won’t magically add that header, so if you use the UI, Basic Auth or OIDC is friendlier.
References
- Prefect security guidance (TLS, proxies, CORS): Security settings
If you tell me how you’re running it (Docker Compose vs. Kubernetes, which proxy/ingress, and whether you want Basic Auth or OIDC), I’ll give you a drop‑in config. I’d say it’s exciting, but it’s auth.Brady Mitchell
08/15/2025, 10:13 PMMarvin
08/15/2025, 10:13 PMMarvin
08/15/2025, 10:14 PMAuthorization: Bearer <token>
is present (automation).
- Most proxies can do this “OR” logic; if not, run two hostnames:
- ui.example.com → cookie/OIDC
- api.example.com → Bearer token
- Point the UI at the API host and enable CORS appropriately.
- Basic Auth for humans, Bearer for automation:
- Keep API requiring Bearer.
- Put Basic Auth in front of the UI and API; browsers will automatically send Basic credentials with requests the UI makes.
- Scripts can use Bearer; the proxy can allow either Basic or Bearer.
- Simpler operationally, but still requires proxy logic.
What won’t work securely
- Requiring Bearer on the API while leaving the UI completely open and expecting it to “just work.” The UI won’t attach your token, and injecting one at the proxy leaks it to anyone using the site.
If you share how you’re hosting (Docker Compose vs. Kubernetes), your proxy/ingress, and whether you prefer OIDC or Basic, I’ll give you a drop‑in config. Prefect’s security notes (TLS, CORS, proxies) are here: Security settings.Brady Mitchell
08/15/2025, 10:24 PMMarvin
08/15/2025, 10:24 PMMarvin
08/15/2025, 10:25 PMapiVersion: <http://networking.k8s.io/v1|networking.k8s.io/v1>
kind: Ingress
metadata:
name: prefect-api
annotations:
<http://kubernetes.io/ingress.class|kubernetes.io/ingress.class>: nginx
# Require a specific Bearer token
<http://nginx.ingress.kubernetes.io/configuration-snippet|nginx.ingress.kubernetes.io/configuration-snippet>: |
if ($http_authorization = "") { return 401; }
if ($http_authorization !~* "^Bearer SUPERSECRET123$") { return 401; }
add_header WWW-Authenticate 'Bearer realm="Prefect API"' always;
# Optional: do not forward the token to the upstream
proxy_set_header Authorization "";
spec:
rules:
- host: <http://api.prefect.example.com|api.prefect.example.com>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: prefect-api
port:
number: 4200
tls:
- hosts:
- <http://api.prefect.example.com|api.prefect.example.com>
secretName: api-tls
Notes:
- Replace SUPERSECRET123 with your token. Be aware it’s visible to anyone who can read this Ingress; rotate accordingly.
- Some clusters disable configuration-snippet/server-snippet for security. If that’s you, use option B.
B) External auth (auth-request) with a tiny validator service (recommended for secrets hygiene)
- Run a small service that checks the Authorization header against a token stored in a Kubernetes Secret and returns 200 or 401.
- Point ingress-nginx at it with auth-url. This keeps the secret out of annotations.
apiVersion: <http://networking.k8s.io/v1|networking.k8s.io/v1>
kind: Ingress
metadata:
name: prefect-api
annotations:
<http://kubernetes.io/ingress.class|kubernetes.io/ingress.class>: nginx
<http://nginx.ingress.kubernetes.io/auth-url|nginx.ingress.kubernetes.io/auth-url>: "<http://bearer-auth.default.svc.cluster.local/validate>"
<http://nginx.ingress.kubernetes.io/auth-snippet|nginx.ingress.kubernetes.io/auth-snippet>: |
# Pass through the incoming Authorization header to the auth service
proxy_set_header Authorization $http_authorization;
# Optional: do not forward Authorization to Prefect upstream
<http://nginx.ingress.kubernetes.io/configuration-snippet|nginx.ingress.kubernetes.io/configuration-snippet>: |
proxy_set_header Authorization "";
spec:
rules:
- host: <http://api.prefect.example.com|api.prefect.example.com>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: prefect-api
port:
number: 4200
tls:
- hosts:
- <http://api.prefect.example.com|api.prefect.example.com>
secretName: api-tls
Then deploy a simple validator (any language) that:
- Reads the expected token from a Secret (env var or mounted file)
- Returns 200 if Authorization: Bearer <token>
matches, else 401 with WWW-Authenticate: Bearer realm="Prefect API"
Keep the UI on a separate hostname without auth, e.g.:
apiVersion: <http://networking.k8s.io/v1|networking.k8s.io/v1>
kind: Ingress
metadata:
name: prefect-ui
annotations:
<http://kubernetes.io/ingress.class|kubernetes.io/ingress.class>: nginx
spec:
rules:
- host: <http://ui.prefect.example.com|ui.prefect.example.com>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: prefect-ui
port:
number: 8080
tls:
- hosts:
- <http://ui.prefect.example.com|ui.prefect.example.com>
secretName: ui-tls
Docs if you want to dig:Marvin
08/15/2025, 10:25 PMBrady Mitchell
08/15/2025, 10:30 PMMarvin
08/15/2025, 10:30 PMMarvin
08/15/2025, 10:31 PM--provider=oidc
--oidc-issuer-url=<https://issuer.example.com/>
--email-domain=*
--cookie-secret=<random 32B base64>
--skip-jwt-bearer-tokens=true
(With provider=oidc, JWKS is discovered from the issuer’s metadata. Add audience/claims constraints per your IdP as needed.)
- Using YAML (alpha config style):
provider: oidc
oidcIssuerURL: <https://issuer.example.com/>
emailDomains:
- "*"
cookie:
secret: <random 32B base64>
skipJwtBearerTokens: true
# Optionally restrict acceptable issuers/audiences explicitly:
jwtIssuers:
- issuer: <https://issuer.example.com/>
jwksURL: <https://issuer.example.com/.well-known/jwks.json>
# audiences:
# - <api://prefect>
Ingress (API) – require Bearer JWT, no redirects
metadata:
annotations:
<http://kubernetes.io/ingress.class|kubernetes.io/ingress.class>: nginx
<http://nginx.ingress.kubernetes.io/auth-url|nginx.ingress.kubernetes.io/auth-url>: "<https://auth.example.com/oauth2/auth>"
# No auth-signin -> 401 for missing/invalid tokens
<http://nginx.ingress.kubernetes.io/auth-response-headers|nginx.ingress.kubernetes.io/auth-response-headers>: "X-Auth-Request-User,X-Auth-Request-Email"
# Optional: strip Authorization before upstream
<http://nginx.ingress.kubernetes.io/configuration-snippet|nginx.ingress.kubernetes.io/configuration-snippet>: |
proxy_set_header Authorization "";
Ingress (UI) – interactive login allowed
metadata:
annotations:
<http://kubernetes.io/ingress.class|kubernetes.io/ingress.class>: nginx
<http://nginx.ingress.kubernetes.io/auth-url|nginx.ingress.kubernetes.io/auth-url>: "<https://auth.example.com/oauth2/auth>"
<http://nginx.ingress.kubernetes.io/auth-signin|nginx.ingress.kubernetes.io/auth-signin>: "<https://auth.example.com/oauth2/start?rd=$scheme://$host$request_uri>"
<http://nginx.ingress.kubernetes.io/auth-response-headers|nginx.ingress.kubernetes.io/auth-response-headers>: "X-Auth-Request-User,X-Auth-Request-Email"
If you actually need an opaque static token (not a JWT):
- oauth2-proxy won’t validate that. Use:
- an ingress-nginx configuration-snippet to match the header, or
- an auth-request service that checks the token against a Kubernetes Secret.
Docs
- oauth2-proxy overview: oauth2-proxy configuration
- JWT bearer tokens feature: JWT bearer tokens
- ingress-nginx external auth: External authentication
If you share your IdP (e.g., Okta, Auth0, Azure AD) and preferred audiences/claims, I can give you exact values to drop into the config.