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

Authentication Configuration

Auth modes, API keys, IAP, IdP sessions, and CEL-based RBAC policies

Configure authentication and authorization for Hadrian in hadrian.toml using the [auth] section. For conceptual overviews and guides, see:

Auth Mode

Hadrian uses a single [auth.mode] to control authentication for both the API and the web UI. Select exactly one mode.

ModetypeAPI KeysIdP SessionsIdentity HeadersUse Case
NonenoneNoNoNoLocal development
API Keyapi_keyYesNoNoProgrammatic access only
IdPidpYesYesNoFull deployment with SSO + API
IAPiapYesNoYesBehind an identity-aware proxy

None

Allow all requests without credentials. Only suitable for local development.

[auth.mode]
type = "none"

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

API Key

Validate API keys stored in the database. Keys are created via the Admin API or UI. No browser-based login is available in this mode.

[auth.mode]
type = "api_key"

API key settings (header name, prefix, caching) are configured separately in [auth.api_key]. See API Key Settings.

IdP

Full identity provider integration with per-organization SSO. Supports both API keys for programmatic access and browser sessions for the web UI. Each organization configures its own OIDC or SAML provider via the Admin UI.

[auth.mode]
type = "idp"

Session settings (cookie name, duration, secret) are configured in [auth.session]. See Session Configuration.

When a bearer token is presented, Hadrian uses format-based detection to determine whether it is an API key or a JWT:

  • X-API-Key header: Always validated as an API key
  • Authorization: Bearer header: Tokens starting with the configured API key prefix (default: gw_) are validated as API keys; all other tokens are validated as JWTs via per-org SSO configuration

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

# 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 (routed to org's IdP for validation)
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." https://gateway.example.com/v1/chat/completions

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

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

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 receive an error.

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

IAP (Identity-Aware Proxy)

Trust identity headers set by an authenticating reverse proxy. Also supports API keys for programmatic access. Works with:

  • Cloudflare Access
  • oauth2-proxy
  • Tailscale
  • Authelia / Authentik
  • Keycloak Gatekeeper
  • Pomerium
[auth.mode]
type = "iap"
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_headerstring---Header 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.mode]
type = "iap"
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.mode]
type = "iap"
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. Configure jwt_assertion inside [auth.mode]:

[auth.mode]
type = "iap"
identity_header = "X-Forwarded-User"

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

API Key Settings

Configure shared API key behavior used by api_key, idp, and iap modes.

[auth.api_key]
header_name = "X-API-Key"
key_prefix = "gw_"
generation_prefix = "gw_live_"
hash_algorithm = "sha256"
cache_ttl_secs = 300
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_secsinteger300Cache 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.

Per-Org JWT Routing

When using idp mode, JWT validation is handled per-organization through SSO configurations -- there is no global JWT config in hadrian.toml. Each organization's SSO config provides the issuer, audience, and JWKS URL for validating JWTs from that organization's identity provider.

