fleet-rlm supports three authentication modes via AUTH_MODE:
dev— Development mode with debug headers and HS256 JWT tokensentra— Production mode with Microsoft Entra ID and RS256 JWT validationneon— Neon Auth with EdDSA JWT validation and repository-backed admission
All routes consume normalized identity fields from auth providers:
| Field | Description | Source |
|---|---|---|
tenant_claim |
Tenant identifier | tid claim in Entra, configured NEON_TENANT_CLAIM in Neon |
user_claim |
User identifier | oid or sub claim |
email |
User email (optional) | email, preferred_username, or upn claim |
name |
User display name (optional) | name claim |
| Variable | Description | Default | Required |
|---|---|---|---|
AUTH_MODE |
Auth provider mode (dev, entra, or neon) |
dev |
No |
AUTH_REQUIRED |
Enforce authentication on all routes | true when AUTH_MODE=entra or AUTH_MODE=neon |
No |
ALLOW_DEBUG_AUTH |
Enable debug header authentication | true in local env |
No |
ALLOW_QUERY_AUTH_TOKENS |
Allow legacy non-Neon tokens in WebSocket query params | true in local env or AUTH_MODE=entra |
No |
DEV_JWT_SECRET |
Secret for HS256 token signing/verification | change-me |
In staging/prod |
ENTRA_JWKS_URL |
Entra JWKS endpoint URL | — | When AUTH_MODE=entra |
ENTRA_AUDIENCE |
Expected token audience (API client ID) | — | When AUTH_MODE=entra |
ENTRA_ISSUER_TEMPLATE |
Issuer URL template with {tenantid} placeholder |
https://login.microsoftonline.com/{tenantid}/v2.0 |
No |
NEON_AUTH_URL |
Neon Auth branch URL | — | When AUTH_MODE=neon |
NEON_TENANT_CLAIM |
Internal tenant admission claim for this Neon deployment | default |
When AUTH_MODE=neon |
The server enforces these guardrails at startup:
When APP_ENV=staging or APP_ENV=production:
AUTH_REQUIREDmust betrueALLOW_DEBUG_AUTHmust befalseALLOW_QUERY_AUTH_TOKENSmust befalseunless a supported compatibility mode requires itCORS_ALLOWED_ORIGINScannot contain*DEV_JWT_SECRETmust be customized from default value
When AUTH_MODE=entra:
AUTH_REQUIREDmust betrueDATABASE_REQUIREDmust betrue(tenant admission requires database)ENTRA_JWKS_URLmust be configuredENTRA_AUDIENCEmust be configuredENTRA_ISSUER_TEMPLATEmust contain{tenantid}placeholder
When AUTH_MODE=neon:
AUTH_REQUIREDmust betrueDATABASE_REQUIREDmust betruebecause tenant/user admission is repository-backedNEON_AUTH_URLmust be configuredNEON_TENANT_CLAIMmust match an admitted tenant row
Development mode provides flexible authentication for local development and testing.
When ALLOW_DEBUG_AUTH=true, the following headers authenticate requests:
X-Debug-Tenant-Id: <tenant-id>
X-Debug-User-Id: <user-id>
X-Debug-Email: <email> # Optional
X-Debug-Name: <name> # OptionalExample:
curl -H "X-Debug-Tenant-Id: tenant-123" \
-H "X-Debug-User-Id: user-456" \
-H "X-Debug-Email: alice@example.com" \
http://localhost:8000/api/v1/auth/meSign and verify JWT tokens using the DEV_JWT_SECRET:
Required claims:
tid— Tenant IDoid— User/object ID
Optional claims:
emailname
Example token payload:
{
"tid": "tenant-123",
"oid": "user-456",
"email": "alice@example.com",
"name": "Alice Smith"
}Example request:
# Generate a dev token from the maintained helper script
TOKEN=$(uv run python scripts/dev_issue_token.py \
--tid tenant-123 \
--oid user-456 \
--email alice@example.com \
--name "Alice Smith" \
--secret "$DEV_JWT_SECRET")
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/v1/auth/meWhen ALLOW_QUERY_AUTH_TOKENS=true, WebSocket connections can bootstrap via query parameters when a bearer Authorization header is not available:
Debug auth (when ALLOW_DEBUG_AUTH=true):
ws://localhost:8000/api/v1/ws/execution?debug_tenant_id=tenant-123&debug_user_id=user-456
Token auth:
ws://localhost:8000/api/v1/ws/execution?access_token=<hs256-jwt>
Prefer Authorization: Bearer ... on HTTP requests and on websocket clients that can forward headers. Use access_token query bootstrap only on websocket paths that explicitly support it.
When AUTH_REQUIRED=false:
- If authentication fails, requests fall back to default identity
- Default tenant:
default - Default user:
anonymous
This is useful for local development but must not be used in staging/production.
Entra mode provides production-grade multitenant authentication using Microsoft Entra ID (Azure AD).
-
Extract Bearer Token — From
Authorization: Bearer <token>header or WebSocketaccess_tokenquery parameter when enabled -
Decode Unverified Claims — Extract
tidclaim to determine the tenant -
Derive Expected Issuer — Replace
{tenantid}inENTRA_ISSUER_TEMPLATEwith the token'stid:https://login.microsoftonline.com/{tenantid}/v2.0 -
Fetch Signing Key — Use JWKS client to fetch the public key matching the token's
kid -
Verify Signature — Validate RS256 signature against the signing key
-
Verify Claims — Validate:
issmatches expected issueraudmatchesENTRA_AUDIENCEexpandiatare valid (required claims)tidis present
# Required for AUTH_MODE=entra
AUTH_MODE=entra
ENTRA_JWKS_URL=https://login.microsoftonline.com/common/discovery/v2.0/keys
ENTRA_AUDIENCE=api://your-api-client-id
DATABASE_URL=postgresql://... # Runtime DB connection (pooled)
# DATABASE_ADMIN_URL=postgresql://... # Optional direct admin/migration connection
# Optional (defaults shown)
ENTRA_ISSUER_TEMPLATE=https://login.microsoftonline.com/{tenantid}/v2.0The JWKS URL provides public keys for signature verification:
| Environment | JWKS URL |
|---|---|
| Azure Public Cloud | https://login.microsoftonline.com/common/discovery/v2.0/keys |
| Azure Government | https://login.microsoftonline.us/common/discovery/v2.0/keys |
| Custom Sovereign Cloud | Use your cloud's discovery endpoint |
JWKS client behavior:
- Keys are cached for 5 minutes (
lifespan=300) - Automatic key refresh on cache expiry
- Network failures return
503 Service Unavailable
Entra tokens map to normalized identity:
| Entra Claim | Normalized Field | Notes |
|---|---|---|
tid |
tenant_claim |
Required; used for issuer derivation |
oid |
user_claim |
Preferred; falls back to sub |
sub |
user_claim |
Used if oid is missing |
preferred_username |
email |
Primary email source |
email |
email |
Fallback |
upn |
email |
Fallback for alternate tokens |
name |
name |
Display name |
For WebSocket connections, include the access token in the query string:
wss://your-api.com/api/v1/ws/execution?access_token=<entra-access-token>
Note: Query tokens are enabled by default for Entra mode (ALLOW_QUERY_AUTH_TOKENS=true).
Neon mode uses Neon Auth for browser sign-in and Fleet's FastAPI backend as the
product/runtime boundary. The frontend uses @neondatabase/auth-ui and
@neondatabase/neon-js to obtain Neon Auth sessions and JWTs, then sends those
JWTs to Fleet HTTP routes with Authorization: Bearer ....
- Extract bearer token from the HTTP
Authorizationheader. - Fetch public keys from
<NEON_AUTH_URL>/.well-known/jwks.json. - Verify EdDSA signature and standard claims. Python decoding must explicitly allow
algorithms=["EdDSA"]so Neon Auth tokens are not rejected before claim validation:issequals the origin ofNEON_AUTH_URLaudequals the origin ofNEON_AUTH_URLexpandiatare present and valid
- Map Neon
sub/idtouser_claim. - Map
tenant_claimfrom configuredNEON_TENANT_CLAIM; do not infer tenant identity from optional Neon org claims. - Resolve the identity through the repository admission flow before exposing product state.
AUTH_MODE=neon
AUTH_REQUIRED=true
DATABASE_REQUIRED=true
DATABASE_URL=postgresql://... # pooled runtime connection
DATABASE_ADMIN_URL=postgresql://... # direct admin/migration connection
NEON_AUTH_URL=https://ep-xxx.neonauth.../neondb/auth
NEON_TENANT_CLAIM=default
FLEET_SECRET_ENCRYPTION_KEY=... # Fernet key for hosted BYOK profile ciphertextTenant onboarding is administrative. A valid Neon Auth token is not enough by itself: the configured tenant and external Neon user id must resolve through Fleet's repository-backed admission path.
Neon Auth is deployment-owned configuration. Hosted users do not import,
choose, or override the Neon project used for authentication; the app validates
tokens only against the configured NEON_AUTH_URL for the current deployment.
LLM provider profiles in Neon mode are per-user BYOK records. The backend sets
Postgres request context before profile queries, RLS limits rows to the admitted
tenant/user, and API responses never include plaintext provider keys. Importing
server DSPY_* environment values into profiles is local-only.
Browsers cannot set arbitrary Authorization headers on native WebSocket
connections. For Neon mode, clients exchange their bearer token for a
short-lived, single-use ticket:
curl -X POST \
-H "Authorization: Bearer <neon-jwt>" \
http://localhost:8000/api/v1/auth/ws-ticketResponse:
{
"ticket": "opaque-one-time-ticket",
"expires_at": "2026-06-20T03:30:00Z"
}Then connect with:
wss://your-api.com/api/v1/ws/execution?ticket=<opaque-one-time-ticket>
Raw Neon JWTs are rejected in WebSocket query parameters so they are not exposed through browser URLs, proxies, or access logs.
Browser clients must also clear TanStack Query/session caches on logout or token expiration before showing unauthenticated or next-user state. Cached session lists are tenant-scoped product data, not anonymous browser state.
Neon Data API remains out of Fleet's product/runtime path. It can query Neon Postgres directly with JWT validation and Postgres RLS, but it does not authenticate Fleet's FastAPI routes, Daytona runtime, or WebSocket transport. Using Data API for product data would require a separate schema exposure, GRANT/RLS, and frontend data-flow review.
Controls whether authentication is enforced on non-health routes.
| Value | Behavior |
|---|---|
true |
All non-health HTTP routes and WebSockets require valid authentication |
false |
Failed authentication falls back to default identity (default/anonymous) |
Default:
truewhenAUTH_MODE=entraorAUTH_MODE=neonfalsewhenAUTH_MODE=devandAPP_ENV=localtruewhenAPP_ENV=stagingorAPP_ENV=production(enforced)
Controls whether debug headers (X-Debug-*) are accepted for authentication.
| Value | Behavior |
|---|---|
true |
Debug headers accepted as authentication credentials |
false |
Debug headers ignored; only Bearer tokens accepted |
Default:
truewhenAPP_ENV=localfalsewhenAPP_ENV=stagingorAPP_ENV=production(enforced)
Security Note: Debug headers must never be enabled in production environments as they bypass real authentication.
When AUTH_MODE=entra or AUTH_MODE=neon, the system performs tenant admission against the Neon database after token validation.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Entra Token │────▶│ JWKS Validate │────▶│ Normalized │
│ Validation │ │ & Claims │ │ Identity │
└─────────────────┘ └─────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Tenant/User │◀────│ Repository │◀────│ Admission │
│ IDs Resolved │ │ Lookup │ │ Check │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- Token Validation — Entra token validated, claims extracted
- Tenant Lookup — Query
tenantstable byentra_tenant_idmatchingtidclaim - Admission Check:
- Unknown tenant →
403 Forbidden("Tenant is not allowlisted for Fleet RLM.") - Suspended tenant →
403 Forbidden("Tenant access is suspended for Fleet RLM.") - Deleted tenant →
403 Forbidden("Tenant access has been removed for Fleet RLM.") - Active tenant → Proceed to user resolution
- Unknown tenant →
- User Resolution — Upsert user into
userstable with membership in the tenant - Return Identity — Internal
tenant_idanduser_idreturned for request context
Entra mode requires database for tenant admission:
DATABASE_URLmust be configuredDATABASE_REQUIRED=trueis enforcedtenantstable is the tenant allowlist source of truth
Tenant creation is not a side effect of login. New tenants must be explicitly added to the database:
INSERT INTO tenants (id, entra_tenant_id, status, plan)
VALUES (gen_random_uuid(), '<entra-tenant-id>', 'active', 'free');The /api/v1/auth/me endpoint returns both external claims and internal IDs:
{
"tenant_claim": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"user_claim": "ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj",
"email": "alice@example.com",
"name": "Alice Smith",
"tenant_id": "uuid-internal-tenant-id",
"user_id": "uuid-internal-user-id"
}GET /health— No auth requiredGET /ready— No auth required- All other routes — Auth required when
AUTH_REQUIRED=true
WS /api/v1/ws/execution— Auth required whenAUTH_REQUIRED=trueWS /api/v1/ws/execution— Auth required whenAUTH_REQUIRED=true
Auth claims are the canonical source of tenant/user identity:
tenant_claimanduser_claimfrom auth are authoritativeworkspace_idanduser_idon websocket payloads and query strings are unsupported and should be rejectedsession_idis the only authoritative client-controlled websocket selector- Internal
tenant_idanduser_idare resolved via database lookup during admission
Frontend SPA expectations:
- Neon Auth URL configured via
VITE_NEON_AUTH_URL - Sign-in and sign-up use
@neondatabase/auth-uiredirect-based forms - Post-auth redirect:
/app/workspace - Logout via
neonAuthClient.signOut()(fire-and-forget)
| Status Code | Description |
|---|---|
401 Unauthorized |
Missing or invalid authentication token |
403 Forbidden |
Tenant not allowlisted or tenant status not active |
503 Service Unavailable |
JWKS configuration missing or network failure |