Configuration
This guide explains how to configure the Agentic Identity Broker for different deployment environments.
Table of Contents
- Overview
- Configuration Sources
- Precedence Rules
- Third-party OAuth2 Credential Files
- Environment-Specific Configuration
- YAML Configuration
- Command-Line Flags
- Configuration Reference
- Available Settings
- Token Refresh
- Trust-Anchor Key Sets
- Security Best Practices
- Troubleshooting
Overview
The Identity Broker supports multiple configuration sources with clear precedence rules. You can combine .env files, YAML configuration, and command-line flags to achieve flexible, environment-specific configuration without code changes.
Key Features
- Multiple Sources: .env files, YAML, CLI flags
- Environment Variable Substitution: Use
${VAR_NAME}in YAML files - Environment-Specific: Automatic loading of
.env.{environment}files - Secure by Default: Sensitive values automatically redacted in logs
- Clear Validation: Helpful error messages with fix instructions
- Audit Logging: JSON audit log of all configuration sources
Configuration Sources
The Identity Broker loads configuration from four types of sources:
1. Built-in Defaults
Default values applied if no other source provides a value:
log.level:infolog.format:text
2. .env Files
Environment-specific files loaded automatically based on the GO_ENV environment variable:
.env- Base configuration (committed to git as .env.example).env.local- Local overrides (gitignored).env.{GO_ENV}- Environment-specific (e.g., .env.production).env.{GO_ENV}.local- Environment-specific local overrides (gitignored)
Example .env:
# Base configuration
IDENTITY_BROKER_LOG_LEVEL=info
IDENTITY_BROKER_LOG_FORMAT=text
Example .env.production:
# Production overrides
IDENTITY_BROKER_LOG_LEVEL=warn
IDENTITY_BROKER_LOG_FORMAT=json
3. YAML Configuration File
Optional YAML file for structured configuration. Supports environment variable substitution using ${VAR_NAME} syntax.
File Location (in order of precedence):
- Path from
--configCLI flag - Path from
IDENTITY_BROKER_CONFIG_PATHenvironment variable config.yamlin current directory
Example config.yaml:
log:
level: ${IDENTITY_BROKER_LOG_LEVEL}
format: ${IDENTITY_BROKER_LOG_FORMAT}
4. Command-Line Flags
Highest precedence - overrides all other sources.
agentic-identity-broker --log-level debug --log-format json
Precedence Rules
When the same configuration key appears in multiple sources, the highest-precedence value wins:
CLI Flags > Environment Variables (including .env files) > YAML File > Defaults
Example:
- Defaults set
log.level=info .envsetsIDENTITY_BROKER_LOG_LEVEL=warnconfig.yamlsetslog.level=${IDENTITY_BROKER_LOG_LEVEL}(expands towarn)- CLI flag
--log-level=debugis provided
Result: log.level=debug (from CLI flag)
Third-party OAuth2 credential files
third_party_oauth2.credential_files maps exact, case-sensitive service canonical IDs to credential file pairs. Its default is {}.
Every binding requires client_id_file and client_secret_file, each with a non-blank absolute path.
Canonical IDs follow the existing canonical-ID grammar. UUIDs, paths, and whitespace are not canonical IDs.
third_party_oauth2:
credential_files:
corporate-api:
client_id_file: /meta/credentials/client-id
client_secret_file: /meta/credentials/client-secret
GitHub.Team:
client_id_file: /meta/github/client-id
client_secret_file: /meta/github/client-secret
The loader accepts this mapping from YAML, environment variables, .env files, or the JSON CLI flag.
For this mapping, precedence is CLI, environment, configuration file, then default. The winning source replaces the whole map, without merging bindings.
A winning {} removes all bindings. It does not change service records or restore stored credentials.
export IDENTITY_BROKER_THIRD_PARTY_OAUTH2_CREDENTIAL_FILES='{"corporate-api":{"client_id_file":"/meta/credentials/client-id","client_secret_file":"/meta/credentials/client-secret"}}'
./bin/agentic-identity-broker
./bin/agentic-identity-broker \
--third_party_oauth2.credential_files='{"corporate-api":{"client_id_file":"/meta/credentials/client-id","client_secret_file":"/meta/credentials/client-secret"}}'
./bin/agentic-identity-broker --third_party_oauth2.credential_files='{}'
Configuration validates types, canonical IDs, and paths without opening files. Unavailable files do not prevent startup or metadata operations. Unknown-ID bindings create no services. Each filesystem service uses only its own exact canonical-ID binding. The broker infers no binding from a service UUID, display name, client ID, or filename.
Register the service with credential_source: "filesystem", a canonical ID, and neither inline credential field.
See source-specific administration for complete create and PUT examples.
Stored services ignore bindings, including unusable bindings. Filesystem services fail closed when their required binding or file is unavailable.
Public, CIMD confidential, and Google services cannot select filesystem mode.
Initiation reads only the current client-ID file. Each exchange attempt and actual refresh reads a fresh, coherent pair. Each file must be regular, readable by the broker UID, non-empty after whitespace removal, and at most 65,536 bytes before trimming. The broker removes surrounding whitespace and preserves internal characters. It caches no descriptor or credential value and reads no files for metadata. Configuration has no hot reload. Binding changes require a restart, but file publication takes effect on the next applicable operation.
For rotation, publish fresh immutable targets and switch the complete pair atomically. Do not modify a published target or reuse a retired target during acquisition.
The reader revalidates both targets after acquisition. A detected replacement returns a safe generation_changed error and sends no provider authentication request.
A pair validated before later publication can finish its in-flight operation. Provider validity overlap must cover that operation.
Secret rotation can retain sessions when the client ID stays the same. A changed client ID requires new connections for old codes and sessions.
Source transitions never rewrite established identities. Removing a binding preserves filesystem mode and makes affected authentication fail with missing_binding.
Administrative responses expose source metadata, not paths, file values, or transition history. Source errors retain generic HTTP responses and closed, credential-free event reasons.
ExtProc approval configuration
The standalone ExtProc service loads EXTPROC_* values. Its source order is CLI flags, environment variables, YAML, then defaults.
Set tool_approvals.enabled: true only with authorization.enabled: true. Set tool_approvals.url to the broker URL. The URL must use HTTPS unless oauth2.tls.allow_http is true for development.
tool_approvals.long_poll_timeout_seconds defaults to 30. Its permitted range is 1 through 120 seconds. tool_approvals.approval_cache_idle_ttl defaults to 5m. tool_approvals.request_timeout defaults to 5s and does not limit long polling.
tool_approvals.max_staleness defaults to 60s. It limits the age of cached approval state that can authorize a request. Its value must cover tool_approvals.long_poll_timeout_seconds plus tool_approvals.request_timeout, so a successful long-poll request can complete. A smaller value causes tool_approvals.max_staleness must be at least tool_approvals.long_poll_timeout_seconds plus tool_approvals.request_timeout.
sessions.extraction.http_header defaults to Mcp-Session-Id. ExtProc uses this value to scope session approvals.
Environment-Specific Configuration
Use the GO_ENV environment variable to control which .env files are loaded:
Development (default)
# GO_ENV defaults to "development" if not set
agentic-identity-broker
# Loads: .env → .env.local → .env.development → .env.development.local
Production
GO_ENV=production agentic-identity-broker
# Loads: .env → .env.local → .env.production → .env.production.local
Staging
GO_ENV=staging agentic-identity-broker
# Loads: .env → .env.local → .env.staging → .env.staging.local
YAML Configuration
Basic Structure
log:
level: info # debug, info, warn, error
format: text # text, json
Environment Variable Substitution
Reference environment variables using ${VAR_NAME} syntax:
log:
level: ${IDENTITY_BROKER_LOG_LEVEL}
format: ${IDENTITY_BROKER_LOG_FORMAT}
Nested Variable References
Environment variables can reference other environment variables:
# In .env
BASE_LEVEL=info
IDENTITY_BROKER_LOG_LEVEL=${BASE_LEVEL}
# In config.yaml
log:
level: ${IDENTITY_BROKER_LOG_LEVEL} # Expands to "info"
Note: Circular references are detected and will cause an error. Maximum expansion depth is 10 levels.
Security Validation
The following patterns are rejected for security:
- Command substitution:
$(command)or backticks - Shell metacharacters:
;,|,&,>,< - These protections prevent command injection attacks
Command-Line Flags
Available Flags
agentic-identity-broker [flags]
Flags:
-c, --config string config file path (overrides IDENTITY_BROKER_CONFIG_PATH)
--log-level string log level: debug, info, warn, error
--log-format string log format: text, json
--third_party_oauth2.client_name string broker/platform name for DCR only
-h, --help help for agentic-identity-broker
Examples
# Override log level
agentic-identity-broker --log-level debug
# Use custom config file
agentic-identity-broker --config /etc/agentic-identity-broker/config.yaml
# Multiple flags
agentic-identity-broker --log-level debug --log-format json
# Short form for config
agentic-identity-broker -c config.production.yaml --log-level warn
Configuration Reference
This section lists all configuration options. See Available Settings for detailed descriptions and examples.
Current Configuration Options
Logging Configuration
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
log.level | enum | info | debug, info, warn, error | No | IDENTITY_BROKER_LOG_LEVEL | --log-level | Sets logging verbosity. Use info to retain security audit events; warn and error suppress Info-level success records. |
log.format | enum | text | text, json | No | IDENTITY_BROKER_LOG_FORMAT | --log-format | Sets log output format. Use json for production and log aggregation systems. |
Each successful confidential-provider secret decryption emits service_secret_decrypted at Info with the provider's service_id, including when listing services. The event does not include the secret.
Token Refresh Configuration
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
token_refresh.lookahead_duration | Go duration string | 5m | Positive Go duration | No | IDENTITY_BROKER_TOKEN_REFRESH_LOOKAHEAD_DURATION | --token_refresh.lookahead_duration | Window before access-token expiry for background refresh. Set it shorter than the provider's access-token lifetime. |
token_refresh.background_workers | integer | 10 | 1–20 | No | IDENTITY_BROKER_TOKEN_REFRESH_BACKGROUND_WORKERS | --token_refresh.background_workers | Maximum concurrent background refreshes per replica. PostgreSQL separately limits all locked-session operations to 20 across background, on-demand, sweep, and reconnect traffic. |
token_refresh.sweep.default_page_size | integer | 100 | 1–1000 | No | IDENTITY_BROKER_TOKEN_REFRESH_SWEEP_DEFAULT_PAGE_SIZE | --token_refresh.sweep.default_page_size | Number of candidates per admin sweep page when the request omits page_size. |
token_refresh.health_metrics.enabled | bool | true | true, false | No | IDENTITY_BROKER_TOKEN_REFRESH_HEALTH_METRICS_ENABLED | --token_refresh.health_metrics.enabled | Publish the oauth2_sessions{service_id,expiry,refreshable} gauge. Takes effect only when telemetry metrics are exported. |
token_refresh.health_metrics.query_timeout | Go duration string | 5s | Positive Go duration | No | IDENTITY_BROKER_TOKEN_REFRESH_HEALTH_METRICS_QUERY_TIMEOUT | --token_refresh.health_metrics.query_timeout | Timeout of the per-export session count query, applied instead of storage.timeouts.read. A slower query reports no gauge data for that export cycle. |
Encryption Configuration
The broker uses a backend-explicit encryption contract.
Configure exactly one of encryption.aws_kms or encryption.memory.
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
encryption.aws_kms.key_arn | string | - | AWS KMS key ARN or alias ARN | Yes when encryption.aws_kms is set | IDENTITY_BROKER_ENCRYPTION_AWS_KMS_KEY_ARN | N/A | Customer-managed KMS key for the AWS KMS backend. |
encryption.aws_kms.dynamodb_table_name | string | IdentityBrokerEncryptionBranchKeys | DynamoDB table name | No | IDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_TABLE_NAME | N/A | DynamoDB table used to cache hierarchical branch keys. |
encryption.aws_kms.branch_key_ttl | duration string | 1h | Go duration string | No | IDENTITY_BROKER_ENCRYPTION_AWS_KMS_BRANCH_KEY_TTL | N/A | Lifetime of cached branch keys in DynamoDB. |
encryption.aws_kms.dynamodb_region | string | AWS SDK default region | AWS region | No | IDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_REGION | N/A | Optional region override for DynamoDB branch-key storage. |
encryption.aws_kms.dynamodb_timeout | duration string | - | Positive Go duration string | No | IDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_TIMEOUT | N/A | Optional timeout applied to AWS-backed encryption operations. |
encryption.aws_kms.disable_ssl | boolean | false | true only with explicit GO_ENV=development | No | IDENTITY_BROKER_ENCRYPTION_AWS_KMS_DISABLE_SSL | N/A | Disable AWS TLS certificate verification for development emulator testing only. Startup rejects this in every other environment. |
encryption.memory.raw_key | string | - | Base64-encoded 32-byte AES-256 key | Yes when encryption.memory is set | IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEY | N/A | Raw AES key for the in-memory backend. Sensitive - redacted in logs. |
Encryption Configuration Notes:
- Exactly one of
encryption.aws_kmsorencryption.memorymust be configured. encryption.aws_kms.key_arnaccepts KMS key ARNs and alias ARNs.encryption.memory.raw_keymust decode to exactly 32 bytes.- Startup fails closed if encryption configuration is missing or invalid.
- Tokens encrypted with old KEK material remain decryptable as long as the corresponding backend material remains available.
Server Configuration
The Identity Broker runs two independent HTTP servers on separate ports:
- End-User Server: Public-facing API for authentication and identity operations (default port 8000)
- Admin Server: Internal management API for monitoring and administration (default port 14000)
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
server.enduser.port | integer | 8000 | 1-65535 | No | IDENTITY_BROKER_SERVER_ENDUSER_PORT | --server.enduser.port | Port for end-user server. Must differ from admin port. |
server.enduser.bind | string | :: | IPv4/IPv6 address or hostname | No | IDENTITY_BROKER_SERVER_ENDUSER_BIND | --server.enduser.bind | Bind address for end-user server. Use :: for dual-stack (IPv6+IPv4), 0.0.0.0 for IPv4 only, or 127.0.0.1 for localhost only. |
server.enduser.public_url | URL | http://localhost:8000 | HTTP or HTTPS URL | Yes | IDENTITY_BROKER_SERVER_ENDUSER_PUBLIC_URL | N/A | Public URL for callbacks and broker metadata. Hosted CIMD and DCR require a stable, public HTTPS URL. |
server.admin.port | integer | 14000 | 1-65535 | No | IDENTITY_BROKER_SERVER_ADMIN_PORT | --server.admin.port | Port for admin server. Must differ from end-user port. |
server.admin.bind | string | :: | IPv4/IPv6 address or hostname | No | IDENTITY_BROKER_SERVER_ADMIN_BIND | --server.admin.bind | Bind address for admin server. In production, restrict to private network (for example 10.0.1.0) or use firewall rules. |
server.admin.public_url | URL | http://localhost:14000 | HTTP or HTTPS URL with a hostname, no userinfo, and no empty explicit port | Yes | IDENTITY_BROKER_SERVER_ADMIN_PUBLIC_URL | N/A | Public admin URL. All admin /api requests must match its Host authority. Reverse proxies must preserve this Host. |
server.shutdown.timeout | duration | 30s | 1s-5m | No | IDENTITY_BROKER_SERVER_SHUTDOWN_TIMEOUT | --server.shutdown.timeout | Maximum time to wait for in-flight requests to complete during graceful shutdown. Use longer timeouts (60s) in production. |
Server configuration notes:
- Both servers start together. If either server cannot bind, both stop.
- After startup, each server runs independently. A failure in one server does not affect the other server.
- Both servers provide
/health. - Graceful shutdown waits for active requests until the configured timeout.
- Admin
/apivalidates Host againstserver.admin.public_url, including its port. Hostnames are case-insensitive and default HTTP/HTTPS ports may be omitted. The broker rejects URLs with an empty explicit port, such ashttps://admin.example.com:, at startup.ForwardedandX-Forwarded-Hostdo not override this check. Configure the public admin URL before exposing the API through an ingress or reverse proxy. - Admin
/apirejects cross-origin browser mutations with 403, including origins allowed by CORS. POST, PUT, and PATCH requireContent-Type: application/jsoneven for actions without a body; other requests carrying a body also require JSON. Missing, malformed, or non-JSON media types return 415. JSON media-type parameters are accepted. Non-browser JSON clients remain supported. - Admin
/healthremains available without Host, cross-origin, or media-type checks.
CORS origin restrictions: server.enduser.cors.allowed_origins and
server.admin.cors.allowed_origins default to empty lists, which disable CORS
headers. Any origin containing *, including "*", "https://*", and
"https://*.example.com", is rejected at startup, even when mixed with specific
origins. Use explicit origins such as "http://localhost:3000" for the local Vite
development server.
CIMD confidential third-party services: This outbound client mode adds no
configuration parameter. The existing server.enduser.public_url must be a
stable, public HTTPS URL. The broker derives each service's client ID from
this URL.
At startup, the broker compares each stored CIMD client ID with the configured public URL. If the URL changes, startup stops before the broker provisions CIMD keys or serves requests. The broker does not rewrite stored client IDs or use a fallback metadata URL.
To restore service, restore the original public URL and keep it externally reachable. Re-registration or identity migration requires a separately approved migration flow.
Protected-Resource Discovery Configuration
third_party_oauth2.client_name identifies the broker or its platform for dynamic client registration (DCR). It does not identify a third-party service.
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
third_party_oauth2.client_name | string | Unset | Non-blank name for DCR | For DCR only | IDENTITY_BROKER_THIRD_PARTY_OAUTH2_CLIENT_NAME | --third_party_oauth2.client_name | Broker-wide client_name sent in DCR requests, never the service display_name. |
If DCR is selected, an absent or whitespace-only name stops the attempt before registration. Startup, manual services, and hosted CIMD do not require this name. A service's display_name cannot replace it.
The YAML key, environment variable, and CLI flag supply the same client name. The CLI flag overrides the environment variable, which overrides YAML. The example uses YAML.
Hosted CIMD and DCR require a stable, public HTTPS server.enduser.public_url. DCR uses it to build the OAuth2 callback URL. The broker reads this value from YAML or IDENTITY_BROKER_SERVER_ENDUSER_PUBLIC_URL. There is no public-URL CLI flag. Helm maps broker.server.enduser.publicUrl to the same broker key.
If you select hosted CIMD without a usable client-authentication key, generate an ES256 key through POST /api/cimd-client-keys on the Admin API. The broker stores its private key. third_party_oauth2.jwe_signing_key protects OAuth2 state, not CIMD client assertions.
Set discovery.resource_url to a public HTTPS URL in a service create or update request to the Admin API. It is not a broker configuration key. The service's display_name and optional authorization_params.resource also belong in that request. If no resource override is present, the broker derives the effective token resource from the verified discovery URL.
One fixed 10-second deadline covers each protected-resource discovery and registration attempt. Each remote discovery or registration response has a fixed 256-KiB limit. These are not configuration keys. CIMD document fetches, OAuth code exchange, and token refresh keep their separate limits.
Discovery-backed Admin create and update requests have a longer, finite HTTP response deadline. It includes the attempt, three configured storage-read timeouts, one storage-write timeout, and five seconds for the response. The default storage timeouts give these requests 40 seconds after parsing. Other requests retain the server's 15-second write timeout. An upstream proxy must permit the full Admin response window.
See the protected-resource configuration example and the Admin API service contract for the separate broker and service fields.
Storage Configuration
Storage configuration controls the persistence backend and steady-state timeout budgets used by storage-backed workflows.
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
storage.backend | enum | memory | memory, postgres | No | IDENTITY_BROKER_STORAGE_BACKEND | N/A | Selects the storage backend. Use postgres for persistent or multi-replica deployments. |
storage.postgres.connection_url | string | - | PostgreSQL connection URL | Yes when storage.backend=postgres | IDENTITY_BROKER_STORAGE_POSTGRES_URL | N/A | PostgreSQL connection string for the persistent storage backend. |
storage.timeouts.read | duration | 5s | Positive duration | No | N/A | N/A | Timeout for steady-state storage read operations. |
storage.timeouts.write | duration | 10s | Positive duration | No | N/A | N/A | Timeout for steady-state storage write operations. |
Storage configuration notes:
storage.timeouts.readandstorage.timeouts.writeapply to steady-state repository operations.oauth2_authorization_server.local.signing_keys.bootstrap_timeoutdefines the signing-key startup time limit. It applies to local and hybrid token issuance.
Example YAML:
storage:
backend: postgres
postgres:
connection_url: ${IDENTITY_BROKER_STORAGE_POSTGRES_URL}
timeouts:
read: 5s
write: 10s
IPv4/IPv6 dual-stack support
The Identity Broker supports these network bindings:
- Dual-stack (default):
::accepts IPv6 and IPv4 connections on dual-stack systems. - IPv6 only: Use
::1for localhost or a specific IPv6 address. - IPv4 only: Use
0.0.0.0for all interfaces or127.0.0.1for localhost. - Fallback: If IPv6 binding fails, the broker uses IPv4 and writes a warning log.
Examples are in examples/config/:
config.ipv6-only.yamluses dual-stack with IPv6 preference.config.ipv4-only.yamluses IPv4 only.
Graceful Shutdown
The broker completes active requests during graceful shutdown:
- On
SIGTERMorSIGINT, health status becomesshutting_down. - New requests return HTTP 503 Service Unavailable.
- Active requests can complete until
server.shutdown.timeout. - After the timeout, the broker closes remaining connections.
- The process exits.
Example:
# Start broker
agentic-identity-broker &
# Graceful shutdown (waits for requests)
kill -TERM $(pgrep agentic-identity-broker)
# Force shutdown (immediate)
kill -KILL $(pgrep agentic-identity-broker)
Authentication Configuration
Pre-authentication mode allows the Identity Broker to trust authenticated principals from a reverse proxy. The reverse proxy handles user authentication and passes the authenticated user identifier via an HTTP header.
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | Description |
|---|---|---|---|---|---|---|
server.enduser.authentication.preauth.principal_header_name | string | X-Remote-User | Any HTTP header name | No | IDENTITY_BROKER_SERVER_ENDUSER_AUTHENTICATION_PREAUTH_PRINCIPAL_HEADER_NAME | HTTP header containing the authenticated user principal for end-user server |
server.admin.authentication.preauth.principal_header_name | string | X-Remote-User | Any HTTP header name | No | IDENTITY_BROKER_SERVER_ADMIN_AUTHENTICATION_PREAUTH_PRINCIPAL_HEADER_NAME | HTTP header containing the authenticated user principal for admin server |
Pre-Authentication Configuration Notes:
- The principal header must be set by a trusted reverse proxy only (nginx, Traefik, HAProxy, etc.)
- Never expose the Identity Broker to untrusted networks - always use a reverse proxy for authentication
- The principal value is trimmed of leading/trailing whitespace
- Maximum principal length: 200 characters (longer principals are rejected with 400 Bad Request)
- Empty/missing headers result in 401 Unauthorized on protected routes
- Optional authentication routes allow missing principals
JWT pre-authentication (both servers): Configure server.enduser.authentication.jwt and
server.admin.authentication.jwt independently. For Helm, set the corresponding
broker.server.enduser.authentication.jwt and broker.server.admin.authentication.jwt
values with the same snake_case keys. When jwt is configured, the plain principal header
is not a fallback. The broker accepts the configured JWT header only from a trusted proxy
or mesh; it must remove client-supplied copies and restrict direct access to both ports.
Option under server.<enduser or admin>.authentication.jwt | Default | Description |
|---|---|---|
header_name | Authorization | JWT header; only Authorization strips a Bearer prefix. A gateway-supplied X-Userinfo contains the raw JWT. |
verification | jwks | jwks verifies a signature; none trusts a gateway-supplied JWT without signature verification. |
jwks_uri | none | One key source for jwks verification. Forbidden with none. Must use HTTPS except when the development-only HTTPS skip is enabled. |
jwks, jwks_file | none | Alternative key sources to jwks_uri, on both server.enduser.authentication.jwt and server.admin.authentication.jwt. Each server is a separate trust anchor with its own key set. At most one of the three key sources may be set, and jwks verification requires one. Forbidden with none. See Trust-Anchor Key Sets. |
expected_audience, expected_issuer | unset | Reject JWTs with other audience/issuer when set. |
claim_extraction.principal_expression | claims.sub | CEL expression returning a non-empty principal; for gateway userinfo containing principal, use claims.principal. |
claim_extraction.display_name_expression, email_expression, picture_url_expression | unset | Optional CEL expressions for profile attributes. |
The admin gateway must authorize callers and set the configured JWT header after discarding
client copies. A bearer token used to authenticate at the gateway is not the broker's
operator principal. With an absent or invalid JWT, admin CIMD key mutations do not create
a key and report server_misconfiguration because the operator principal is missing.
Example YAML configurations:
Development (custom header):
server:
enduser:
port: 8000
bind: "::"
authentication:
preauth:
principal_header_name: X-Authenticated-User
admin:
port: 14000
bind: "::"
authentication:
preauth:
principal_header_name: X-Remote-User
Production (environment variable):
server:
enduser:
port: 8000
bind: "::"
authentication:
preauth:
principal_header_name: ${IDENTITY_BROKER_PRINCIPAL_HEADER}
admin:
port: 14000
bind: "10.0.1.0" # Restrict to private network
authentication:
preauth:
principal_header_name: ${IDENTITY_BROKER_PRINCIPAL_HEADER}
Then set the environment variable:
export IDENTITY_BROKER_PRINCIPAL_HEADER="X-Authenticated-User"
Nginx Reverse Proxy Example:
upstream identity_broker {
server localhost:8000;
}
server {
listen 80;
server_name api.example.com;
location / {
auth_request /auth;
proxy_pass http://identity_broker;
proxy_set_header X-Remote-User $remote_user;
}
location = /auth {
internal;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
return 200;
}
}
Request Context Configuration
The broker captures a request security context for every inbound HTTP request. This capture is
always enabled. This section configures trusted-proxy caller IP derivation and the additive
W3C traceresponse response header.
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
request_context.trusted_proxy.enabled | boolean | false | true, false | No | IDENTITY_BROKER_REQUEST_CONTEXT_TRUSTED_PROXY_ENABLED | --request_context.trusted_proxy.enabled | Use the configured forwarded header to derive the caller IP. When false, the broker ignores client forwarding headers and uses the direct connection address. |
request_context.trusted_proxy.forwarded_header | string | X-Forwarded-For | Any HTTP header name | No | IDENTITY_BROKER_REQUEST_CONTEXT_TRUSTED_PROXY_FORWARDED_HEADER | --request_context.trusted_proxy.forwarded_header | Header read in trusted-proxy mode. The right-most entry is authoritative. |
request_context.trace.response_enabled | boolean | true | true, false | No | IDENTITY_BROKER_REQUEST_CONTEXT_TRACE_RESPONSE_ENABLED | --request_context.trace.response_enabled | Add the W3C traceresponse response header. Disabling this option does not disable request capture or log correlation. |
Request context notes:
- Security-context capture is always enabled. It has no feature-level disable switch.
- When
request_context.trusted_proxy.enabled=false, the broker ignores forwarding headers to prevent IP spoofing. - In trusted-proxy mode, the broker uses the right-most configured-header entry as the caller IP.
request_context.trace.response_enabled=falseremoves only the response header. Structured logs still includetrace_id.
Example YAML:
request_context:
trusted_proxy:
enabled: false
forwarded_header: X-Forwarded-For
trace:
response_enabled: true
Third-Party OAuth2 Session Configuration
These options control the authorization-code flow and token refresh for third-party service connections.
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
third_party_oauth2.jwe_signing_key | string | — | Base64, 32 bytes decoded | Yes | IDENTITY_BROKER_JWE_SIGNING_KEY | — | Key that encrypts OAuth2 state tokens. |
third_party_oauth2.state_token_ttl | duration | 10m | Up to 15m | No | IDENTITY_BROKER_STATE_TOKEN_TTL | — | Lifetime of the OAuth2 state token between authorize and callback. |
third_party_oauth2.pkce_verifier_length | integer | 32 | 32–128 | No | IDENTITY_BROKER_PKCE_VERIFIER_LENGTH | — | PKCE verifier length in bytes (RFC 7636). |
Refresh notes:
- Token exchange returns a still-valid access token immediately, unless the provider has rejected the session's refresh token. A recorded rejection refuses the exchange with
invalid_granteven while the access token has not expired. Withintoken_refresh.lookahead_duration(default5m), a usable refresh token allows background refresh without delaying the response. Failed or dropped background work does not change the response. - Refresh retries complete HTTP 5xx and 429 error responses and pre-send dial failures, with at most three attempts. Backoff delays are 1s and 2s. It honors
Retry-Afterup to 10 seconds; larger values, including numeric overflow, stop retries. It does not retry incomplete response bodies, other 4xx rejections, timeouts, or ambiguous transport failures after sending request headers. A lost response can lose a provider rotation; stored credentials remain unchanged, and a later provider rejection can require login. - Refresh error bodies are limited to 64 KiB. Larger bodies are treated as incomplete and are not retried.
- If a reconnect omits
refresh_token, the broker accepts the new access token and clears the stored refresh token and any recorded rejection. This prevents combining credentials from different upstream accounts. The connection requires login again after the new access token expires. To restore automatic refresh, obtain a new refresh token through provider consent. For providers that require it, configureprompt=consentthrough the service's authorization parameters. - The broker does not infer or record refresh-token expiry from provider-specific lifetime fields. It records only an observed provider
invalid_grantrejection, which requires the user to log in again. Provider rejection remains authoritative. - The broker accepts access-token
expires_invalues as JSON numbers or numeric strings. During refresh, absent or null metadata leaves expiry unspecified. Malformed or unrepresentable values, including positive lifetimes below one second, fail withprovider_response_invalid. Other positive fractions are truncated to whole seconds. Refresh commits explicit token type and scope with the new tokens. An omitted scope retains the stored scope; an explicit empty scope clears it. GitHub scopes use comma separators.
Notes:
- All configuration options have built-in defaults and are optional unless marked "Required"
- Environment variables follow the pattern:
IDENTITY_BROKER_{SECTION}_{KEY}(uppercased) - CLI flag names vary by setting. Use the exact flag shown in the reference table.
- See Precedence Rules for how values from different sources are resolved
- For YAML configuration syntax, see YAML Configuration
Future Configuration Options
The following configuration sections are planned for future releases. This table documents the intended structure for extensibility:
| Option | Type | Default Value | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
server.host | string | 0.0.0.0 | Any valid hostname/IP | No | IDENTITY_BROKER_SERVER_HOST | --server-host | Server bind address. Use 127.0.0.1 for local-only access. |
server.port | integer | 8080 | 1-65535 | No | IDENTITY_BROKER_SERVER_PORT | --server-port | Server listen port for HTTP requests. |
server.tls.enabled | boolean | false | true, false | No | IDENTITY_BROKER_SERVER_TLS_ENABLED | --server-tls-enabled | Enable TLS/HTTPS for secure connections. |
server.tls.cert_file | string | - | Valid file path | Yes (if TLS enabled) | IDENTITY_BROKER_SERVER_TLS_CERT_FILE | --server-tls-cert-file | Path to TLS certificate file (PEM format). |
server.tls.key_file | string | - | Valid file path | Yes (if TLS enabled) | IDENTITY_BROKER_SERVER_TLS_KEY_FILE | --server-tls-key-file | Path to TLS private key file (PEM format). |
database.type | enum | postgres | postgres, mysql, sqlite | No | IDENTITY_BROKER_DATABASE_TYPE | --database-type | Database backend type for persistent storage. |
database.host | string | localhost | Valid hostname/IP | Yes | IDENTITY_BROKER_DATABASE_HOST | --database-host | Database server hostname or IP address. |
database.port | integer | 5432 | 1-65535 | No | IDENTITY_BROKER_DATABASE_PORT | --database-port | Database server port (defaults: PostgreSQL 5432, MySQL 3306). |
database.name | string | identity_broker | Valid database name | Yes | IDENTITY_BROKER_DATABASE_NAME | --database-name | Database name to use for broker data. |
database.username | string | - | Valid username | Yes | IDENTITY_BROKER_DATABASE_USERNAME | --database-username | Database authentication username. |
database.password | string | - | Any string | Yes | IDENTITY_BROKER_DATABASE_PASSWORD | N/A | Database authentication password. Sensitive - redacted in logs. |
database.ssl_mode | enum | prefer | disable, allow, prefer, require, verify-ca, verify-full | No | IDENTITY_BROKER_DATABASE_SSL_MODE | --database-ssl-mode | SSL/TLS mode for database connections. |
database.max_connections | integer | 25 | 1-1000 | No | IDENTITY_BROKER_DATABASE_MAX_CONNECTIONS | --database-max-connections | Maximum number of open database connections in pool. |
auth.jwt.secret | string | - | Base64 string (min 32 bytes) | Yes | IDENTITY_BROKER_AUTH_JWT_SECRET | N/A | JWT signing secret key. Sensitive - redacted in logs. |
auth.jwt.expiry | duration | 1h | Valid duration (e.g., 30m, 2h) | No | IDENTITY_BROKER_AUTH_JWT_EXPIRY | --auth-jwt-expiry | JWT token expiration duration. |
auth.session.timeout | duration | 24h | Valid duration | No | IDENTITY_BROKER_AUTH_SESSION_TIMEOUT | --auth-session-timeout | User session inactivity timeout. |
auth.providers | list | [] | Array of provider configs | Yes | N/A | N/A | List of configured identity providers (OAuth, SAML, etc.). |
Future options notes:
- These options are for planning and design.
- The current release does not implement them.
- Requirements and feedback can change their structure.
- Sensitive values, such as passwords, secrets, and keys, never have CLI flags.
- Duration values use formats such as
30s,5m,1h, and24h.
Naming Conventions
The configuration system follows consistent naming patterns across all sources:
- YAML Paths: Use nested mappings with lowercase structural keys. This guide uses dotted paths, such as
log.level, as shorthand. - Environment Variables: Prefix + uppercase + underscores (e.g.,
IDENTITY_BROKER_LOG_LEVEL,IDENTITY_BROKER_SERVER_TLS_ENABLED) - CLI Flags: Use the exact name shown in the reference table (for example,
--log-level,--token_refresh.background_workers, or--third_party_oauth2.client_name). - Nested Configuration: YAML nesting defines the structure. Flat keys such as
telemetry.exporter.endpointare literal keys, not structural paths. - Token Refresh: Use nested YAML mappings for
token_refresh. Its CLI flags use., and its environment variables use_.
Configuration by Use Case
Development Environment:
log:
level: debug
format: text
Production Environment:
log:
level: warn
format: json
Troubleshooting:
# Temporarily override to debug level
agentic-identity-broker --log-level debug
For complete configuration examples and detailed explanations, continue to the Available Settings section.
Available Settings
Logging Configuration
log.level
Description: Sets the logging verbosity level.
Valid Values: debug, info, warn, error
Default: info
Environment Variable: IDENTITY_BROKER_LOG_LEVEL
CLI Flag: --log-level
Examples:
# .env file
IDENTITY_BROKER_LOG_LEVEL=debug
# YAML file
log:
level: warn
# CLI flag
--log-level error
log.format
Description: Sets the log output format.
Valid Values: text, json
Default: text
Environment Variable: IDENTITY_BROKER_LOG_FORMAT
CLI Flag: --log-format
Examples:
# .env file
IDENTITY_BROKER_LOG_FORMAT=json
# YAML file
log:
format: json
# CLI flag
--log-format json
Recommendation: Use json format in production for structured logging and log aggregation.
Token Refresh
Set these optional values for proactive refresh of third-party OAuth2 access tokens:
token_refresh:
lookahead_duration: 5m
background_workers: 10
sweep:
default_page_size: 100
health_metrics:
enabled: true
query_timeout: 5s
The source order is CLI flags > environment variables > YAML file > defaults. The five flags use the exact names in the reference table.
Set lookahead_duration to less than the shortest access-token lifetime that your providers issue. Otherwise, every token exchange can submit a refresh while the token remains valid.
Keep background_workers between 1 and 20 per replica. If all background slots are busy, the broker drops new submissions for other sessions and still returns the valid token.
The PostgreSQL adapter separately limits locked-session operations to 20 across all refresh triggers and reconnects. The worker limit alone does not bound distinct on-demand sessions or concurrent sweeps. Excess operations wait outside the 25-connection pool, leaving five connections outside locked-session work. Admission and connection acquisition share storage.timeouts.write (default 10s) and respect earlier caller cancellation. Saturation can return a storage timeout without a provider call.
Each admitted refresh holds its connection through all provider attempts, retry waits, and token persistence. Two maximum Retry-After waits add 20 connection-seconds per refresh, before provider latency. The bounded refresh context limits the total duration. Lower background concurrency leaves more of the shared admission capacity for on-demand refreshes and sweeps.
With health_metrics.enabled, each replica runs one grouped count of stored sessions per metric export cycle and publishes oauth2_sessions by service, access-token expiry class, and refreshability. The query reads no token material. A failed or slow query exports no points for that cycle and logs session health query failed at WARN. Consumers aggregate the gauge across replicas with max.
The broker does not schedule the admin sweep. Enable the chart's sessionSweep CronJob, or schedule POST /api/sessions/sweep with another external job. See the session sweep runbook for scheduling, credentials, and monitoring.
Encryption Backend Configuration
The broker requires encryption in every environment.
Configure exactly one of encryption.aws_kms or encryption.memory.
encryption.aws_kmsfor AWS KMS with hierarchical branch-key cachingencryption.memoryfor development/testing with a base64 AES-256 key
There are no CLI flags for encryption configuration.
encryption.aws_kms
Description: Configures the AWS KMS backend for envelope encryption.
Required field: encryption.aws_kms.key_arn
Core environment variables:
IDENTITY_BROKER_ENCRYPTION_AWS_KMS_KEY_ARNIDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_TABLE_NAMEIDENTITY_BROKER_ENCRYPTION_AWS_KMS_BRANCH_KEY_TTLIDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_REGIONIDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_TIMEOUTIDENTITY_BROKER_ENCRYPTION_AWS_KMS_REGIONIDENTITY_BROKER_ENCRYPTION_AWS_KMS_ASSUME_ROLE_ARN
Example:
# config.yaml - Production deployment
encryption:
aws_kms:
key_arn: ${IDENTITY_BROKER_ENCRYPTION_AWS_KMS_KEY_ARN}
dynamodb_table_name: ${IDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_TABLE_NAME:IdentityBrokerEncryptionBranchKeys}
branch_key_ttl: ${IDENTITY_BROKER_ENCRYPTION_AWS_KMS_BRANCH_KEY_TTL:1h}
# .env.production
IDENTITY_BROKER_ENCRYPTION_AWS_KMS_KEY_ARN=arn:aws:kms:eu-central-1:123456789012:key/12345678-1234-1234-1234-123456789012
IDENTITY_BROKER_ENCRYPTION_AWS_KMS_DYNAMODB_TABLE_NAME=IdentityBrokerEncryptionBranchKeys
IDENTITY_BROKER_ENCRYPTION_AWS_KMS_BRANCH_KEY_TTL=1h
key_arn accepts both KMS key ARNs and alias ARNs.
TLS certificate verification is enabled by default. Never enable disable_ssl in production.
Setting it to true requires explicit GO_ENV=development; production, staging, unset,
and unknown environments abort startup before AWS clients are created. The default
development environment used for .env file selection does not authorize this bypass.
HTTP emulator endpoints do not require disable_ssl; leave it false in
config.aws-emulator.yaml.
encryption.memory.raw_key
Description: Configures the in-memory backend with a base64-encoded 32-byte AES-256 key.
Environment Variable: IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEY
Example:
.env.local values are literal strings; paste a generated base64 key instead of shell command substitution.
# .env.local
IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEY=base64-encoded-32-byte-key
# shell
export IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEY="$(openssl rand -base64 32)"
# config.yaml - Development deployment
encryption:
memory:
raw_key: ${IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEY}
AWS KMS Setup Instructions:
- Create a customer-managed key in AWS KMS:
aws kms create-key \
--description "Identity Broker OAuth Token Encryption Key" \
--region eu-central-1
- Create an alias for easier reference:
aws kms create-alias \
--alias-name "alias/identity-broker-encryption" \
--target-key-id "arn:aws:kms:eu-central-1:123456789012:key/12345678-1234-1234-1234-123456789012"
- Grant IAM permissions to the Identity Broker service role:
aws kms create-grant \
--key-id "arn:aws:kms:eu-central-1:123456789012:key/12345678-1234-1234-1234-123456789012" \
--grantee-principal "arn:aws:iam::123456789012:role/IdentityBrokerRole" \
--operations "Encrypt" "Decrypt" "GenerateDataKey" "DescribeKey"
- Reference the key in configuration:
encryption:
aws_kms:
key_arn: "arn:aws:kms:eu-central-1:123456789012:alias/identity-broker-encryption"
Startup validation:
- AWS KMS backend: At startup, the broker validates KMS key access and service permissions. It does not start when validation fails.
- Memory backend: At startup, the broker validates
IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEYor its YAML value. The value must decode to a 32-byte key. The broker does not start when validation fails.
Security considerations:
- AWS KMS: The KEK remains in AWS KMS. The application receives only encrypted data encryption keys. KMS performs cryptographic operations.
- Memory backend: The broker loads KEK material into application memory at startup.
- Rotation: KMS keys can rotate without an application restart. Tokens encrypted with prior key material remain decryptable.
- Permissions: Grant the service role
kms:Decryptandkms:GenerateDataKey. Limit permissions to the required resources.
Backend by environment:
| Environment | Approach | Configuration |
|---|---|---|
| Production | AWS KMS | encryption.aws_kms.key_arn with a customer-managed KMS key |
| Staging | AWS KMS | A separate AWS KMS key for each environment |
| Development | Memory backend | encryption.memory.raw_key from IDENTITY_BROKER_ENCRYPTION_MEMORY_RAW_KEY |
| CI/CD | Memory backend | encryption.memory.raw_key from CI/CD secrets |
| Testing | Memory backend | A new random key for each test |
Request Context Configuration
The request-context configuration defines how the broker derives caller IP metadata and whether it returns the
W3C traceresponse response header. The security-context capture itself is always enabled.
request_context.trusted_proxy.enabled
Description: Enables trusted-proxy caller-IP derivation.
Valid Values: true, false
Default: false
Environment Variable: IDENTITY_BROKER_REQUEST_CONTEXT_TRUSTED_PROXY_ENABLED
CLI Flag: None
Behavior:
false: use the direct connection address and ignore client-supplied forwarding headers.true: inspectrequest_context.trusted_proxy.forwarded_headerand use the right-most forwarded entry as the trusted caller IP.
request_context.trusted_proxy.forwarded_header
Description: Names the HTTP header that carries the forwarded caller-IP chain when trusted proxy mode is enabled.
Default: X-Forwarded-For
Environment Variable: IDENTITY_BROKER_REQUEST_CONTEXT_TRUSTED_PROXY_FORWARDED_HEADER
CLI Flag: None
Notes:
- This value applies only when
request_context.trusted_proxy.enabled=true. - The broker uses the right-most entry to prevent trust in attacker-controlled left-most values.
request_context.trace.response_enabled
Description: Controls the W3C traceresponse response header.
Valid values: true, false
Default: true
Environment variable: IDENTITY_BROKER_REQUEST_CONTEXT_TRACE_RESPONSE_ENABLED
CLI flag: None
Notes:
- When enabled, each HTTP response can contain
traceresponse: 00-<trace-id>-<child-id>-<flags>. <trace-id>matches thetrace_idin structured logs.- Disabling this option removes only the response header. Trace capture and log correlation remain active.
Example:
request_context:
trusted_proxy:
enabled: true
forwarded_header: X-Forwarded-For
trace:
response_enabled: true
See examples/config/request-context.yaml for a documented overlay example.
Security Best Practices
CIMD SSRF protection
Never enable security.skip_cimd_ssrf_validation in production. The default is false.
This flag disables both the IP blocklist and TLS certificate verification for CIMD document fetches.
Use it only for local mock servers in development or tests.
Set GO_ENV=development explicitly only for local CIMD mock testing.
Otherwise, the configuration loader rejects security.skip_cimd_ssrf_validation: true before storage or network initialization, even when CIMD is disabled.
This includes unset, empty, staging, test, production, and unrecognized environment values.
The default development environment for .env file selection does not permit this bypass.
Audit the effective flag value and its source in the startup configuration summary.
Alert on security.skip_cimd_ssrf_validation: true outside local development or tests.
The configuration_loaded JSON audit event identifies the configuration source and key, but does not include the effective value.
When the builder creates an insecure CIMD fetcher, it also emits a warning.
Sensitive Values
Any configuration key starting with IDENTITY_BROKER_ or containing these keywords is considered sensitive and will be redacted in logs:
PASSWORDSECRETTOKENKEYCREDENTIALAUTH
Example:
IDENTITY_BROKER_API_KEY=secret123
DB_PASSWORD=mypassword
The startup summary and audit logs show both values as ***REDACTED***.
PostgreSQL connection URL validation errors redact the entire rejected value as
***REDACTED***, including malformed URLs that may contain credentials. The error
still identifies storage.postgres.connection_url and the expected URL format.
Do not commit secrets
- Use
.env.localand.env.{environment}.localfor local secrets. Git ignores these files. - Commit
.env.exampleand.env.production.exampleas templates. - Use environment variables or secret-management systems in production.
File permissions
Do not make configuration files world-readable:
# Recommended permissions
chmod 600 .env
chmod 600 config.yaml
Environment Variable Injection
The system validates against command injection attempts. The following will be rejected:
# REJECTED: Command substitution
log:
level: $(malicious_command)
# REJECTED: Shell metacharacters
database:
host: localhost; rm -rf /
Troubleshooting
Configuration Not Loading
Symptom: The application uses default values instead of your configuration.
Solutions:
- Make sure that
.envfiles are in the current directory. Or, use an absolute path. - Make sure that environment variable names start with
IDENTITY_BROKER_. - Validate YAML syntax with
yamllintor an online validator. - Use
--helpto make sure that flag names are correct.
Undefined environment variable error
Symptom: The error message is "environment variable 'VAR_NAME' is not set".
Cause: The YAML file references ${VAR_NAME}, but the environment has no variable with
that name.
Solutions:
- Set the environment variable with
export VAR_NAME=value. - Add
VAR_NAME=valueto.env. - Remove the
${}YAML reference and use a literal value.
Invalid configuration value
Symptom: The error message is "invalid value 'X' for field 'Y'".
Cause: The configuration value does not match its expected format or enum.
Solutions:
- Read the error message for expected values. For example: "expected: debug, info, warn, or error".
- Make sure that spelling and case are correct. Values are case-sensitive.
- Remove extra whitespace or quotes from the value.
Circular Reference Detected
Symptom: Error message: "circular reference detected: VAR1 → VAR2 → VAR1"
Cause: Environment variables reference each other in a loop.
Solution: Break the circular dependency:
# WRONG
VAR1=${VAR2}
VAR2=${VAR1}
# CORRECT
VAR1=value1
VAR2=${VAR1}
Viewing Effective Configuration
To see configuration values and their sources:
agentic-identity-broker
# Output shows:
# === Configuration Summary ===
# log.level: debug [source: cli]
# log.format: json [source: yaml (/path/to/config.yaml)]
#
# === Configuration Sources ===
# [0] default: defaults (loaded at ...)
# [1] env_file: /path/to/.env (loaded at ...)
# [2] yaml: /path/to/config.yaml (loaded at ...)
# [3] cli: cli (loaded at ...)
Audit Log
For compliance and troubleshooting, read the JSON audit log. It is the first startup output:
{
"timestamp": "2025-12-15T09:00:00Z",
"event": "configuration_loaded",
"sources": [...],
"keys": ["log.level", "log.format"],
"redacted_keys": []
}
OAuth2 Authorization Server Configuration
CIMD
Description: Controls Client ID Metadata Document fetching and validation in local and hybrid modes. CIMD is disabled by default. Every setting is optional.
| Parameter | Type | Default | Required | Env var | Description |
|---|---|---|---|---|---|
oauth2_authorization_server.cimd.enabled | boolean | false | No | IDENTITY_BROKER_CIMD_ENABLED | Enable URL-based client IDs from pre-registered HTTPS metadata URLs. |
oauth2_authorization_server.cimd.fetch_timeout | duration | 1s | No | IDENTITY_BROKER_CIMD_FETCH_TIMEOUT | Timeout for fetching client metadata documents. |
oauth2_authorization_server.cimd.max_response_bytes | integer | 5120 | No | IDENTITY_BROKER_CIMD_MAX_RESPONSE_BYTES | Maximum metadata document response size. |
oauth2_authorization_server.cimd.cache.max_ttl | duration | 1h | No | IDENTITY_BROKER_CIMD_CACHE_MAX_TTL | Maximum cache lifetime for metadata documents. |
oauth2_authorization_server.cimd.cache.min_ttl | duration | 60s | No | IDENTITY_BROKER_CIMD_CACHE_MIN_TTL | Minimum cache lifetime for metadata documents. |
oauth2_authorization_server.cimd.cache.max_entries | integer | 1000 | No | IDENTITY_BROKER_CIMD_CACHE_MAX_ENTRIES | Maximum number of cached metadata documents. |
oauth2_authorization_server.cimd.ssrf.extra_blocked_cidrs | string list | [] | No | IDENTITY_BROKER_CIMD_SSRF_EXTRA_BLOCKED_CIDRS | Additional IP ranges blocked during metadata fetches. Built-in blocked ranges remain active. |
oauth2_authorization_server.cimd.client_name_blocklist | string list | [] | No | IDENTITY_BROKER_CIMD_CLIENT_NAME_BLOCKLIST | Additional case-insensitive blocked client-name terms. |
oauth2_authorization_server.cimd.client_authentication.key_set_cache_ttl | duration | 1h | No | IDENTITY_BROKER_CIMD_CLIENT_AUTHENTICATION_KEY_SET_CACHE_TTL | Maximum age of a cached public key set. Values above 24h are rejected at startup. |
oauth2_authorization_server.cimd.client_authentication.key_set_min_refetch_interval | duration | 5m | No | IDENTITY_BROKER_CIMD_CLIENT_AUTHENTICATION_KEY_SET_MIN_REFETCH_INTERVAL | Minimum interval between key-set refetches for an unknown kid. |
oauth2_authorization_server.cimd.client_authentication.key_set_max_bytes | integer | 65536 | No | IDENTITY_BROKER_CIMD_CLIENT_AUTHENTICATION_KEY_SET_MAX_BYTES | Maximum response size for a remote key set. |
oauth2_authorization_server.cimd.client_authentication.key_set_fetch_timeout | duration | 5s | No | IDENTITY_BROKER_CIMD_CLIENT_AUTHENTICATION_KEY_SET_FETCH_TIMEOUT | Timeout for fetching a remote key set. |
Example:
oauth2_authorization_server:
mode: local
cimd:
enabled: true
client_authentication:
key_set_cache_ttl: 1h
key_set_min_refetch_interval: 5m
key_set_max_bytes: 65536
key_set_fetch_timeout: 5s
See examples/config/cimd.yaml for the full annotated example.
oauth2_authorization_server.multi_agent_client
Description: Controls whether multiple agents share one upstream OAuth2 client_id.
The broker stores this value as agent.client_id. When disabled, the broker requires each
agent upstream client_id to be unique. It always resolves an incoming OAuth2 request with
the internal UUID in agent.id. When enabled, agents can share one upstream OAuth2
application. The broker validates the agent identity with a custom claim in the upstream
authorization redirect and token response.
Configuration block: Nested under oauth2_authorization_server.
| Option | Type | Default | Valid Values | Required | Description |
|---|---|---|---|---|---|
multi_agent_client.enabled | boolean | false | true, false | No | Allow multiple agents to share one upstream OAuth2 client_id. When false, duplicate client_id on agent creation or update returns 409 Conflict. |
multi_agent_client.agent_id_param_name | string | — | Any URL-safe string | Yes if enabled=true | Query parameter on the upstream authorization redirect that contains the agent internal UUID. It must match the upstream OAuth2 claim name. |
multi_agent_client.agent_id_claim_name | string | — | Any string | Yes if enabled=true | JWT claim in the upstream token response that contains the agent internal UUID. The broker validates it on each token proxy response. A missing claim is rejected. |
Startup validation: If enabled = true and agent_id_param_name or
agent_id_claim_name is empty, the broker does not start.
Feature disabled (default):
server:
enduser:
public_url: "https://broker.example.com"
oauth2_authorization_server:
mode: "proxy"
proxy:
upstream_issuer_uri: "https://auth.example.com"
upstream_authorize_endpoint: "https://auth.example.com/authorize"
upstream_token_endpoint: "https://auth.example.com/token"
multi_agent_client:
enabled: false # default — each agent must have a unique client_id
Feature enabled:
server:
enduser:
public_url: "https://broker.example.com"
oauth2_authorization_server:
mode: "proxy"
proxy:
upstream_issuer_uri: "https://auth.example.com"
upstream_authorize_endpoint: "https://auth.example.com/authorize"
upstream_token_endpoint: "https://auth.example.com/token"
multi_agent_client:
enabled: true
agent_id_param_name: "x_agent_id" # injected into authorize redirect as ?x_agent_id=<agent.id>
agent_id_claim_name: "x_agent_id" # expected in upstream token response JWT
Token exchange CEL expression: Set token_exchange.claim_extraction.agent_id_expression according to multi_agent_client.enabled:
multi_agent_client.enabled | CEL expression | Purpose |
|---|---|---|
false (default) | resolveAgentIdByClientId(subject_token.azp) | Maps the upstream client_id to agent.id. |
true | subject_token.x_agent_id (use your agent_id_claim_name) | Reads the agent UUID from the token claim. |
See examples/config/oauth2-authorization-server.yaml for a complete configuration example with both modes commented.
Token Exchange Client Assertion Configuration
token_exchange.client_assertion
Description: Configures the external identity-provider trust anchor used to validate privileged-gateway client_assertion JWTs for RFC 8693 token exchange and machine-facing approval authentication. Broker-minted tokens are never valid client assertions.
| Option | Type | Default | Required? | Environment Variable | Rules |
|---|---|---|---|---|---|
token_exchange.client_assertion.issuer_uri | string | Proxy upstream issuer | Yes for configured token exchange or explicit key sources in local mode | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_ISSUER_URI | External IdP issuer URI. In proxy and hybrid modes, an empty value defaults to oauth2_authorization_server.proxy.upstream_issuer_uri. In local mode it must be explicit. The broker's own issuer is rejected at startup. |
token_exchange.client_assertion.expected_audience | string | token_exchange.expected_audience | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_EXPECTED_AUDIENCE | Audience required only for client assertions. When omitted, the shared token-exchange audience is retained for backward compatibility; subject tokens in third-party token exchange use token_exchange.expected_audience. Impersonation subject credentials use the applicable rule audience or no audience in unverified mode. |
token_exchange.client_assertion.jwks_uri | string | Discovered from issuer_uri | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS_URI | Explicit external IdP JWKS endpoint. Set this when the IdP does not support OAuth/OIDC discovery. |
token_exchange.client_assertion.jwks | string | unset | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS | Inline JWK Set JSON, as an alternative to jwks_uri and discovery. Mutually exclusive with jwks_uri and jwks_file. See Trust-Anchor Key Sets. |
token_exchange.client_assertion.jwks_file | string | unset | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS_FILE | Absolute path of a JWK Set file inside security.trust_material_dir. Mutually exclusive with jwks_uri and jwks. |
token_exchange.client_assertion.jwks_min_refresh | duration | 15m | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS_MIN_REFRESH | Minimum interval between client-assertion JWKS refresh attempts. Must be a non-negative Go duration. |
token_exchange.client_assertion.jwks_max_refresh | duration | max(jwks_min_refresh, 1h) | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS_MAX_REFRESH | Maximum client-assertion JWKS refresh interval. Must be a non-negative Go duration. For jwks_file, the default is max(3 × jwks_min_refresh, 1h) when unset or below the minimum; remote sources use max(jwks_min_refresh, 1h). |
Example:
token_exchange:
client_assertion:
issuer_uri: "https://privileged-gateway-idp.example.com"
expected_audience: "https://broker.example.com/oauth2/token"
# Optional when discovery is unavailable:
# jwks_uri: "https://privileged-gateway-idp.example.com/keys"
# jwks_min_refresh: "15m"
# jwks_max_refresh: "1h"
In local mode, set issuer_uri whenever token exchange or approval authentication is configured or whenever jwks_uri, jwks, or jwks_file is set. In proxy and hybrid modes it defaults to the configured proxy upstream issuer when omitted. The configured issuer must be an external IdP; a broker-self issuer is rejected at startup to prevent broker-minted tokens from becoming privileged client assertions.
Trust-Anchor Key Sets
jwks, jwks_file, and security.trust_material_dir
Description: A trust anchor is an authority whose signed credentials the broker verifies. Each trust anchor takes its public verification keys from one key set source: a remote jwks_uri (or discovery), an inline jwks document, or a jwks_file that the broker reads from the trust directory. Inline and file sources need no network-reachable key-publishing service and cause no outbound request. See adrs/040-trust-anchor-local-key-sets.md.
The in-scope trust anchors are:
- the token-exchange client assertion trust anchor, which also authenticates machine-facing approval requests;
- every trusted issuer of every impersonation rule;
- JWT pre-authentication on the end-user server and, separately, on the admin server.
| Option | Type | Default | Required? | Environment Variable | Rules |
|---|---|---|---|---|---|
security.trust_material_dir | string | unset | Yes, for any jwks_file | IDENTITY_BROKER_SECURITY_TRUST_MATERIAL_DIR | Absolute path. It must resolve, after symbolic links, to an existing directory other than /. The broker reads key-set files only from here. When no jwks_file is configured, the broker does not touch the filesystem. |
token_exchange.client_assertion.jwks | string | unset | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS | Inline JWK Set JSON. |
token_exchange.client_assertion.jwks_file | string | unset | No | IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS_FILE | Absolute path of a JWK Set file inside the trust directory. |
oauth2_authorization_server.impersonation.rules[].trusted_issuers[].jwks | string | unset | No | None (rule lists are YAML-only) | Inline JWK Set JSON. |
oauth2_authorization_server.impersonation.rules[].trusted_issuers[].jwks_file | string | unset | No | None (rule lists are YAML-only) | Absolute path of a JWK Set file inside the trust directory. |
server.enduser.authentication.jwt.jwks | string | unset | No | None. ${VAR} expansion in YAML applies | Inline JWK Set JSON. |
server.enduser.authentication.jwt.jwks_file | string | unset | No | None. ${VAR} expansion in YAML applies | Absolute path of a JWK Set file inside the trust directory. |
server.admin.authentication.jwt.jwks | string | unset | No | None. ${VAR} expansion in YAML applies | Inline JWK Set JSON. |
server.admin.authentication.jwt.jwks_file | string | unset | No | None. ${VAR} expansion in YAML applies | Absolute path of a JWK Set file inside the trust directory. |
The end-user and admin servers' JWT pre-authentication are separate trust anchors. Each has its own key set, and a JWT verified by one server's keys is not accepted by the other unless both declare the same key.
Mutual exclusivity: A trust anchor accepts at most one of jwks_uri, jwks, and jwks_file. There is no precedence. Setting more than one fails startup. A trust anchor that sets none keeps its existing behavior: issuer-metadata discovery, or the proxy upstream key set for the client assertion trust anchor in proxy and hybrid modes. A failed local source never falls back to discovery or to another source. To move a trust anchor from a remote key set to a local one, replace jwks_uri with jwks or jwks_file.
For JWT pre-authentication, verification: none is mutually exclusive with every key source (jwks_uri, jwks, and jwks_file).
A local key set is used only by the trust anchor that declares it. The broker never publishes it at /oauth2/jwks.json and never uses it to validate subject tokens. Two impersonation trusted issuers that share an issuer_uri but declare different key sets each verify against only their own keys. The ADR 029 self-trust check applies to every source. In proxy and hybrid modes, a client assertion trust anchor with a local key set and no issuer_uri keeps the upstream issuer as the expected issuer and verifies client assertions against the local keys only. In local mode, any explicit client-assertion key source requires issuer_uri.
Environment overrides
IDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKSandIDENTITY_BROKER_TOKEN_EXCHANGE_CLIENT_ASSERTION_JWKS_FILEoverride the client assertion keys. Any valid JWK Set JSON is accepted, including multiple lines.IDENTITY_BROKER_SECURITY_TRUST_MATERIAL_DIRoverridessecurity.trust_material_dir.- Impersonation rule lists are YAML-only, as for every other rule setting.
- JWT pre-authentication on either server has no environment variable. Use
${VAR}expansion in YAML, as the whole value:jwks: ${MESH_JWKS}. The loader checks the YAML value that contains the reference, so that value must fit on one line and must not contain shell metacharacters; the substituted environment value itself may span lines. A YAML block scalar (jwks: |) that holds the JSON directly may also span lines.
Refresh bounds
jwks_min_refresh and jwks_max_refresh apply as follows.
| Key set source | Behavior |
|---|---|
jwks_uri or discovery | Unchanged. |
jwks_file | jwks_min_refresh is how often the broker re-reads the file. jwks_max_refresh is how long after the last successful read the broker still serves the last valid key set. The minimum defaults to 15m. When the maximum is unset or below the minimum, it defaults to max(3 × jwks_min_refresh, 1h). An explicit maximum at least as large as the minimum is used as configured. |
jwks | Rejected when either bound is set (refresh_bounds_with_inline). An inline key set never refreshes. Change it by editing the configuration and restarting. |
| Trust anchor | Re-read interval | Staleness window |
|---|---|---|
token_exchange.client_assertion | jwks_min_refresh, default 15m | jwks_max_refresh: file default max(3 × min, 1h); remote default max(min, 1h) |
Impersonation trusted_issuers[] | jwks_min_refresh, default 15m | jwks_max_refresh: file default max(3 × min, 1h); remote default max(min, 1h) |
server.enduser.authentication.jwt, server.admin.authentication.jwt | 5m, fixed | 15m, fixed |
JWT pre-authentication does not accept refresh settings.
Key-set document rules
The inline value and the file contents are JSON text of an RFC 7517 section 5 JWK Set: an object with a keys array. The broker applies the same rules to both sources, at startup and on every re-read.
| Rule | Category |
|---|---|
| At most 1 MiB (1048576 bytes) | too_large |
An object with a keys array that the JOSE library parses as a key set | not_a_jwk_set |
| At least one key | empty_key_set |
No kty: oct (symmetric) key | symmetric_key |
| No private key members | private_key |
alg, when present, is one of RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA | unapproved_algorithm |
Every key has a non-empty kid | missing_kid |
kid values are unique | duplicate_kid |
A bare JWK without a keys array is rejected. A document that is only partly valid is rejected as a whole.
Set alg on every key. A local key must have a unique non-empty kid; a missing or empty kid fails startup. Only alg is optional. Token exchange and impersonation skip keys without alg, so those keys can pass startup but cannot verify credentials through those trust anchors. Every trust anchor requires a kid in the credential header. Remote key-set acceptance is unchanged.
Key-set file rules
| Rule | Category |
|---|---|
| The path is absolute | relative_path |
security.trust_material_dir is set | missing_trust_directory |
| The cleaned path lies under the cleaned trust directory | outside_trust_directory |
Opening the file beneath the trust directory does not leave the directory through .. or a symbolic link | outside_trust_directory |
| The file exists | not_found |
| The file can be read | unreadable |
| The file is a regular file after the broker follows symbolic links | not_regular_file |
The broker opens the file through the trust directory (os.Root). The regular-file check and the read use the same open file, so retargeting a link between a check and a read cannot escape the directory.
Symbolic links: Relative symbolic links that stay inside the trust directory are accepted. Kubernetes Secret, ConfigMap, and projected volumes use this layout (jwks.json links to ..data/jwks.json, which links to a timestamped directory). Absolute symbolic links inside the trust directory are rejected, even when their target is also inside the directory. A link that resolves outside the directory is rejected. Do not mount a key-set file with subPath: Kubernetes does not update subPath mounts, so the file would never rotate.
Choose a narrow trust directory that holds only trust material. The broker rejects /.
Re-read behavior
After startup the broker re-reads each key-set file every jwks_min_refresh, in the background.
- A valid re-read replaces the whole key set. Added and removed key identifiers are logged.
- A failed re-read keeps the last valid key set, logs a warning, and sets
trust_anchor_key_filestodegradedinGET /health. A failure is any file rule or document rule above. - After
jwks_max_refreshhas elapsed since the last successful read, verification for that trust anchor fails closed until a valid re-read succeeds. - The next valid re-read returns the component to
healthy.
Removing a compromised key from a file that keeps other keys is a valid change. It takes effect within jwks_min_refresh. Emptying the file is invalid and does not disable the trust anchor. To disable a trust anchor, change the configuration.
trust_anchor_key_files appears in the end-user server's /health components only when at least one trust anchor uses jwks_file. It never changes the HTTP status or the top-level status. Inline key sets never affect it.
Startup errors
Every error names the trust anchor's configuration path. No error includes inline content, file contents, or key material. Key identifiers, file paths, and the trust directory may appear.
Configuration validator errors use the existing format. For an inline key set, the value is always [inline JWK Set redacted]:
configuration error: invalid value "<value>" for field "<path>" (expected: <expectation>)
Builder errors use <path>: <message>. For document rules, <path> is the source setting, ….jwks or ….jwks_file.
| Category | Message |
|---|---|
conflicting_key_sources | only one of jwks_uri, jwks, jwks_file may be set (found: jwks_uri, jwks) |
refresh_bounds_with_inline | jwks_min_refresh and jwks_max_refresh are not allowed with jwks; an inline key set never refreshes |
verification_disabled_with_key_source | verification 'none' and jwks are mutually exclusive (or jwks_file, or the existing jwks_uri message) |
relative_path | jwks_file "<path>" must be an absolute path |
missing_trust_directory | jwks_file requires security.trust_material_dir to be set |
invalid_trust_directory | security.trust_material_dir "<dir>" must be an absolute path to an existing directory other than / |
outside_trust_directory | jwks_file "<path>" is outside security.trust_material_dir "<dir>" |
not_found | jwks_file "<path>" does not exist |
unreadable | jwks_file "<path>" cannot be read |
not_regular_file | jwks_file "<path>" is not a regular file |
too_large | key set exceeds 1048576 bytes |
not_a_jwk_set | key set is not a valid JWK Set document |
empty_key_set | key set contains no keys |
symmetric_key | key set contains a symmetric key; only public asymmetric keys are allowed |
private_key | key set contains private key material; only public keys are allowed |
unapproved_algorithm | key "<kid>" declares an algorithm outside the approved asymmetric set |
missing_kid | key at index <zero-based index> requires a non-empty kid |
duplicate_kid | key identifier "<kid>" appears more than once |
For a local key with a missing or empty kid, missing_kid reports its zero-based index only. An unapproved_algorithm key without kid reports unapproved_algorithm first, using that index. The existing startup errors still apply to every source: the self-trust rejection, a missing issuer for local-mode approval authentication, and JWT verification with no key source.
Log events
All events are structured slog records. None includes inline content, file contents, or key material.
| Event message | Level | When | Fields |
|---|---|---|---|
trust anchor key set loaded | Info | Startup, once per trust anchor with an inline or file source | trust_anchor, key_source (inline or file), path (file only), key_count, key_ids |
trust anchor key set changed | Info | A file re-read that changes the key identifiers | trust_anchor, key_source, path, key_count, added_key_ids, removed_key_ids |
trust anchor key set re-read failed | Warn | A failed file re-read | trust_anchor, key_source, path, error_kind, last_success_at, fails_closed_at |
trust anchor key set recovered | Info | First valid re-read after a failure | trust_anchor, key_source, path, key_count |
trust anchor key set expired | Warn | First key lookup after the staleness window expires | trust_anchor, key_source, path, error_kind=stale |
A re-read with unchanged content logs nothing.
Example: a locally signing gateway with an inline client assertion key set and a file-based end-user JWT key set:
security:
trust_material_dir: "/etc/agentic-identity-broker/trust"
token_exchange:
client_assertion:
issuer_uri: "https://agentgateway.internal.example.com"
jwks: |
{"keys":[{"kty":"EC","crv":"P-256","kid":"agentgateway-2026-10","alg":"ES256","use":"sig","x":"<base64url x>","y":"<base64url y>"}]}
server:
enduser:
authentication:
jwt:
verification: jwks
jwks_file: "/etc/agentic-identity-broker/trust/mesh-jwks.json"
See examples/config/trust-anchor-local-jwks.yaml for a complete configuration that also covers an impersonation trusted issuer.
OAuth2 Server Mode Configuration
oauth2_authorization_server
Description: Controls the broker's OAuth2 operating mode. Three symmetric modes are supported:
proxy: OAuth2 requests are forwarded to an upstream authorization server. The broker acts as a transparent proxy — it handles consent and delegation, then routes the final authorization to the upstream. No local token issuance; discovery andjwks_uriremain broker-hosted using upstream verification keys.local: The broker acts as a standalone OAuth2 authorization server, minting its own JWT access tokens signed with managed asymmetric keys. Supportsclient_credentialsandauthorization_code(with PKCE) grant types, and exposes RFC 8414 discovery and JWKS endpoints.hybrid: Both proxy and local paths coexist. Agents are classified by their properties: agents with an upstreamClientIDare routed to the proxy path; local agents (noClientID, noclient_uris) and CIMD agents (client_urisset) are issued local tokens. Requires bothproxyandlocalconfiguration sections.
Configuration block (nested under oauth2_authorization_server):
| Option | Type | Default | Valid Values | Required? | Environment Variable | CLI Flag | Description |
|---|---|---|---|---|---|---|---|
oauth2_authorization_server.mode | enum | — | proxy, local, hybrid | Yes (if any auth server field is set) | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_MODE | — | Operating mode. proxy forwards to upstream; local mints tokens locally; hybrid supports both. |
oauth2_authorization_server.proxy.upstream_issuer_uri | string | — | Valid HTTPS URI | Yes (if proxy or hybrid) | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_PROXY_UPSTREAM_ISSUER_URI | — | Upstream OAuth2 issuer URI. Used for proxy path routing. |
oauth2_authorization_server.proxy.upstream_authorize_endpoint | string | — | Valid HTTPS URI | Yes (if proxy or hybrid) | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_PROXY_UPSTREAM_AUTHORIZE_ENDPOINT | — | Upstream authorization endpoint. |
oauth2_authorization_server.proxy.upstream_token_endpoint | string | — | Valid HTTPS URI | Yes (if proxy or hybrid) | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_PROXY_UPSTREAM_TOKEN_ENDPOINT | — | Upstream token endpoint. |
oauth2_authorization_server.proxy.upstream_jwks_min_refresh | duration | 15m | Go duration (e.g. 30s, 5m, 1h) | No | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_PROXY_UPSTREAM_JWKS_MIN_REFRESH | — | Minimum interval between upstream JWKS refresh attempts. Applies a floor to the cache cadence derived from upstream cache headers. |
oauth2_authorization_server.proxy.upstream_jwks_max_refresh | duration | 1h | Go duration (e.g. 5m, 30m, 2h) | No | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_PROXY_UPSTREAM_JWKS_MAX_REFRESH | — | Maximum interval between upstream JWKS refresh attempts. Caps how stale the broker will allow upstream JWKS cache entries to become. |
oauth2_authorization_server.local.token_ttl | duration | 1h | Go duration (e.g. 30m, 2h) | No | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_LOCAL_TOKEN_TTL | — | Validity period for locally issued JWT access tokens. |
oauth2_authorization_server.local.token_claims_expression | string | "" | CEL expression | No | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_LOCAL_TOKEN_CLAIMS_EXPRESSION | — | CEL expression to inject custom claims into issued JWTs. |
oauth2_authorization_server.local.signing_keys.bootstrap_timeout | duration | 90s | Positive duration | No | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_LOCAL_SIGNING_KEYS_BOOTSTRAP_TIMEOUT | — | Startup budget for signing-key bootstrap in local/hybrid mode. Covers advisory locking, key generation, encryption, and persistence. |
oauth2_authorization_server.local.client_secret_hashing.max_concurrent | int | 2 | Positive integer | No | IDENTITY_BROKER_OAUTH2_AUTH_SERVER_LOCAL_CLIENT_SECRET_HASHING_MAX_CONCURRENT | — | Client secret hashes that may run at once in local/hybrid mode. Each Argon2id hash allocates 64 MiB, and further requests wait for a free slot. Plan about 128 MiB of container memory per slot. Each hash already uses up to four CPUs, so more slots add memory, not throughput. |
Required: oauth2_authorization_server.mode is mandatory — the broker rejects startup when the block is absent or mode is empty or invalid. Set mode to proxy, local, or hybrid before deploying.
Startup validation: The broker validates configuration at startup and rejects incompatible combinations — proxy-only fields in local mode, local-only fields in proxy mode, or missing sections in hybrid mode.
Proxy mode:
oauth2_authorization_server:
mode: "proxy"
proxy:
upstream_issuer_uri: "https://auth.example.com"
upstream_authorize_endpoint: "https://auth.example.com/oauth/authorize"
upstream_token_endpoint: "https://auth.example.com/oauth/token"
upstream_jwks_min_refresh: "15m"
upstream_jwks_max_refresh: "1h"
Local mode:
oauth2_authorization_server:
mode: "local"
local:
token_ttl: "1h"
token_claims_expression: '{"team": agent.display_name}'
signing_keys:
bootstrap_timeout: 90s
Hybrid mode (proxy and local agents coexist):
oauth2_authorization_server:
mode: "hybrid"
proxy:
upstream_issuer_uri: "https://auth.example.com"
upstream_authorize_endpoint: "https://auth.example.com/oauth/authorize"
upstream_token_endpoint: "https://auth.example.com/oauth/token"
upstream_jwks_min_refresh: "15m"
upstream_jwks_max_refresh: "1h"
local:
token_ttl: "1h"
signing_keys:
bootstrap_timeout: 90s
Security notes:
- PKCE is always enforced (S256 only) for authorization code grants. No plaintext challenge method.
- Client secrets are hashed with Argon2id and never stored in plaintext.
- Signing key private material is encrypted at rest via
EncryptionPort. - Authorization codes are single-use with 60-second TTL, stored as SHA-256 hashes.
- Signing key decryption failure prevents token issuance (fail-closed).
- In
proxymode, locally registered agents (noClientID) are rejected. Inlocalmode, proxy agents (withClientID) are rejected. Mode boundaries are strict.
See OAuth2 server modes for how the broker issues or proxies tokens.
Continuing consent for local refresh
Local refresh requires an active grant for the token's principal and agent. This also applies to locally issued tokens in hybrid mode.
Grant deletion, expiration, or replacement ends refresh authority. Renewal of expired consent revokes previous sessions. Credential deletion revokes all agent sessions. Agent deletion removes them.
Consent denial returns the existing invalid_grant response without tokens. Verifier or storage errors return server_error without rotating the token.
Grant and credential deletion precede session revocation because these repositories do not share the refresh transaction. A revocation error does not undo deletion.
If deletion commits but refresh cleanup fails, the audit event uses action=grant_deleted and refresh_cleanup=failed. The request still returns an error. Retrying DELETE returns 404 because the grant is already absent. The event does not report successful session cleanup.
Memory storage rejects a late successor after lifecycle revocation, including renewal that retains the grant ID. Revocation also marks consumed predecessors. Successor insertion and its predecessor check share one lock. PostgreSQL retains the rotation transaction and agent lock through insertion. Neither backend retracts access tokens from a refresh that completed before revocation.
Accepted limits:
- A row without
grant_idgets one rotation on the strength of an older active grant. Its successor stores the grant ID. - Old pods do not check consent or lock the agent first. Revocation can still deadlock while old binaries serve traffic during deployment.
- Existing access tokens remain valid until
token_ttlexpires.
OAuth2 User Impersonation Configuration
oauth2_authorization_server.impersonation
Description: Configures RFC 8693 user impersonation on POST /oauth2/token. In local mode only, exactly one request audience of <impersonation.audience_prefix>/<canonical lower-case AgentID UUID or canonical_id> selects a registered target agent before the third-party resource guard. Both forms resolve the same target and retain its UUID as minted agent_id; the target supplies local-token CEL agent.*, while the validated client assertion remains privileged-client authorization and audit material. Every eligible human-category request must also have an active UserGrant for the extracted subject identity and selected target agent. The extracted human subject is used unchanged as the grant principal; this remains mandatory for signed and unverified humans, with no configuration opt-out. Accepted ADR 045 narrowly exempts eligible signed self-agent requests, as described below. A missing or expired grant returns generic 403 access_denied plus error_uri=<server.enduser.public_url>/agents/<target-agent-id> so the privileged client can direct the authenticated user to the existing consent-management page. A grant lookup failure returns 500 server_error without error_uri. The routing prefix is never copied to an issued aud: token_claims_expression controls aud, including omission, exactly as normal local issuance. The subject role supports signed (verification: jwks) and broker-profile unverified (verification: none, ADR 031) JWTs.
Subject categories and self-agent issuance
rules[].allowed_subject_categories is an optional YAML-only string list, like the existing rule list. Omission alone means [human]. A present list must contain one or two distinct, case-sensitive values from human and agent; either dual order is valid. Empty lists, unknown/case-mismatched values, duplicates, explicit null, scalars, mappings, non-string entries and null entries fail startup. Errors identify oauth2_authorization_server.impersonation.rules[i].allowed_subject_categories, without credential content. Direct configurations enforce the same value invariants.
The broker classifies a request as agent only after the rule's signed subject credential verifies and its raw string sub exactly equals the audience-selected target's canonical UUID. An unsigned (verification: none) subject with that exact sub is agent only when the rule's authorization predicate binds subject_token.sub. The broker only checks that the predicate uses that field, so make it limit the agents, for example subject_token.sub in ['agent-uuid-a', 'agent-uuid-b']; the predicate is then the only control on which agent a client may represent, because agent issuance skips consent. subject_token.sub == request.agent_id does not limit anything: an agent request already requires sub to equal the target, which is request.agent_id. A sub or extracted subject principal that is the canonical UUID of a different registered agent is rejected on every rule as a cross-agent request, without falling back to human delegation. Everything else is human. No alias/case/UUID normalization, extracted-principal mapping, or request parameter selects agent issuance. Eligibility is checked separately, before CEL authorization and delegation. Ineligible rules fall through; no matching rule returns generic 403 access_denied without consent recovery. Signed exact-target equality is rejected on human-only/omitted rules even with active same-text delegation.
| Allowed categories | Human subject | Signed exact-target subject |
|---|---|---|
Omitted / [human] | Active delegation required | Ineligible |
[agent] | Ineligible | No delegation required |
[human, agent] / [agent, human] | Active delegation required | No delegation required |
Every principal extraction and the CEL predicate remain mandatory for agents; the minted sub and reserved agent_id are the target UUID, and act retains verified actor attribution. The broker skips agent email extraction, supplies no credential-derived email and creates no grant/session. Custom output claims, including policy-owned email and audience, retain their existing semantics. Human delegation failure is terminal, never agent fallback. Listing agent on an unsigned-subject rule is valid at startup but has no effect: [agent] cannot match it and [human, agent] acts as [human]. ADR 031's other controls are unchanged.
See the complete agent-subject configuration, human configuration, and examples index.
Agent-subject detection for third-party exchange
token_exchange.claim_extraction.agent_subject_expression is an optional CEL boolean over validated subject_token claims. Empty or omitted selects the exact guarded default:
has(subject_token.sub) && has(subject_token.agent_id) && subject_token.sub == subject_token.agent_id
The scalar setting supports YAML, IDENTITY_BROKER_TOKEN_EXCHANGE_CLAIM_EXTRACTION_AGENT_SUBJECT_EXPRESSION, and --token_exchange.claim_extraction.agent_subject_expression. Explicit CLI overrides environment, which overrides YAML, which overrides the evaluator default; an unchanged CLI flag does not shadow a lower source. Environment/CLI work with no YAML key or file. An explicitly empty highest-precedence value selects the evaluator default, not a lower source. Rule lists remain YAML-only.
The expression compiles once at startup. Syntax errors, undeclared variables, a static output type other than boolean/dynamic, or an expression that does not reference subject_token.sub fail startup. Evaluation uses the existing CEL timeout. Runtime errors/timeouts return 500 server_error (identity_resolution/cel_evaluation_failed); dynamic non-boolean output returns 500 server_error (identity_resolution/cel_configuration). Neither is treated as false. Matching tokens receive 403 access_denied (identity_resolution/subject_category_unsupported) without error_uri, before any grant/session/provider access. The default matches all broker agent-subject tokens, but also legacy human, mapped-principal/unsigned human and trusted external issuer tokens with equal sub/agent_id; fix identity collisions through the migration guide, not consent fabrication.
A non-empty override replaces the default entirely. A configured expression must reference subject_token.sub (field selection, has(), or subject_token["sub"]); otherwise startup fails with a CEL configuration error, so false, true, or an azp-only comparison cannot be configured. Keep broker agent-token matching, for example:
token_exchange:
claim_extraction:
agent_subject_expression: >-
(has(subject_token.sub) && has(subject_token.agent_id) &&
subject_token.sub == subject_token.agent_id) ||
(has(subject_token.token_use) && subject_token.token_use == 'machine')
Warning: A non-matching override that references sub (a detector without subject_token.sub, such as false, fails startup) removes categorical rejection and protection against same-text human-credential access. Existing checks may then consult matching human grants/sessions and return consent errors; this is not support for third-party agent identity or guaranteed exchange success. See token-exchange examples, concepts, reference, and token endpoint.
Configuration block (nested under oauth2_authorization_server, local mode only):
| Option | Type | Default | Valid Values | Required | Description |
|---|---|---|---|---|---|
impersonation.audience_prefix | URI | — | Absolute HTTP(S) URI; host required; no userinfo/query/fragment/trailing slash | Yes (CR-001) | Routing-only prefix. Exactly one <audience_prefix>/<canonical lower-case AgentID UUID or canonical_id> selects a registered target agent; it never sets the issued token aud. |
impersonation.rules | list | — | Non-empty, ordered | Yes (CR-002) | Ordered rules evaluated first-match. |
impersonation.rules[].name | string | — | Unique across rules | Yes (CR-003) | Operator-facing rule identifier. |
impersonation.rules[].allowed_subject_categories | list of strings | [human] when omitted | One or two distinct human/agent values | No | Category eligibility, independent of signed raw-sub classification; YAML-only. |
impersonation.rules[].roles | map | — | Keys client_assertion, actor, subject | Yes (CR-003) | Role semantics keyed by role name; all three roles MUST be defined. |
impersonation.rules[].roles.<role>.expected_audience | string | — | Non-empty string | Yes for signed roles without audience_requirement: absent | The aud each signed credential for this role must carry. Omit it for an unverified subject (verification: none) or when audience_requirement is absent. |
impersonation.rules[].roles.<role>.audience_requirement | enum | — | absent | No | Signed roles only. absent accepts only a credential with no aud claim and rejects a credential with an aud. It is mutually exclusive with expected_audience, forbidden on an unverified subject, and requires the authorization predicate to reference this role (CR-009). |
impersonation.rules[].roles.<role>.principal_expression | string | — | CEL expression | Yes | CEL extracting a non-empty identity from the role's token. |
impersonation.rules[].roles.subject.verification | enum | jwks | jwks, none | No | subject role only. jwks = signed subject validated against a trusted issuer; none = unverified unsigned alg:none subject JWT under subject_token_type=urn:ietf:params:oauth:token-type:jwt (broker profile extension, ADR 031). |
impersonation.rules[].roles.subject.email_expression | string | "" | CEL expression | No | CEL extracting a human subject's email; not evaluated for agents; subject role only. For an unverified subject the email is minted only when the authorization predicate binds subject_token.email (FR-006a). Omitted from the issued token when it evaluates empty. |
impersonation.rules[].trusted_issuers | list | — | Non-empty | Yes (CR-008) | Trust anchors permitted to sign this rule's credentials. |
impersonation.rules[].trusted_issuers[].issuer_uri | string | — | Valid URI | Yes | Matched against a credential's iss. |
impersonation.rules[].trusted_issuers[].jwks_uri | string | Discovered from issuer_uri | Valid URI | No | Explicit JWKS endpoint; discovered from issuer metadata when empty. |
impersonation.rules[].trusted_issuers[].jwks | string | — | Inline JWK Set JSON | No | Alternative to jwks_uri and discovery. Mutually exclusive with jwks_uri and jwks_file. Rejected with jwks_min_refresh or jwks_max_refresh. See Trust-Anchor Key Sets. |
impersonation.rules[].trusted_issuers[].jwks_file | string | — | Absolute path inside security.trust_material_dir | No | Alternative to jwks_uri and discovery. Mutually exclusive with jwks_uri and jwks. Re-read every jwks_min_refresh. |
impersonation.rules[].trusted_issuers[].jwks_min_refresh | duration | 15m | Non-negative Go duration | No | Minimum interval between JWKS refresh attempts. |
impersonation.rules[].trusted_issuers[].jwks_max_refresh | duration | max(jwks_min_refresh, 1h) | Non-negative Go duration | No | Maximum JWKS refresh interval. For jwks_file, the default is max(3 × jwks_min_refresh, 1h) when unset or below the minimum; remote sources use max(jwks_min_refresh, 1h). |
impersonation.rules[].trusted_issuers[].allowed_algorithms | list | — | Subset of RS256,RS384,RS512,PS256,PS384,PS512,ES256,ES384,ES512,EdDSA | Yes (CR-007) | Accepted signing algorithms; none and HS* are rejected. |
impersonation.rules[].trusted_issuers[].signs_roles | list | — | Subset of defined signed roles | Yes (CR-008) | Which signed roles this issuer may sign. |
impersonation.rules[].authorization.type | enum | — | cel | Yes (CR-005) | Authorization predicate type; reuses the token-exchange schema. |
impersonation.rules[].authorization.cel.expression | string | — | CEL expression | Yes (CR-005) | Predicate deciding the request; compiled at startup. |
impersonation.rules[].authorization.cel.evaluation_timeout | duration | 100ms | 10ms–5s | No (CR-005) | Per-evaluation CEL timeout. |
Startup validation: Each failure returns a ConfigError with an indexed field path. For
example: oauth2_authorization_server.impersonation.rules[1].trusted_issuers[0].allowed_algorithms.
- CR-001:
audience_prefixis an absolute HTTP(S) routing URI. It has a host and no userinfo, query, fragment, or trailing slash. One exact<audience_prefix>/<canonical lower-case AgentID UUID or canonical_id>enables impersonation. A bare or invalid suffix isinvalid_request. An unknown valid target isinvalid_target. - CR-002:
rulesis non-empty and ordered. The broker evaluates the first matching rule. - CR-003: Each rule has a unique
name. Itsroleskeys are a subset of{client_assertion, actor, subject}and define all three roles. Each role requiresprincipal_expression. Signed roles requireexpected_audience. An unverified subject must not use it. Onlysubjectcan useverificationandemail_expression. - CR-004: The local
issuer_urimust not be a trusted issuer forclient_assertion. - CR-005:
authorization.typeiscel. Its expression is required and compiles at startup.authorization.cel.evaluation_timeoutmust be 10ms through 5s. The default is 100ms. - CR-005a: Unverified subject mode is disabled unless a rule declares
verification: none. Its authorization predicate must referencesubject_token. The broker checks this at startup. It can issue a caller-provided email only when the predicate bindssubject_token.email. - CR-006: The broker rejects
impersonationoutside local mode. - CR-007:
allowed_algorithmsis a non-empty subset of{RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA}. The broker rejectsnoneandHS*. This rule governs signed credentials only. It does not validate the unsigned unverified subject. - CR-008: Within a rule,
issuer_uriis unique acrosstrusted_issuers. Everysigns_rolesentry is a defined signed role. At least one trusted issuer covers every signed role:client_assertion,actor, and a signedsubject. An unverified subject must not appear insigns_roles. The sameissuer_urican appear in different rules. - CR-010: Impersonation requires the user-delegation verifier and
server.enduser.public_url. The broker does not start without both. It has no bypass mode.
Example (local mode; the same issuer signs the credentials of two signed rules):
oauth2_authorization_server:
mode: "local"
local:
issuer_uri: "https://broker.example.com"
token_ttl: "1h"
# Routing never sets aud. The existing local token policy may emit it.
token_claims_expression: '{"aud": "https://policy.example.com"}'
impersonation:
audience_prefix: "https://broker.example.com/impersonation"
rules:
- name: "internal-gateway-signed-subject"
roles:
client_assertion:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "client_assertion.sub"
actor:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "actor_token.sub"
subject:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "subject_token.sub"
email_expression: "has(subject_token.email) ? subject_token.email : ''"
trusted_issuers:
- issuer_uri: "https://idp.internal.example.com"
jwks_min_refresh: "15m"
jwks_max_refresh: "1h"
allowed_algorithms: ["ES256", "RS256"]
signs_roles: ["client_assertion", "actor", "subject"]
authorization:
type: cel
cel:
expression: |
client_assertion.iss == "https://idp.internal.example.com" &&
client_assertion.sub in ["gateway-prod", "gateway-staging"]
evaluation_timeout: "100ms"
- name: "partner-gateway-signed-subject"
roles:
client_assertion:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "client_assertion.sub"
actor:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "actor_token.sub"
subject:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "subject_token.sub"
trusted_issuers:
- issuer_uri: "https://idp.internal.example.com"
jwks_min_refresh: "15m"
jwks_max_refresh: "1h"
allowed_algorithms: ["ES256", "RS256"]
signs_roles: ["client_assertion", "actor", "subject"]
authorization:
type: cel
cel:
expression: |
client_assertion.sub == "partner-gateway-prod"
evaluation_timeout: "100ms"
Request contract (POST /oauth2/token): Send
grant_type=urn:ietf:params:oauth:grant-type:token-exchange and one
audience=<impersonation.audience_prefix>/<canonical lower-case AgentID UUID or canonical_id>.
Send client assertion, actor, and subject credentials. The suffix must identify a registered
target agent. requested_token_type is optional. When present, it must be the access-token
type. resource must be absent. scope is optional and uses literal-space-separated values.
Each non-reserved scope must be in the resolved target allowed_scopes. An empty allow list
permits all non-reserved scopes. offline and offline_access are reserved and permitted.
The response and minted token contain a non-empty granted scope. The target UUID and agent
always supply agent_id and local CEL agent.*, regardless of identifier form. Local policy
controls aud.
Unverified subject rule (broker profile extension, ADR 031): The subject role uses
verification: none and has no expected_audience. It does not occur in any issuer
signs_roles. The authorization predicate must reference subject_token. It must bind
subject_token.email before the broker mints a caller-provided email:
- name: "chat-bridge-unverified-subject"
roles:
client_assertion:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "client_assertion.sub"
actor:
expected_audience: "https://broker.example.com/impersonation"
principal_expression: "actor_token.sub"
subject:
verification: "none"
principal_expression: "subject_token.sub"
email_expression: "has(subject_token.email) ? subject_token.email : ''"
trusted_issuers:
- issuer_uri: "https://idp.internal.example.com"
allowed_algorithms: ["ES256", "RS256"]
signs_roles: ["client_assertion", "actor"]
authorization:
type: cel
cel:
expression: |
client_assertion.sub == "chat-bridge" &&
subject_token.sub != "" &&
has(subject_token.email) && subject_token.email.endsWith("@example.com")
evaluation_timeout: "100ms"
For the unverified subject, send subject_token_type=urn:ietf:params:oauth:token-type:jwt and subject_token=<unsigned alg:none JWT carrying sub and optional email>; all other request parameters are identical to the signed contract above.
Observability / OpenTelemetry
The Identity Broker supports configurable OpenTelemetry (OTel) tracing, metrics, and logging export via OTLP (gRPC or HTTP). When disabled (default), the OTel SDK is never initialized and there is zero overhead.
Quickstart
See examples/config/telemetry.yaml for a complete production-ready example. The minimal configuration to enable tracing:
telemetry:
enabled: true
service_name: agentic-identity-broker
exporter:
protocol: grpc
endpoint: otel-collector:4317
insecure: true # only for non-production environments
OTel Configuration Reference
| Parameter | Type | Default | Environment Variable | Description |
|---|---|---|---|---|
telemetry.enabled | bool | false | IDENTITY_BROKER_TELEMETRY_ENABLED | Master switch. When false, no OTel SDK is initialized and overhead is zero. |
telemetry.service_name | string | "agentic-identity-broker" | IDENTITY_BROKER_TELEMETRY_SERVICE_NAME | Value for the service.name OTel resource attribute. Appears on all spans, metrics, and logs. |
telemetry.resource_attributes | map[string]string | {} | N/A (config file only) | Additional OTel resource attributes added to every telemetry signal (e.g., deployment.environment: production). |
telemetry.traces.enabled | bool | true (when telemetry.enabled) | IDENTITY_BROKER_TELEMETRY_TRACES_ENABLED | Enable trace export. Disabling traces suppresses otelchi HTTP spans and storage child spans. |
telemetry.traces.sampling_rate | float64 | 1.0 | IDENTITY_BROKER_TELEMETRY_TRACES_SAMPLING_RATE | Fractional sampling rate for traces (0.0–1.0). 1.0 = 100% sampled. Uses ParentBased(TraceIDRatioBased(rate)). |
telemetry.traces.propagators | []string | ["ottrace","b3multi","baggage"] | N/A (config file only) | Propagators to register globally. Supported values: ottrace (OpenTracing interop), b3multi (Zipkin B3 multiple headers), b3 (B3 single header), tracecontext (W3C), baggage (W3C). Propagators are registered unconditionally when telemetry is enabled, even if traces.enabled is false. |
telemetry.metrics.enabled | bool | true (when telemetry.enabled) | IDENTITY_BROKER_TELEMETRY_METRICS_ENABLED | Enable metrics export. When enabled, process runtime metrics (goroutines, memory, GC) and HTTP request metrics (via otelchi) are exported automatically, along with the session refresh and sweep instruments and the oauth2_sessions gauge when token_refresh.health_metrics.enabled is true. |
telemetry.metrics.export_interval | duration | 30s | IDENTITY_BROKER_TELEMETRY_METRICS_EXPORT_INTERVAL | How often metrics are pushed to the collector. Must be positive. Example: 30s, 1m. |
telemetry.logs.enabled | bool | true (when telemetry.enabled) | IDENTITY_BROKER_TELEMETRY_LOGS_ENABLED | Enable OTLP log export via the slog bridge. Set to false if the collector does not support opentelemetry.proto.collector.logs.v1.LogsService. |
telemetry.exporter.protocol | string | "grpc" | IDENTITY_BROKER_TELEMETRY_EXPORTER_PROTOCOL | OTLP transport protocol. Accepted values: grpc, http, https. The https protocol uses the HTTP OTLP exporter with TLS; bare host:port endpoints are auto-prefixed with https://. |
telemetry.exporter.endpoint | string | "" | IDENTITY_BROKER_TELEMETRY_EXPORTER_ENDPOINT | OTLP collector endpoint. Required when telemetry.enabled=true. Format: host:port for gRPC, http(s)://host:port for HTTP/HTTPS. |
telemetry.exporter.headers | map[string]string | {} | N/A (config file only) | Additional HTTP/gRPC headers sent with every export request (e.g., authentication tokens). Use ${ENV_VAR} substitution to avoid committing secrets. |
telemetry.exporter.timeout | duration | 10s | IDENTITY_BROKER_TELEMETRY_EXPORTER_TIMEOUT | Per-export request timeout. Must be positive. Example: 5s, 30s. |
telemetry.exporter.compression | string | "none" | IDENTITY_BROKER_TELEMETRY_EXPORTER_COMPRESSION | Payload compression for all OTLP exporters. Accepted values: none, gzip. |
telemetry.exporter.insecure | bool | false | IDENTITY_BROKER_TELEMETRY_EXPORTER_INSECURE | Disable TLS for the OTLP exporter. Do not use in production — a startup warning is emitted when this is true. Only meaningful for gRPC; for HTTP the URL scheme controls TLS. |
For HTTP/HTTPS, configure the collector URL without a signal path. AIB sends traces to /v1/traces, metrics to /v1/metrics, and logs to /v1/logs on that collector. Do not append a signal path to the shared endpoint. All three exporters use the same endpoint configuration.
Environment Variable Mapping
Scalar OTel settings with environment bindings accept the IDENTITY_BROKER_TELEMETRY_ prefix. Resource attributes and exporter headers use YAML maps, not whole-map environment strings. For example:
IDENTITY_BROKER_TELEMETRY_ENABLED=true
IDENTITY_BROKER_TELEMETRY_EXPORTER_ENDPOINT=otel-collector:4317
IDENTITY_BROKER_TELEMETRY_EXPORTER_INSECURE=true
IDENTITY_BROKER_TELEMETRY_SERVICE_NAME=my-broker-instance
Literal Map Keys
Both the broker and ExtProc preserve literal dots in resource attribute names and exporter header names:
telemetry:
resource_attributes:
service: broker
service.namespace: production
service.version: release
exporter:
headers:
x.vendor.token: ${OTLP_TOKEN}
x.vendor.region: eu
service and service.namespace remain separate string keys. Nested maps where string values are expected cause a decoding error. Missing maps and explicit {} maps decode to nil, which consumers treat as empty.
The broker expands ${VAR} in both maps. ExtProc expands exporter header values but leaves resource attribute values unchanged.
The shared internal/configutil utility reserves :: for structural Viper paths. It does not own service defaults or loading policy. For caller-created ExtProc instances, call configutil.NewViper() before populating the instance for LoadFromViper. Address a literal leaf with configutil.Key("telemetry", "resource_attributes", "service.namespace").
Nested YAML and public CLI names remain unchanged. For example, ExtProc keeps --telemetry.exporter.endpoint and EXTPROC_TELEMETRY_EXPORTER_ENDPOINT. The broker keeps IDENTITY_BROKER_TELEMETRY_EXPORTER_ENDPOINT. Source metadata continues to show readable dotted paths.
ExtProc replaces both :: and literal dots with underscores for environment lookup. Known YAML leaf keys retain their existing environment overrides. This conversion is not unambiguous for arbitrary map keys. Keys containing :: are outside this contract. Viper remains case-insensitive and does not preserve attribute-name case.
What is Instrumented
When tracing is enabled, the following operations emit child spans:
| Span Name | Operation | Key Attributes |
|---|---|---|
| HTTP span (per route) | All inbound HTTP requests (via otelchi) | http.method, http.route, http.status_code |
storage.get.agent | PostgreSQL AgentRepository.Get | db.system=postgresql, db.operation=GetAgent |
storage.list.user_grants_by_principal | PostgreSQL UserGrantRepository.ListByPrincipal | db.system=postgresql, db.operation=ListByPrincipal |
storage.create.user_grant | PostgreSQL UserGrantRepository.Create (upsert) | db.system=postgresql, db.operation=CreateUserGrant |
storage.*.tool_approval, storage.*.approval_sync | PostgreSQL tool approval and approval sync repositories | db.system=postgresql, db.operation |
encryption.encrypt | AWS KMS envelope encryption | encryption.key_type=aws_kms |
encryption.decrypt | AWS KMS envelope decryption | encryption.key_type=aws_kms |
jwks.fetch | JWKS key set fetch/cache lookup | error.type on failure |
oauth2.token_proxy | Upstream OAuth2 token proxy | http.request.method=POST, http.response.status_code |
storage.* spans have span kind Client. A failed database call, or a stored value that cannot be decoded, sets the error status and error.type, with the value connection, timeout, or unknown.
These results are not failures. They leave the status unset and set storage.outcome:
storage.outcome | Meaning |
|---|---|
not_found | No matching row, or a referenced row does not exist |
conflict | The write conflicts with existing data |
validation | The repository rejected the input |
canceled | The caller canceled the operation |
Security notes
telemetry.exporter.insecuredefaults tofalse. TLS is required by default. The broker records a startup warning when this value is true.- Span attributes do not contain tokens, encryption keys, PII, credentials, or request body content.
- Use
${VAR_NAME}substitution fortelemetry.exporter.headersvalues such as API keys. This prevents secrets from entering configuration files.
Getting help
- Read error messages. They include correction instructions.
- Read the startup summary to identify loaded sources.
- Read
ARCHITECTURE.mdfor the configuration subsystem architecture. - Make sure that each configuration path is absolute or relative to the current working directory.
- Make sure that
GO_ENVhas the expected environment value. - Read
adrs/002-configuration-libraries.mdfor the configuration library decision.