When a JWT is presented on a /v1/* endpoint:

  1. Hadrian decodes the iss (issuer) claim from the token
  2. Looks up the organization whose SSO config matches that issuer
  3. Validates the token against that organization's JWKS
  4. Associates the request with the matched organization

This enables true multi-tenant deployments where each organization uses a different identity provider without any global JWT configuration.

Configure each organization's SSO via the Admin UI or API. See the SSO Admin Guide for setup instructions.

Session Configuration

Configure browser session settings for idp mode. Sessions are created after successful SSO login.

[auth.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 Provisioning

JIT (Just-in-Time) provisioning automatically creates users and adds them to organizations when they first authenticate via SSO. JIT provisioning is configured per-organization via the Admin UI or Admin API, not in hadrian.toml.

See the SSO Admin Guide for instructions on configuring JIT provisioning per organization.

Each organization's JIT provisioning settings include:

SettingTypeDefaultDescription
enabledbooleanfalseEnable JIT provisioning for this organization.
create_usersbooleanfalseCreate users in the database on first login.
default_team_idstringNoneTeam to add users to on first login.
default_org_rolestringmemberRole assigned when added to the organization.
default_team_rolestringmemberRole assigned when added to the default team.
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.

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.

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
namestring---Unique policy identifier (required).
descriptionstringNoneHuman-readable description.
resourcestring*Resource type this policy applies to.
actionstring*Action this policy applies to.
conditionstring---CEL expression that must evaluate to true (required).
effectstring---allow 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. Use idp mode to support both API keys and JWTs:

[auth.mode]
type = "idp"

[auth.api_key]
key_prefix = "gw_"

API key authentication alone does not provide role information. Use JWT authentication (via per-org SSO) 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@acme.com", "bob@acme.com"]

[auth.bootstrap.initial_org]
slug = "acme-corp"
name = "Acme Corporation"
admin_identities = ["alice@acme.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.

Bootstrap API Key Generation

Create an API key during bootstrap for programmatic access:

[auth.bootstrap]
api_key = "${HADRIAN_BOOTSTRAP_KEY}"
auto_verify_domains = ["acme.com"]

[auth.bootstrap.initial_org]
slug = "acme-corp"
name = "Acme Corporation"
admin_identities = ["admin@acme.com"]

[auth.bootstrap.initial_api_key]
name = "production-api-key"
SettingTypeDescription
initial_api_key.namestringName for the auto-created API key (scoped to the initial org)

The generated API key is printed to stdout on first creation. Subsequent runs skip creation if a key with the same name already exists.

Bootstrap SSO Configuration

Pre-configure SSO for the initial organization directly in the config file, avoiding manual Admin UI setup:

[auth.bootstrap.initial_org]
slug = "acme-corp"
name = "Acme Corporation"
admin_identities = ["admin@acme.com"]

[auth.bootstrap.initial_org.sso]
provider_type = "oidc"
issuer = "https://accounts.google.com"
client_id = "${OIDC_CLIENT_ID}"
client_secret = "${OIDC_CLIENT_SECRET}"
redirect_uri = "https://gateway.example.com/auth/callback"
discovery_url = "https://accounts.google.com/.well-known/openid-configuration"
allowed_email_domains = ["acme.com"]
SettingTypeDescription
sso.provider_typestring"oidc" or "saml"
sso.issuerstringIdP issuer URL
sso.client_idstringOAuth client ID
sso.client_secretstringOAuth client secret (stored in secrets manager)
sso.redirect_uristringOAuth redirect URI
sso.discovery_urlstringOIDC discovery endpoint (optional)
sso.allowed_email_domainsstring[]Restrict SSO login to these email domains

Domains listed in both auto_verify_domains and sso.allowed_email_domains are automatically verified during bootstrap.

Bootstrap CLI

Run bootstrap as a standalone CLI command instead of at server startup. This is the recommended approach for GitOps and IaC workflows:

# Run bootstrap against the database
hadrian bootstrap --config hadrian.toml

# Preview what would be created without making changes
hadrian bootstrap --config hadrian.toml --dry-run

The hadrian bootstrap command:

  • Connects directly to the database (no HTTP server started)
  • Runs pending migrations before bootstrapping
  • Creates the initial organization, SSO config, and API key as specified in [auth.bootstrap]
  • Is fully idempotent — safe to run repeatedly (skips resources that already exist)
  • Prints the generated API key to stdout on first creation (pipe to a secret manager or file)
# Example: capture the generated API key
API_KEY=$(hadrian bootstrap --config hadrian.toml 2>/dev/null)
echo "Generated key: $API_KEY"

Use --dry-run to verify your bootstrap configuration before applying it to a production database.

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@acme.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.mode]
type = "none"

Production API Keys Only

[auth.mode]
type = "api_key"

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

IdP with Per-Org SSO

Each organization configures their own OIDC or SAML provider via the Admin UI. API keys provide programmatic access.

[auth.mode]
type = "idp"

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

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

[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

[auth.bootstrap]
api_key = "${HADRIAN_BOOTSTRAP_KEY}"
auto_verify_domains = ["acme.com"]

After deploying, use the bootstrap API key to create organizations and configure SSO. Once SSO is active, use the Admin UI to set up group mappings that map IdP 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.mode]
type = "iap"
identity_header = "Cf-Access-Authenticated-User-Email"
email_header = "Cf-Access-Authenticated-User-Email"

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

[auth.api_key]
key_prefix = "gw_"

Multi-Tenant with Full RBAC

[auth.mode]
type = "idp"

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

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

[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.rbac.gateway]
enabled = true
default_effect = "allow"

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

[auth.bootstrap.initial_org]
slug = "acme-corp"
name = "Acme Corporation"
admin_identities = ["admin@acme.com"]

[auth.bootstrap.initial_api_key]
name = "admin-key"

Multi-Org with Per-IdP API Authentication

Each organization configures their own identity provider via the Admin UI. No global JWT config is needed -- per-org SSO configs automatically enable JWT validation on /v1/* endpoints.

[auth.mode]
type = "idp"

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

# No global JWT config -- per-org SSO configs provide JWT validation for each org's IdP.

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

[auth.rbac]
enabled = true
default_effect = "deny"

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

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

[auth.bootstrap]
api_key = "${HADRIAN_BOOTSTRAP_KEY}"
auto_verify_domains = ["acme.com", "globex.io"]

After deploying, use the bootstrap API key to create organizations and their SSO configs. Once SSO is configured, each org's users can authenticate with both the web UI (via SSO login) and the API (via JWT tokens from their org's IdP).

On this page