Hadrian is experimental alpha software. Do not use in production.
Hadrian
Configuration

Authentication Configuration

API keys, JWT/OIDC, reverse proxy auth, and CEL-based RBAC policies

This page is the configuration reference for authentication settings in hadrian.toml. For conceptual overviews and guides, see:

The [auth] section configures authentication and authorization for both API requests and the web UI.

Authentication Architecture

Hadrian separates authentication into two contexts:

ContextSectionPurpose
Gateway[auth.gateway]Authenticate programmatic gateway API requests
Admin[auth.admin]Authenticate browser sessions for the admin UI

You can configure different authentication methods for each context. For example, use API keys for programmatic access and OIDC for the admin UI.

Gateway Authentication

Configure how gateway API requests (/v1/* endpoints) are authenticated.

No Authentication

Allows any request without credentials. Only suitable for local development.

[auth.gateway]
type = "none"

Never use type = "none" in production. All requests will be unauthenticated and usage cannot be tracked or billed.

API Key Authentication

Validates API keys stored in the database. Keys are created via the Admin API or UI.

[auth.gateway]
type = "api_key"
header_name = "X-API-Key"
key_prefix = "gw_"
generation_prefix = "gw_live_"
hash_algorithm = "sha256"
cache_ttl_secs = 60
SettingTypeDefaultDescription
header_namestringX-API-KeyHeader containing the API key. Also accepts Authorization: Bearer <key>.
key_prefixstringgw_Prefix for validating keys. Keys not starting with this prefix are rejected.
generation_prefixstringgw_live_Prefix for generating new keys. Distinguishes live keys from test keys.
hash_algorithmstringsha256Algorithm for hashing stored keys. Options: sha256, argon2.
cache_ttl_secsinteger60Cache validated keys for this duration. Set to 0 for no caching.

Hash Algorithms

AlgorithmSpeedUse Case
sha256FastHigh-entropy keys (recommended). Minimal latency impact.
argon2SlowLow-entropy keys or extra security. Adds ~50ms per uncached lookup.

Request Format

API keys can be sent in two formats:

# X-API-Key header
curl -H "X-API-Key: gw_live_abc123..." https://gateway.example.com/v1/chat/completions

# Authorization header (OpenAI-compatible)
curl -H "Authorization: Bearer gw_live_abc123..." https://gateway.example.com/v1/chat/completions

API Key Scoping

API keys can be restricted with permission scopes, model restrictions, IP allowlists, and per-key rate limits.

Permission Scopes

Control which API endpoints a key can access:

ScopeEndpoints
chat/v1/chat/completions, /v1/responses
completions/v1/completions (legacy)
embeddings/v1/embeddings
images/v1/images/*
audio/v1/audio/*
files/v1/files/*, /v1/vector_stores/*
models/v1/models
admin/admin/*

Example creating a key limited to chat and embeddings:

curl -X POST https://gateway.example.com/admin/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ML Pipeline Key",
    "owner": {"type": "organization", "org_id": "..."},
    "scopes": ["chat", "embeddings"]
  }'

When scopes is null or omitted, the key has full access to all endpoints.

Model Restrictions

Limit which models a key can use with wildcard patterns:

{
  "allowed_models": ["gpt-4*", "claude-3-opus"]
}

Pattern rules:

  • Exact match: "gpt-4" matches only gpt-4
  • Trailing wildcard: "gpt-4*" matches gpt-4, gpt-4o, gpt-4-turbo
  • No bare *: Use null for unrestricted model access

IP Allowlists

Restrict key usage to specific IP addresses or CIDR ranges:

{
  "ip_allowlist": ["10.0.0.0/8", "192.168.1.100", "2001:db8::/32"]
}

Supports both IPv4 and IPv6 addresses and CIDR notation.

Per-Key Rate Limits

Override global rate limits for specific keys:

{
  "rate_limit_rpm": 100,
  "rate_limit_tpm": 50000
}
SettingTypeDescription
rate_limit_rpmintegerRequests per minute
rate_limit_tpmintegerTokens per minute

By default, per-key limits cannot exceed global limits. Set allow_per_key_above_global = true in [limits.rate_limits] to allow per-key limits higher than global defaults.

Key Rotation

Rotate keys with a grace period during which both old and new keys work:

curl -X POST https://gateway.example.com/admin/v1/api-keys/{key_id}/rotate \
  -H "Content-Type: application/json" \
  -d '{"grace_period_seconds": 86400}'
ParameterTypeDefaultMaxDescription
grace_period_secondsinteger86400604800Duration both keys remain valid (24h default, 7 days max)

The response includes the new API key (store securely). The old key remains valid until the grace period expires, then is automatically treated as revoked.

Key rotation is useful for zero-downtime credential updates. Update your applications to use the new key during the grace period, then the old key automatically becomes inactive.

JWT Authentication

Validates JWTs signed by an identity provider. Tokens are verified against a JWKS endpoint.

[auth.gateway]
type = "jwt"
issuer = "https://auth.example.com"
audience = "hadrian"
jwks_url = "https://auth.example.com/.well-known/jwks.json"
jwks_refresh_secs = 3600
identity_claim = "sub"
org_claim = "org_id"
additional_claims = ["email", "name"]
allowed_algorithms = ["RS256", "ES256"]
SettingTypeDefaultDescription
issuerstringExpected iss claim. Tokens from other issuers are rejected.
audiencestring or arrayExpected aud claim. Can be a single value or list.
jwks_urlstringURL to fetch public keys for signature verification.
jwks_refresh_secsinteger3600How often to refresh the JWKS (seconds).
identity_claimstringsubClaim to use as the user's identity ID.
org_claimstringNoneClaim containing the user's organization ID (optional).
additional_claimsstring[][]Extra claims to extract and include in the request context.
allow_expiredbooleanfalseAccept expired tokens. For testing only.
allowed_algorithmsstring[]["RS256", "RS384", "RS512", ...]JWT signing algorithms to accept.

Allowed Algorithms

Always explicitly specify allowed_algorithms to prevent algorithm confusion attacks. Avoid HMAC algorithms (HS256, HS384, HS512) unless you control both signing and verification.

AlgorithmTypeSecurity LevelNotes
RS256RSARecommendedMost widely supported
RS384RSARecommendedStronger hash
RS512RSARecommendedStrongest RSA option
ES256ECDSARecommendedCompact signatures, P-256 curve
ES384ECDSARecommendedP-384 curve
PS256RSA-PSSRecommendedProbabilistic signatures
PS384RSA-PSSRecommendedProbabilistic signatures
PS512RSA-PSSRecommendedProbabilistic signatures
EdDSAEdDSARecommendedEd25519, modern and fast
HS256HMACUse with careSymmetric, requires shared secret
HS384HMACUse with careSymmetric, requires shared secret
HS512HMACUse with careSymmetric, requires shared secret

Multi-Auth (API Key + JWT)

Support both API keys and JWTs. The gateway uses format-based detection to determine which authentication method to use:

  • X-API-Key header: Always validated as an API key
  • Authorization: Bearer header: Uses format-based detection:
    • Tokens starting with the configured API key prefix (default: gw_) are validated as API keys
    • All other tokens are validated as JWTs

Providing both X-API-Key and Authorization headers simultaneously results in a 400 error (ambiguous credentials). Choose one authentication method per request.

[auth.gateway]
type = "multi"

[auth.gateway.api_key]
header_name = "X-API-Key"
key_prefix = "gw_"
cache_ttl_secs = 300

[auth.gateway.jwt]
issuer = "https://auth.example.com"
audience = "hadrian"
jwks_url = "https://auth.example.com/.well-known/jwks.json"

Request Examples:

# API key in X-API-Key header
curl -H "X-API-Key: gw_live_abc123..." https://gateway.example.com/v1/chat/completions

# API key in Authorization: Bearer header (format-based detection)
curl -H "Authorization: Bearer gw_live_abc123..." https://gateway.example.com/v1/chat/completions

# JWT in Authorization: Bearer header
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." https://gateway.example.com/v1/chat/completions

Admin Authentication

Configure how browser sessions for the admin UI are authenticated.

No Authentication

Allows anonymous access to the UI. Suitable for local development or internal tools.

[auth.admin]
type = "none"

Reverse Proxy Authentication

Trust identity headers set by an authenticating reverse proxy. Works with:

  • Cloudflare Access
  • oauth2-proxy
  • Tailscale
  • Authelia / Authentik
  • Keycloak Gatekeeper
  • Pomerium
[auth.admin]
type = "proxy_auth"
identity_header = "X-Forwarded-User"
email_header = "X-Forwarded-Email"
name_header = "X-Forwarded-Name"
groups_header = "X-Forwarded-Groups"
require_identity = true
SettingTypeDefaultDescription
identity_headerstringHeader containing the authenticated user's ID (required).
email_headerstringNoneHeader containing the user's email.
name_headerstringNoneHeader containing the user's display name.
groups_headerstringNoneHeader containing groups/roles (comma-separated or JSON array).
require_identitybooleantrueRequire identity headers on all requests. If false, anonymous access allowed.

Critical Security: Configure [server.trusted_proxies] to prevent header spoofing. Without this, attackers can forge identity headers and impersonate any user.

Cloudflare Access Example

[server.trusted_proxies]
cidrs = ["173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22"]

[auth.admin]
type = "proxy_auth"
identity_header = "Cf-Access-Authenticated-User-Email"
email_header = "Cf-Access-Authenticated-User-Email"

oauth2-proxy Example

[server.trusted_proxies]
cidrs = ["10.0.0.0/8"]

[auth.admin]
type = "proxy_auth"
identity_header = "X-Forwarded-User"
email_header = "X-Forwarded-Email"
groups_header = "X-Forwarded-Groups"

JWT Assertion (Optional)

For additional security, validate a signed JWT from the proxy:

[auth.admin]
type = "proxy_auth"
identity_header = "X-Forwarded-User"

[auth.admin.jwt_assertion]
header = "Cf-Access-Jwt-Assertion"
jwks_url = "https://your-team.cloudflareaccess.com/cdn-cgi/access/certs"
issuer = "https://your-team.cloudflareaccess.com"
audience = "your-audience-id"

Session Authentication

Session-based authentication using per-organization SSO. Each organization configures their own identity provider (OIDC or SAML) via the Admin UI, and users authenticate by discovering their organization via email domain.

[auth.admin]
type = "session"
cookie_name = "__gw_session"
duration_secs = 604800     # 7 days
secure = true
same_site = "lax"
secret = "${SESSION_SECRET}"
SettingTypeDefaultDescription
cookie_namestring__gw_sessionSession cookie name.
duration_secsinteger604800 (7 days)Session duration.
securebooleantrueHTTPS-only cookies. Set to false for local development over HTTP.
same_sitestringlaxSameSite attribute. Options: strict, lax, none.
secretstringAuto-generatedSecret for signing session cookies. Sessions are lost on restart if not set.

With session auth, SSO is configured per-organization via the Admin UI. Each organization can have their own OIDC or SAML identity provider. Users authenticate by entering their email, which discovers their organization's SSO configuration. See the SSO Admin Guide.

Login Flow

  1. User navigates to /auth/login?org=<org-slug> or uses email discovery via /auth/discover
  2. Gateway redirects to the organization's configured IdP
  3. After IdP authentication, callback creates a session cookie
  4. Subsequent requests are authenticated via the session cookie

Per-Organization SSO

SSO configurations are stored in the database and managed via the Admin API or UI:

  • POST /admin/v1/organizations/{org_slug}/sso-configs - Create SSO configuration
  • GET /admin/v1/organizations/{org_slug}/sso-configs - Get SSO configuration
  • PUT /admin/v1/organizations/{org_slug}/sso-configs - Update SSO configuration
  • DELETE /admin/v1/organizations/{org_slug}/sso-configs - Delete SSO configuration

Each SSO configuration includes:

  • Provider type (OIDC or SAML)
  • Issuer URL and discovery settings
  • Client credentials (stored encrypted)
  • Allowed email domains
  • JIT provisioning settings

Single-Org Membership

Users can only belong to one organization. When a user authenticates via an organization's SSO, they are associated with that organization. If a user tries to authenticate via a different organization's SSO, they will receive an error.

To move a user to a different organization, first remove them from their current organization.

Session Configuration

[auth.admin.session]
cookie_name = "__gw_session"
duration_secs = 604800     # 7 days
secure = true
same_site = "lax"
secret = "${SESSION_SECRET}"
SettingTypeDefaultDescription
cookie_namestring__gw_sessionSession cookie name.
duration_secsinteger604800 (7 days)Session duration.
securebooleantrueHTTPS-only cookies. Set to false for local development over HTTP.
same_sitestringlaxSameSite attribute. Options: strict, lax, none.
secretstringAuto-generatedSecret for signing session cookies. Sessions are lost on restart if not set.

If secret is not configured, a random key is generated on startup. This means sessions won't survive gateway restarts and won't work in multi-node deployments.

Auth States in Memory: During login flows, authentication state (PKCE verifiers, nonces, return URLs) is stored in memory by default. If the gateway restarts during an active login flow, users will see a "Session not found" error. For production deployments with high availability requirements, configure Redis as the session backend to persist auth states across restarts and share them across nodes.

JIT (Just-in-Time) Provisioning

Automatically create users and add them to organizations when they first authenticate via OIDC. This eliminates the need to pre-provision users before they can access the system.

[auth.admin.provisioning]
enabled = true
create_users = true
organization_id = "acme-corp"       # Required: org slug or UUID
default_team_id = "engineering"     # Optional: team slug or UUID
default_org_role = "member"
default_team_role = "member"
sync_attributes_on_login = true
# sync_memberships_on_login = true
# allowed_email_domains = ["example.com", "acme.org"]
SettingTypeDefaultDescription
enabledbooleanfalseEnable JIT provisioning.
create_usersbooleanfalseCreate users in the database on first login.
organization_idstringNoneOrganization slug or UUID to provision users into.
default_team_idstringNoneTeam slug or UUID to add users to (requires organization_id).
default_org_rolestringmemberRole assigned to users when added to organizations.
default_team_rolestringmemberRole assigned to users when added to teams.
allowed_email_domainsstring[][]Restrict provisioning to specific email domains. Empty = allow all.
sync_attributes_on_loginbooleanfalseUpdate user name/email from IdP on subsequent logins.
sync_memberships_on_loginbooleanfalseRemove memberships not in current provisioning config. See warning below.

The organization_id can be either a UUID or a slug. The organization must exist in the database before users can be provisioned into it. Create it via the Admin API or UI first.

Membership Sync Warning: When sync_memberships_on_login is enabled, users will lose access to organizations and teams not configured in the provisioning settings. All membership changes are logged in the audit log.

Example: Basic Setup

Provision all SSO users into a single organization:

[auth.admin.provisioning]
enabled = true
create_users = true
organization_id = "acme-corp"
default_org_role = "member"
sync_attributes_on_login = true

Example: With Default Team

Provision users into an organization and a default team:

[auth.admin.provisioning]
enabled = true
create_users = true
organization_id = "acme-corp"
default_team_id = "new-employees"
default_org_role = "member"
default_team_role = "member"

Example: Restrict by Email Domain

Only provision users from specific email domains:

[auth.admin.provisioning]
enabled = true
create_users = true
organization_id = "acme-corp"
allowed_email_domains = ["acme.com", "acme.org"]

SSO Group Mappings

Map IdP groups to Hadrian teams for automatic team assignment during JIT provisioning.

SSO group mappings are configured via the Admin UI, not in hadrian.toml. See the SSO Admin Guide for setup instructions.

Prerequisite: SSO group mappings require groups_claim to be configured in your OIDC settings. The IdP must include group membership in the ID token.

Each mapping specifies:

FieldDescription
IdP GroupThe exact group name from the identity provider (case-sensitive)
TeamThe Hadrian team to add the user to
RoleRole within the team (member, admin, etc.). Uses default_team_role if not specified.
PriorityPrecedence when multiple mappings target the same team (higher wins)

Priority Resolution

When a user belongs to multiple IdP groups that map to the same team, the mapping with the highest priority value determines the role:

  1. Higher priority value wins
  2. If priorities are equal, alphabetically earlier group name wins
  3. If still tied, earlier creation time wins

Each team appears at most once in the resolved memberships.

Example: Role Escalation by Group

IdP GroupTeamRolePriority
Engineersplatformmember0
SeniorEngineersplatformlead10
PlatformAdminsplatformadmin20

A user in both Engineers and SeniorEngineers groups receives the lead role (priority 10 beats priority 0). A user in all three groups receives the admin role (priority 20 wins).

Example: Multiple Team Membership

IdP GroupTeamRolePriority
Engineeringbackendmember0
Engineeringfrontendmember0
Engineeringplatformmember0

A user in the Engineering group is added to all three teams with member role.

Mappings without a team target org-level roles only and are skipped during team assignment. Use these for granting organization-wide permissions without team membership.

Admin API

Group mappings can also be managed programmatically via the Admin API:

  • GET /admin/v1/organizations/{org_slug}/sso-group-mappings - List mappings
  • POST /admin/v1/organizations/{org_slug}/sso-group-mappings - Create mapping
  • PATCH /admin/v1/organizations/{org_slug}/sso-group-mappings/{id} - Update mapping
  • DELETE /admin/v1/organizations/{org_slug}/sso-group-mappings/{id} - Delete mapping
  • POST /admin/v1/organizations/{org_slug}/sso-group-mappings/test - Test mappings against IdP groups

See the API Reference for full documentation.

Bulk Import/Export

For large mapping sets, use the import/export feature in the Admin UI:

  • Export: Download mappings as CSV or JSON for backup or migration
  • Import: Upload a JSON file with mappings and choose conflict resolution:
    • skip - Skip mappings that already exist
    • overwrite - Update existing mappings with imported values
    • error - Fail if any mapping already exists

SCIM Provisioning

SCIM (System for Cross-domain Identity Management) provides real-time user provisioning and deprovisioning from your identity provider. Unlike JIT provisioning which only triggers on login, SCIM syncs changes immediately.

SCIM is configured per-organization via the Admin UI, not in hadrian.toml. See the SCIM Provisioning Guide for setup instructions.

JIT vs SCIM Provisioning

CapabilityJITSCIM
User creationOn first loginImmediate
User deactivationOn next login attemptImmediate
API key revocationNever (manual)Immediate (configurable)
Group membership syncOn loginReal-time
Compliance (SOC 2, HIPAA)PartialFull

When to use each:

  • JIT only: Simple deployments where immediate deprovisioning isn't critical
  • SCIM only: Enterprise deployments requiring real-time access control
  • JIT + SCIM: SCIM for provisioning, JIT for fallback and attribute sync on login

Admin API

SCIM configuration is managed via the Admin API:

  • GET /admin/v1/organizations/{org_slug}/scim-configs - Get SCIM configuration
  • POST /admin/v1/organizations/{org_slug}/scim-configs - Create SCIM configuration
  • PUT /admin/v1/organizations/{org_slug}/scim-configs - Update SCIM configuration
  • DELETE /admin/v1/organizations/{org_slug}/scim-configs - Delete SCIM configuration
  • POST /admin/v1/organizations/{org_slug}/scim-configs/rotate-token - Rotate bearer token

RBAC Configuration

Role-Based Access Control uses CEL (Common Expression Language) policies for fine-grained authorization.

[auth.rbac]
enabled = true
default_effect = "deny"
role_claim = "roles"
org_claim = "groups"
team_claim = "groups"
project_claim = "project_ids"

[auth.rbac.role_mapping]
"Administrator" = "admin"
"Developer" = "user"

[auth.rbac.audit]
log_allowed = false
log_denied = true
SettingTypeDefaultDescription
enabledbooleanfalseEnable RBAC policy evaluation.
default_effectstringdenyDefault when no policy matches. Options: allow, deny.
role_claimstringrolesJWT claim containing user roles.
org_claimstringNoneJWT claim containing organization IDs.
team_claimstringNoneJWT claim containing team IDs.
project_claimstringNoneJWT claim containing project IDs.
role_mappingmap{}Map IdP role names to internal role names.

CEL Policy Structure

Policies are defined as an array of rules, each with a CEL condition:

[[auth.rbac.policies]]
name = "super-admin-full-access"
description = "Super admins have unrestricted access"
resource = "*"
action = "*"
condition = "'super_admin' in subject.roles"
effect = "allow"
priority = 100
FieldTypeDefaultDescription
namestringUnique policy identifier (required).
descriptionstringNoneHuman-readable description.
resourcestring*Resource type this policy applies to.
actionstring*Action this policy applies to.
conditionstringCEL expression that must evaluate to true (required).
effectstringallow or deny (required).
priorityinteger0Evaluation order. Higher = evaluated first. Ties: deny before allow.

CEL Variables

The following variables are available in CEL expressions:

Subject Variables

VariableTypeDescription
subject.user_idstringInternal user ID
subject.external_idstringIdP user ID (from identity claim)
subject.emailstringUser's email address
subject.rolesstring[]List of role names
subject.org_idsstring[]Organization IDs the user belongs to
subject.team_idsstring[]Team IDs the user belongs to
subject.project_idsstring[]Project IDs the user belongs to

Context Variables (All Endpoints)

VariableTypeDescription
context.resource_typestringResource being accessed
context.actionstringAction being performed
context.resource_idstringSpecific resource ID
context.org_idstringTarget organization ID
context.team_idstringTarget team ID
context.project_idstringTarget project ID

API Endpoint Variables

These variables are available when Gateway RBAC is enabled for /v1/* endpoints:

VariableTypeDescription
context.modelstringModel being requested (e.g., "gpt-4o", "claude-3-opus")
context.request.max_tokensintMaximum tokens requested
context.request.messages_countintNumber of messages in conversation
context.request.has_toolsboolWhether request includes tools/functions
context.request.has_file_searchboolWhether request includes file_search tool (RAG)
context.request.streamboolWhether streaming is requested
context.request.reasoning_effortstringReasoning effort: "none", "minimal", "low", "medium", "high"
context.request.response_formatstringOutput format: "text", "json_object", "json_schema"
context.request.temperaturefloatSampling temperature (0.0-2.0)
context.request.has_imagesboolWhether request contains image content (multimodal)
context.request.image_countintNumber of images to generate (image endpoints)
context.request.image_sizestringImage size: "256x256", "512x512", "1024x1024", etc.
context.request.image_qualitystringImage quality: "standard", "hd", "low", "medium", "high", "auto"
context.request.character_countintText length in characters (TTS endpoints)
context.request.voicestringTTS voice: "alloy", "echo", "fable", "onyx", "nova", "shimmer"
context.request.languagestringISO-639-1 language code (audio transcription)
context.now.hourintCurrent hour (0-23)
context.now.day_of_weekintDay of week (1=Monday, 7=Sunday)
context.now.timestampintUnix timestamp

Policy Examples

Deny Self-Deletion

[[auth.rbac.policies]]
name = "deny-self-delete"
description = "Users cannot delete themselves"
resource = "user"
action = "delete"
condition = "subject.user_id == context.resource_id"
effect = "deny"
priority = 200  # High priority, evaluated first

Org Admin Access

[[auth.rbac.policies]]
name = "org-admin-manage-org"
description = "Org admins can manage their own organization"
resource = "organization"
action = "*"
condition = "'org_admin' in subject.roles && context.org_id in subject.org_ids"
effect = "allow"
priority = 80

Cross-Org Isolation

[[auth.rbac.policies]]
name = "org-isolation"
description = "Users can only access resources in their organizations"
resource = "*"
action = "*"
condition = "context.org_id == null || context.org_id in subject.org_ids"
effect = "allow"
priority = 10

User Self-Service

[[auth.rbac.policies]]
name = "user-manage-own-api-keys"
description = "Users can manage their own API keys"
resource = "api_key"
action = "*"
condition = "context.owner_id == subject.user_id"
effect = "allow"
priority = 40

Gateway Authorization

Gateway RBAC extends CEL policies to /v1/* endpoints, enabling fine-grained control over model access, token limits, feature gating, and more.

Configuration

[auth.rbac.gateway]
enabled = true
default_effect = "allow"
SettingTypeDefaultDescription
enabledbooleanfalseEnable policy evaluation for API endpoints.
default_effectstringallowDefault when no policy matches. Options: allow (fail-open), deny.

Gateway RBAC defaults to enabled = false and default_effect = "allow" for backwards compatibility. Existing deployments continue to work without changes. Enable explicitly to enforce policies.

Authentication Requirements

Gateway RBAC policies require identity information (roles, org membership) from JWT tokens. Configure multi-auth to support both API keys and JWTs:

[auth.gateway]
type = "multi"

[auth.gateway.api_key]
key_prefix = "gw_"

[auth.gateway.jwt]
issuer = "https://auth.example.com"
audience = "hadrian-api"
jwks_url = "https://auth.example.com/.well-known/jwks.json"

API key authentication alone does not provide role information. Use JWT authentication or multi-auth for Gateway RBAC policies that check subject.roles.

Policy Examples

Model Access Control

Restrict expensive models to premium users:

[[auth.rbac.policies]]
name = "restrict-premium-models"
description = "Premium models require premium role"
resource = "model"
action = "use"
condition = """
  context.model != null &&
  context.model.startsWith('gpt-4') &&
  !('premium' in subject.roles)
"""
effect = "deny"
priority = 90

Allow admins unrestricted model access:

[[auth.rbac.policies]]
name = "admin-all-models"
description = "Admins can use any model"
resource = "model"
action = "use"
condition = "'admin' in subject.roles"
effect = "allow"
priority = 100

Token Limits

Enforce token limits by tier:

[[auth.rbac.policies]]
name = "basic-token-limit"
description = "Basic users limited to 1000 tokens"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.max_tokens > 1000 &&
  !('premium' in subject.roles)
"""
effect = "deny"
priority = 85

Feature Gating

Require specific roles for advanced features:

# Function calling requires tools_enabled role
[[auth.rbac.policies]]
name = "tools-feature-gate"
description = "Function calling requires tools_enabled role"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.has_tools &&
  !('tools_enabled' in subject.roles)
"""
effect = "deny"
priority = 85

# RAG/file search requires rag_enabled role
[[auth.rbac.policies]]
name = "rag-feature-gate"
description = "File search requires rag_enabled role"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.has_file_search &&
  !('rag_enabled' in subject.roles)
"""
effect = "deny"
priority = 85

Reasoning/Extended Thinking Control

Restrict high-effort reasoning to premium users:

[[auth.rbac.policies]]
name = "reasoning-premium"
description = "High reasoning effort requires premium role"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.reasoning_effort == 'high' &&
  !('premium' in subject.roles)
"""
effect = "deny"
priority = 85

Vision/Multimodal Control

Restrict image input to users with vision access:

[[auth.rbac.policies]]
name = "vision-feature-gate"
description = "Image input requires vision role"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.has_images &&
  !('vision' in subject.roles)
"""
effect = "deny"
priority = 85

Image Generation Limits

Control image generation by count and quality:

# Limit image count for free tier
[[auth.rbac.policies]]
name = "image-count-limit"
description = "Free tier limited to 2 images per request"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.image_count > 2 &&
  !('premium' in subject.roles)
"""
effect = "deny"
priority = 85

# Restrict HD quality to premium
[[auth.rbac.policies]]
name = "hd-images-premium"
description = "HD image quality requires premium role"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.image_quality == 'hd' &&
  !('premium' in subject.roles)
"""
effect = "deny"
priority = 85

Audio TTS Limits

Limit text-to-speech character count:

[[auth.rbac.policies]]
name = "tts-character-limit"
description = "TTS limited to 1000 characters for basic tier"
resource = "model"
action = "use"
condition = """
  context.request != null &&
  context.request.character_count > 1000 &&
  !('tts_extended' in subject.roles)
"""
effect = "deny"
priority = 85

Time-Based Access

Restrict API access to business hours:

[[auth.rbac.policies]]
name = "business-hours-only"
description = "API access restricted to business hours for non-admins"
resource = "model"
action = "use"
condition = """
  !('admin' in subject.roles) &&
  (context.now.hour < 9 || context.now.hour > 17)
"""
effect = "deny"
priority = 80

Bootstrap Configuration

Bootstrap mode enables initial setup when no users exist in the database. This solves the chicken-and-egg problem of needing admin access to configure SSO before any users can authenticate.

Bootstrap API Key

For automated deployments and SSO setup, use a pre-shared API key:

[auth.bootstrap]
# Pre-shared API key for initial setup (only works when no users exist)
api_key = "${HADRIAN_BOOTSTRAP_KEY}"

# Domains automatically verified when SSO config is created (skips DNS verification)
auto_verify_domains = ["acme.com", "acme.io"]
SettingTypeDescription
api_keystringPre-shared key for admin access before first user exists
auto_verify_domainsstring[]Email domains auto-verified when SSO config is created

The bootstrap API key:

  • Only works when the database has no users (organizations can exist)
  • Uses the reserved _system_bootstrap role (cannot be assigned by IdPs)
  • Automatically becomes inactive after the first user is provisioned via IdP login

For SAML or OIDC initial setup, see the SAML Admin Guide for a complete walkthrough using the bootstrap API key.

Identity-Based Bootstrap

For scenarios where users will authenticate via IdP before accessing the Admin UI:

[auth.bootstrap]
admin_identities = ["alice@example.com", "bob@example.com"]

[auth.bootstrap.initial_org]
slug = "acme-corp"
name = "Acme Corporation"
admin_identities = ["alice@example.com"]
SettingTypeDescription
admin_identitiesstring[]External IDs granted system admin role
initial_org.slugstringURL-safe organization identifier
initial_org.namestringDisplay name for the organization
initial_org.admin_identitiesstring[]External IDs added as organization admins

Identity-based bootstrap runs only once on first startup with an empty database. Existing users and organizations are not modified.

Emergency Access Configuration

Emergency access provides break-glass admin access when SSO is unavailable. Unlike bootstrap mode, emergency access remains available indefinitely (when enabled) and is designed for disaster recovery scenarios.

[auth.emergency]
enabled = true
allowed_ips = ["10.0.0.0/8"]  # Optional: restrict to admin network

[[auth.emergency.accounts]]
id = "emergency-admin-1"
name = "Primary Emergency Admin"
key = "${EMERGENCY_KEY_1}"
email = "admin@company.com"
roles = ["_emergency_admin", "super_admin"]

[auth.emergency.rate_limit]
max_attempts = 5
window_secs = 900
lockout_secs = 3600
SettingTypeDefaultDescription
enabledbooleanfalseEnable emergency access
allowed_ipsstring[][]Global IP allowlist (CIDR notation)
accounts[].idstring-Unique identifier for audit logs
accounts[].namestring-Human-readable account name
accounts[].keystring-Emergency access key (secret)
accounts[].emailstring-Email for audit logging
accounts[].rolesstring[][]Roles granted on authentication
accounts[].allowed_ipsstring[][]Per-account IP restrictions (CIDR)
rate_limit.max_attemptsu325Failed attempts before lockout
rate_limit.window_secsu64900Time window for counting attempts (15 min)
rate_limit.lockout_secsu643600Lockout duration after max attempts (1 hour)

Use emergency keys via the X-Emergency-Key header or Authorization: EmergencyKey <key>:

curl -H "X-Emergency-Key: $EMERGENCY_KEY" https://gateway.example.com/admin/v1/organizations

For detailed usage instructions and the IdP outage runbook, see Emergency Access.

Complete Examples

Development (No Auth)

[auth.gateway]
type = "none"

# No UI auth config = anonymous access

Production API Keys Only

[auth.gateway]
type = "api_key"
header_name = "X-API-Key"
key_prefix = "gw_"
cache_ttl_secs = 300

Keycloak OIDC with JIT Provisioning

[auth.admin]
type = "oidc"
issuer = "https://keycloak.example.com/realms/hadrian"
client_id = "hadrian"
client_secret = "${OIDC_CLIENT_SECRET}"
redirect_uri = "https://gateway.example.com/auth/callback"
identity_claim = "preferred_username"
groups_claim = "groups"  # Required for SSO group mappings

[auth.admin.session]
secure = true
secret = "${SESSION_SECRET}"

# JIT provisioning - auto-create users and add to organization
[auth.admin.provisioning]
enabled = true
create_users = true
organization_id = "acme-corp"
default_org_role = "member"
sync_attributes_on_login = true

[auth.gateway]
type = "api_key"
key_prefix = "gw_"
cache_ttl_secs = 300

[auth.rbac]
enabled = true
default_effect = "deny"
role_claim = "roles"
org_claim = "groups"

[[auth.rbac.policies]]
name = "super-admin"
resource = "*"
action = "*"
condition = "'super_admin' in subject.roles"
effect = "allow"
priority = 100

[[auth.rbac.policies]]
name = "org-member-access"
resource = "*"
action = "*"
condition = "context.org_id in subject.org_ids"
effect = "allow"
priority = 50

After deploying this configuration, use the Admin UI to set up SSO group mappings that map Keycloak groups to Hadrian teams.

Cloudflare Access + API Keys

[server.trusted_proxies]
cidrs = ["173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22"]

[auth.admin]
type = "proxy_auth"
identity_header = "Cf-Access-Authenticated-User-Email"
email_header = "Cf-Access-Authenticated-User-Email"

[auth.admin.jwt_assertion]
header = "Cf-Access-Jwt-Assertion"
jwks_url = "https://your-team.cloudflareaccess.com/cdn-cgi/access/certs"
issuer = "https://your-team.cloudflareaccess.com"
audience = "your-app-audience-tag"

[auth.gateway]
type = "api_key"
key_prefix = "gw_"

Multi-Tenant with Full RBAC

[auth.admin]
type = "oidc"
issuer = "https://auth.example.com"
client_id = "hadrian"
client_secret = "${OIDC_CLIENT_SECRET}"
redirect_uri = "https://gateway.example.com/auth/callback"
groups_claim = "groups"

[auth.gateway]
type = "multi"

[auth.gateway.api_key]
key_prefix = "gw_"
cache_ttl_secs = 300

[auth.gateway.jwt]
issuer = "https://auth.example.com"
audience = "hadrian-api"
jwks_url = "https://auth.example.com/.well-known/jwks.json"

[auth.rbac]
enabled = true
default_effect = "deny"
role_claim = "roles"
org_claim = "groups"
team_claim = "groups"

[auth.rbac.audit]
log_allowed = false
log_denied = true

# Deny policies (high priority)
[[auth.rbac.policies]]
name = "deny-self-delete"
resource = "user"
action = "delete"
condition = "subject.user_id == context.resource_id"
effect = "deny"
priority = 200

# Super admin (priority 100)
[[auth.rbac.policies]]
name = "super-admin"
resource = "*"
action = "*"
condition = "'super_admin' in subject.roles"
effect = "allow"
priority = 100

# Org admin (priority 80)
[[auth.rbac.policies]]
name = "org-admin"
resource = "*"
action = "*"
condition = "'org_admin' in subject.roles && context.org_id in subject.org_ids"
effect = "allow"
priority = 80

# Team admin (priority 60)
[[auth.rbac.policies]]
name = "team-admin"
resource = "*"
action = "*"
condition = "'team_admin' in subject.roles && context.team_id in subject.team_ids"
effect = "allow"
priority = 60

# User self-service (priority 40)
[[auth.rbac.policies]]
name = "user-own-resources"
resource = "*"
action = "*"
condition = "context.owner_id == subject.user_id"
effect = "allow"
priority = 40

# Read access for org members (priority 20)
[[auth.rbac.policies]]
name = "org-member-read"
resource = "*"
action = "read"
condition = "context.org_id in subject.org_ids"
effect = "allow"
priority = 20

[auth.bootstrap]
admin_identities = ["admin@example.com"]

[auth.bootstrap.initial_org]
slug = "default"
name = "Default Organization"
admin_identities = ["admin@example.com"]

On this page