Configuration
TL;DR — AuthPlane loads config in three layers, highest priority last: built-in defaults → YAML file →
AUTHPLANE_*env vars. Everything you need to boot is optional (SQLite + auto-generated keys + no config file); everything you need for production is explicit and validated at boot. This page lists every section, every key, and every env var.
Precedence
- Defaults — sensible values for local development, built into the binary.
- YAML file —
authserver serve --config config.yaml. - Environment variables —
AUTHPLANE_*prefix; override YAML.
Env vars win because you want the same YAML file across environments with per-deployment overrides for secrets and issuer URLs.
Comma-separated list variables (like AUTHPLANE_OIDC_SCOPES=openid,email,profile) split on commas. Duration variables accept Go duration strings (10s, 5m, 1h, 24h). Bool variables accept true, false, 1, 0.
Minimal dev config
Zero config works — the binary starts on :9000 with SQLite, auto-generated signing keys, and no resource. The moment you need to accept a real MCP client, add a resource:
resources:
- slug: my-mcp-server
uri: http://localhost:3000/
backend_kind: mint
display_name: My MCP Server
scopes:
- name: tools/echo
description: Echo tool
Or a single Mint resource via env vars:
export AUTHPLANE_RESOURCE_URI=http://localhost:3000/
export AUTHPLANE_RESOURCE_SCOPES=tools/echo
authserver serve
Minimal production config
Boot validation enforces these when server.issuer is not localhost:
server:
issuer: https://auth.example.com
session:
secret: "generate-a-32-byte-random-string" # openssl rand -hex 32
secure: true
admin:
api_key: "generate-a-secure-api-key" # openssl rand -hex 32
Anything missing here fails boot with a structured error — you cannot accidentally deploy a non-local AuthPlane with a random ephemeral session secret.
server
Public HTTP server settings.
server:
issuer: http://localhost:9000 # Public URL — appears in AS metadata and JWT `iss`
address: ":9000" # Listen address
read_timeout: 30s
write_timeout: 30s
idle_timeout: 120s
shutdown_wait: 10s # Graceful shutdown drain
allowed_origins: [] # CORS allowlist for browser MCP clients
issuer appears in the /.well-known/oauth-authorization-server metadata and as the iss claim in every JWT. MCP clients use it for discovery. Must match the URL clients use to reach AuthPlane exactly — no trailing slash.
allowed_origins — empty by default. Browser-based MCP clients (MCP Inspector, Claude Desktop web) fail CORS preflight silently without it. For local dev set AUTHPLANE_SERVER_ALLOWED_ORIGINS=*; for production set an explicit allowlist. Server-to-server-only deployments may leave this empty.
storage
Persistence layer. Both backends implement the same interfaces — choose by driver.
storage:
driver: sqlite # "sqlite" or "postgres"
sqlite:
path: data/authserver.db
wal: true # WAL mode for concurrent reads
postgres:
dsn: "postgres://user:pass@localhost:5432/authserver?sslmode=require"
max_conns: 25
min_conns: 5
max_conn_lifetime: 1h
max_conn_idle_time: 30m
SQLite is default and recommended for single-instance deployments. Pure Go via modernc.org/sqlite (no CGO). WAL mode enables concurrent reads while writes serialize. DB file is created automatically on first boot.
PostgreSQL is required for multi-instance (HA) deployments. Uses LISTEN/NOTIFY for cross-instance signing-key rotation. Run authserver migrate before first start.
signing
JWT signing keys and algorithms.
signing:
algorithm: ES256 # "ES256" (default) or "RS256"
key_store: keyfile # "keyfile", "postgres_key", or "vault_transit"
key_path: data/keys # Keyfile store: directory for PEM files
vault_transit: # Only when key_store: vault_transit
address: https://vault:8200
token: "" # Static token (mutually exclusive with approle)
mount: transit
key_name: authserver-signing
timeout: 10s
approle:
role_id: ""
secret_id: ""
mount: approle
Algorithms — ES256 (ECDSA P-256) is default: compact tokens, fast verification. RS256 (RSA 2048) for environments that require it (legacy IDPs, HSM constraints).
Key stores — keyfile writes PEM keys to key_path. postgres_key (HA-safe) stores keys in the storage DB with encryption at rest — required when running multiple instances so key rotation propagates via LISTEN/NOTIFY. Note: postgres (without _key) fails boot; the suffix disambiguates from the storage driver of the same name. vault_transit delegates signing to HashiCorp Vault Transit — private keys never leave Vault. See Security: Key management and Operate: Vault Transit.
Key rotation — authserver admin key rotate generates a new signing key. The old key stays in the JWKS until it expires so in-flight tokens keep verifying.
dcr — Dynamic Client Registration (RFC 7591)
dcr:
mode: open # "open", "approved_redirects", or "admin_only"
approved_redirects: # Used when mode: approved_redirects
- http://localhost:*
- https://inspector.mcp.garden/*
rate_limit: 10 # Registrations per second
rate_limit_burst: 20
default_token_expiry: 15m
default_refresh_expiry: 168h # 7 days
Modes:
open— any client can register with any redirect URI. Development only.approved_redirects— anyone can register, but the redirect URI must match a pattern inapproved_redirects. Balanced for production with well-known MCP clients.admin_only— clients must be pre-registered via the Admin API or CLI. Hardened default for internal-only deployments.
Rate limits apply per-IP to the /oauth/register endpoint. Defaults are conservative — bump for automated CI environments.
cimd — Client ID Metadata Document
Auto-registration via URL client_id (draft-ietf-oauth-client-id-metadata-document).
cimd:
enabled: true
require_https: true # Require HTTPS for CIMD document URLs
cache_ttl: 1h # Cache fetched documents for this long
fetch_timeout: 10s # HTTP timeout for fetching CIMD docs
When a client presents a URL as its client_id, AuthPlane fetches and validates the metadata document at that URL. Set require_https: false only for local dev with http:// CIMD URLs.
session
Cookie-based session for the login and consent pages.
session:
cookie_name: authserver_session
max_age: 24h
secure: false # Must be true in production
same_site: lax # "lax", "strict", or "none"
secret: "" # Required in production (32+ bytes)
The session secret signs and encrypts session cookies. For localhost dev, a random ephemeral secret is generated if none is set — sessions don’t survive restart. For production, set a stable 32+ byte value (openssl rand -hex 32).
rate_limit
Global rate limiting + brute-force protection on auth endpoints.
rate_limit:
enabled: true
requests_per_second: 100 # Global RPS limit
burst: 200 # Burst allowance
auth_fail_max: 10 # Max failed auth attempts per IP
auth_fail_window: 10m # Window for counting failures
auth_lockout: 15m # Lockout duration after exceeding max
Token-bucket global limit plus per-IP tracking of failed logins. Lockout applies to the /login endpoint only — OAuth endpoints stay open.
oauth
oauth:
require_scope: true # Require scope parameter in authorize requests
When true (default), /oauth/authorize requests without a scope parameter are rejected with invalid_scope per RFC 6749 §3.3. When false, missing scope defaults to all registered scopes for the target resource — useful for MCP clients (like Claude Code at v0.1.x) that don’t send scope on /authorize. See ADR-012.
Optional grants — disabled by default
Three grants are off by default. Turn them on explicitly:
client_credentials:
enabled: false # RFC 6749 §4.4
token_expiry: 1h
dpop:
enabled: false # RFC 9449
nonce_ttl: 60s
proof_lifetime: 60s
require_nonce: false
token_exchange:
enabled: false # RFC 8693
allow_self_exchange: false
max_chain_depth: 5
token_expiry: 1h
client_credentials— required for machine-to-machine flows and any SDK that performs server-to-server introspection. See Concepts: Grants & flows and Topologies: Backend service + MCP.dpop— sender-constrained tokens. Additive: clients that don’t send a DPoP header still get bearer tokens (unlessrequire_nonce: truelocks it down). See Concepts: DPoP.token_exchange— powers delegation, Broker resources, and Cross-App Access. Cross-client authorization is enforced per-resource viapolicy.exchange.allowed_client_idson each registered resource. See Concepts: Delegation & act-chain.
XAA and JWT Bearer — YAML-only at v0.1.x
Enterprise-Managed Auth (Cross-App Access) is configured via YAML at v0.1.x. Env-var overrides are not exposed for this section.
xaa:
enabled: false
jwks_cache_ttl: 1h
token_expiry: 1h
max_assertion_age: 5m
subject_mode: auto_map # "auto_map" or "strict"
require_resource: true # reject assertions that don't specify `resource=`
Trusted IdPs, policies, and subject mappings are managed via the Admin REST API only — POST /admin/idps, POST /admin/xaa/policies, POST /admin/xaa/subject-mappings. There is no authserver admin xaa … CLI subcommand. See Concepts: Cross-App Access and Guides: Enterprise-Managed Auth.
agents
agents:
enable_jwks_listing: false # Publish agent JWK sets on /.well-known/agents.json
Agent identity claims (agent_id, agent_chain) are emitted on every token by default. This flag controls the optional publishing of agent JWK sets for third-party verification.
admin
Admin API + Admin UI, on a separate port from public OAuth.
admin:
enabled: true
address: ":9001" # Separate port
api_key: "" # Required in production
Hosts both the REST API under /admin/* and the built-in React Admin UI at /admin/ui/. Authenticate API requests with Authorization: Bearer <api_key>. The UI uses the same key entered in-browser and stored in sessionStorage. Never expose :9001 to the public internet — keep it on an internal network or behind a firewall/NetworkPolicy.
oidc — Upstream OIDC Federation
oidc:
enabled: false
issuer: https://accounts.google.com
client_id: ""
client_secret: ""
display_name: Google # Button text on login page
scopes: [openid, email, profile]
redirect_uri: https://auth.example.com/oidc/callback
show_local_login: true # Show password form alongside OIDC button
include_groups_scope: true # Include "groups" scope if upstream supports it
connector_id: "" # Dex connector_id parameter (optional)
Validation: oidc.issuer and oidc.redirect_uri must use HTTPS in production; client_id, client_secret, and redirect_uri are required when enabled. See Guides: Federate to your IdP.
resources
Every MCP server (Mint) and every upstream provider target (Broker) is a resources entry.
resources:
- slug: my-mcp-server
uri: http://mcp-server:3000/mcp # MCP canonical form — no trailing slash
backend_kind: mint
display_name: My MCP Server
scopes:
- name: tools/echo
description: Echo a message
- name: tools/query_database
description: Query the database
policy:
exchange:
allowed_client_ids: [] # Empty = any consented client
runtime:
client_ids: [] # Empty = no runtime binding
connect:
allowed_return_urls: [] # Overrides connect.allowed_return_urls
Env vars support a single Mint resource. For multiple resources or any Broker, use YAML or the Admin API.
uri must match byte-for-byte what your MCP server publishes in its Protected Resource Metadata (RFC 9728). Compliant MCP clients echo the PRM resource field verbatim on /oauth/authorize and /oauth/token; AuthPlane uses exact string matching for audience validation. A one-character mismatch silently rejects every token. Use the MCP spec canonical form (no trailing slash, lowercase host, no default port). Watch for scheme/host case, default ports, percent-encoding — see Guides: Connect an MCP client.
backend_kind — mint (AS issues its own JWT) or broker (AS vends an upstream provider token via RFC 8693). See Concepts: Architecture.
policy.exchange.allowed_client_ids — per-resource replacement for the legacy global cross-client allowlist. Empty means any consented client can exchange into this resource.
policy.runtime.client_ids — per-resource runtime binding: whitelist of clients whose tokens are accepted at THIS resource. See Guides: Runtime client binding.
observability
Logs, traces, metrics.
observability:
logging:
level: info # debug | info | warn | error
format: json # json | text
add_source: false # Include file:line in log records
outputs:
stdout: true
otel: false
otel_endpoint: "" # OTLP gRPC endpoint
insecure: false # Allow plaintext gRPC
tracing:
enabled: false
endpoint: "" # OTLP gRPC endpoint (e.g. localhost:4317)
insecure: false
sample_rate: 1.0 # 0.0 to 1.0
metrics:
provider: prometheus # prometheus | otel | both | none
path: /metrics # Prometheus scrape endpoint
otel_endpoint: "" # OTLP endpoint when provider=otel or both
insecure: false
Full metric catalog and Prometheus setup in Guides: Monitoring and Reference: Metrics & CLI.
data_encryption
Encrypts sensitive data at rest — upstream refresh grants in broker_grants, and (when Postgres key store is enabled) signing keys.
data_encryption:
driver: aes_master # "aes_master" or "vault_transit_encrypt"
aes_master:
key_env: AUTHPLANE_VAULT_ENCRYPTION_KEY # Env var containing hex-encoded 256-bit key
old_key_env: AUTHPLANE_VAULT_OLD_KEY # Previous key for rotation (optional)
vault_transit_encrypt:
address: https://vault:8200
auth_method: token # "token" or "approle"
token_env: VAULT_TOKEN
mount_path: transit
key_name: authserver-data
approle:
role_id_env: VAULT_APPROLE_ROLE_ID
secret_id_env: VAULT_APPROLE_SECRET_ID
AES master key — local AES-256-GCM with HKDF-derived per-purpose subkeys. Generate: openssl rand -hex 32.
Vault Transit encrypt — delegates encryption to HashiCorp Vault Transit; plaintext never touches disk. Required for compliance environments.
broker_providers
Upstream OAuth / API-key / service-account providers that Broker resources reference. See Concepts: Token Vault.
broker_providers:
- slug: github
display_name: GitHub
protocol: oauth
config_data:
client_id: "your-github-app-client-id"
client_secret_ref: "CONNECTOR_GITHUB_SECRET"
authorize_url: "https://github.com/login/oauth/authorize"
token_url: "https://github.com/login/oauth/access_token"
response_format: standard
- slug: slack
display_name: Slack
protocol: oauth
config_data:
client_id: "your-slack-app-client-id"
client_secret_ref: "CONNECTOR_SLACK_SECRET"
authorize_url: "https://slack.com/oauth/v2/authorize"
token_url: "https://slack.com/api/oauth.v2.access"
response_format: form
No env-var shortcut — broker providers are managed via YAML or the Admin API/CLI (authserver admin provider create). The slug is the public ID; Broker resources reference it via broker_provider_slug.
protocol — one of oauth, apikey, service_account. Each dispatches to a different adapter under internal/brokerproto/.
Default scopes live on the Broker Resource row, not the provider — one provider can back multiple resources with different scope sets.
connect
The user-facing OAuth flow to grant your MCP server upstream access.
connect:
state_secret: "generate-a-32-byte-random-string" # HMAC key for state tokens
allowed_return_urls: # Global default (per-resource override available)
- http://localhost:*
- https://myapp.example.com/*
redirect_base_url: http://localhost:9000 # Base URL for OAuth callbacks
state_secret — HMAC key for signing state tokens during the connect flow. Must be at least 32 characters (openssl rand -hex 32). Different key from session.secret.
allowed_return_urls — global default whitelist for connect-flow return URLs. Each resource can override via resources[].policy.connect.allowed_return_urls for finer control.
Validation rules enforced at boot
Boot fails with a structured error if any of these are violated:
session.secretrequired whenserver.issueris not localhostsession.secure: truerequired whenserver.issueris not localhostadmin.api_keyrequired whenadmin.enabled: trueandserver.issueris not localhostoidc.client_id+client_secret+redirect_uriall required whenoidc.enabled: trueoidc.issuer+oidc.redirect_urimust use HTTPS in production- Every enabled feature’s required-config combination is validated up-front (partial config → boot fails at startup, not at first request)
resources[].urimust be a well-formed URL
Config precedence in practice
Debug your effective config by comparing all three sources:
# See defaults + YAML
$ authserver serve --config config.yaml --dry-run
# See what env vars are set
$ env | grep AUTHPLANE_
# See the final self-check report in the boot log
$ authserver serve --config config.yaml 2>&1 | grep -A20 "feature self-check"
Every enabled feature logs a one-liner at boot indicating what config it picked up. Misconfigurations are fatal — the process exits with a structured error naming the missing keys.
Related
- Concepts: Architecture — the layers each config section maps to
- Guides: Federate to your IdP — full OIDC setup by provider
- Guides: Enable DPoP end-to-end — tuning the DPoP knobs above
- Guides: Wire up the Token Vault — putting
broker_providers+connecttogether - Operate: Vault Transit — key store + data encryption
- Reference: Metrics & CLI — what
observabilityproduces - Security: Key management — rotation, algorithm choice, HSM