IAM Configuration
iam.svc reads configuration from three planes: a bootstrap YAML file loaded from disk at
process start, a cluster node and a tenant node. Both resolved through the chassis config
client, and, per tenant, a set of product definition files delivered through
meta.
Where each key lives
Section titled “Where each key lives”| Plane | Source | Scope | Changes at runtime |
|---|---|---|---|
| Boot | The -f YAML file (default .iam.yaml) | The process | No: restart |
| Cluster | The config client’s resolved cluster node, with the bootstrap merged on top | Every tenant this process serves | Yes, on config refresh |
| Tenant | The config client’s resolved tenant node, keyed by CEPT | One tenant | Yes: refresh drains that tenant’s scope |
| Product | iam/*.yaml files fetched from meta, plus tenant_extensions rows | One tenant | Yes. A product change drops the tenant’s cached engine |
The bootstrap file is merged over the fetched cluster node, so any key you set top-level in
the bootstrap wins over the config server. That is why jwks.issuers.iam and bootstrap.token
work when written at the top level of the file even in remote-config deployments.
Boot order
Section titled “Boot order” 1. tenancy init (product iam, multi-tenant) + config-refresh hooks 2. load the -f YAML (or the in-memory map) 3. environment init: merge datacenter/cluster/sid → resolve zerotrust → resolve cert paths → resolve port (fatal if empty) → fold config-server identity into `config:` → merge CLI flags → create logger and apply log.level → goroutine pool → register TLS → optional vault client → service-discovery client (retried) → config client (retried) → serviceCfg = resolved cluster node + bootstrap on top → register vault + meta clients 4. boot config load: surface gate → build the embedded base schema (namespace iam) → split the superadmin plane → configure the schema source → register the API providers 5. assemble the middleware chain: security headers → 1 MiB body cap → request timeout → tenant resolution → token validation → router 6. register routes (GET /ready and GET /health are registered first) 7. mark healthy + ready, bind the listener 8. block on SIGINT / SIGTERMNothing touches PostgreSQL during boot. Connection pools, engines, per-tenant script runtimes and the composed behavior config are built on the first request that names a CEPT.
Boot-plane keys
Section titled “Boot-plane keys”Identity and listener
Section titled “Identity and listener”| Key | Type | Default | Effect |
|---|---|---|---|
name | string | — | Present in every shipped profile as name: iam, but no reader. Documentary only. |
host | string | "" (binds all interfaces) | Listener bind address. |
port | string or int | required | Listener port. Empty in both flag and YAML aborts boot with invalid/empty port provided. |
datacenter | string | default | Deployment tag: log field, service-discovery scoping, config-client datacenter, request context. |
cluster | string | default | Same, for the cluster axis. |
sid | string | <hostname>-<service>-<port> | Unique instance id: log tag, service-discovery registration id, config instance leaf. |
instance | string | falls back to sid | The config client’s instance leaf, the deepest config resolution level. |
labels | map[string]string | — | Cohort labels passed to the config client for match/cohort resolution. |
tags | map[string]string | — | Legacy alias for labels. labels wins. |
Transport security
Section titled “Transport security”| Key | Type | Default | Effect |
|---|---|---|---|
zerotrust | bool | true | When true the listener resolves a TLS config at start and fails if cert material is missing. Also gates outbound mTLS. |
svccert | string | "" | Service certificate path. TLS is configured only when both svccert and svckey are non-empty. |
svckey | string | "" | Service private key path. |
rootcacert | string | "" | Root CA certificate path for the trust chain. |
rootcakey | string | "" | Root CA key path. No CLI flag. |
insecureskipverify | bool | false | Skip peer certificate verification. |
auto_renew_certificates | bool | true | Chassis certificate auto-renewal. |
jwt_public_key | string | "" | File path loaded into the chassis TLS object as the JWT public key. Unrelated to the per-tenant signing keyring. |
jwt_private_key | string | "" | File path loaded as the chassis JWT private key. |
Token verification
Section titled “Token verification”| Key | Type | Default | Effect |
|---|---|---|---|
jwks.issuers.<issuer> | map[string]string | — | Maps a token iss value to the base URL whose /.well-known/jwks.json is fetched to verify signatures. |
jwks.issuers.iam must point at this service’s own externally reachable URL. Without it, a
token this service just minted is rejected on its own /rest and /admin routes with
jwks: no configured URL for issuer "iam" in the log.
Logging and runtime
Section titled “Logging and runtime”| Key | Type | Default | Effect |
|---|---|---|---|
log.level | string | error | Sets the service logger and the root logger level. |
routinepool.size | int | 100 | Global goroutine-pool size. The pool that runs the HTTP serve loop and scheduled work. |
routinepool.queue | int | 100 | Goroutine-pool queue depth. |
routinepool.spawn | int | 50 | Goroutine-pool spawn count. |
Config source
Section titled “Config source”The config: block alone selects the mode. config.url selects remote delivery from
config.svc over SSE; config.path
selects a local folder of flat-path YAML; neither means the bootstrap file’s own hives:
block is the config source. config.url wins if both are set, and the ignored path is logged.
| Key | Type | Default | Effect |
|---|---|---|---|
config.url | string | "" | Remote config server base URL. Requires config.apikey or svccert, otherwise boot aborts with error: either config.apikey or svccert should be present. |
config.path | string | "" | Local folder of flat-path YAML. Ignored with a warning when config.url is set. |
config.apikey | string | "" | API key for the remote config server. |
config.cache | string | "" | Path for the warm-start config cache. |
Dependency clients
Section titled “Dependency clients”| Key | Type | Default | Effect |
|---|---|---|---|
boot.retry_window | duration or int seconds | 90s | How long each boot-time client init (service discovery, config, vault, meta) retries with exponential backoff (1s doubling to a 10s cap) before failing loud. |
vault.url | string | "" | Master switch for vault. Empty means no vault client is registered at all. |
vault.apikey | string | "" | API key for the vault client. |
vault.file | string | "" | Local vault file, used when vault.url is empty. |
vault.<auth block> | map | — | The whole vault: map is handed to the vault client initializer, so token:, approle: {roleid, secretid} and kubernetes: sub-blocks are consumed there. |
meta.url | string | "" | Meta URL for the service’s own boot-plane meta client. |
meta.apikey | string | "" | API key for the boot-plane meta client. |
meta.dir | string | "" | Local directory for the boot-plane meta client, used when meta.url is empty. |
servicediscovery_url | string | "" | Service-registry URL. When set, either servicediscovery_apikey or svccert must be present or boot aborts. |
servicediscovery_apikey | string | "" | API key for the service registry. |
With vault.url empty: database pool passwords are used as literals, a jwt.signing.key naming
a vault secret is a hard error, and field encryption at rest is silently disabled.
HTTP server timeouts
Section titled “HTTP server timeouts”Each accepts a bare integer of seconds or a Go duration string.
| Key | Type | Default | Effect |
|---|---|---|---|
timeouts.read | duration or int seconds | 15s | http.Server read timeout. |
timeouts.write | duration or int seconds | 15s | http.Server write timeout. |
timeouts.readheader | duration or int seconds | 5s | http.Server read-header timeout. |
Per-request deadlines come from the chassis timeout middleware, not from a handler wrapper.
Surface gate
Section titled “Surface gate”| Key | Type | Default | Effect |
|---|---|---|---|
v1-only | bool | false | Disables the entire route surface. |
Keys that do nothing
Section titled “Keys that do nothing”| Key | Where it appears | Reality |
|---|---|---|
enablecache | All four server-shaped profiles | No reader anywhere. Inert. |
telemetry.enabled | The full profile | Telemetry is never initialized on this boot path. No OTLP export of logs, metrics or traces happens, regardless of the value. |
Observability is structured logs only, the service ships metric definitions for the auth flows, but nothing records them and the global meter provider is a no-op, so do not treat them as an operator-facing contract.
CLI flags
Section titled “CLI flags”| Flag | Maps to | Default |
|---|---|---|
-f, --config | the bootstrap file path | .iam.yaml |
-p, --port | port | — |
-l, --loglevel | nothing at serve time (see above) | error |
--sid | sid | — |
--datacenter | datacenter | — |
--cluster | cluster | — |
--config_url | config.url | — |
--config_apikey | config.apikey | — |
--servicediscovery_url | servicediscovery_url | — |
--servicediscovery_apikey | servicediscovery_apikey | — |
--svccert | svccert | — |
--svckey | svckey | — |
--rootcacert | rootcacert | — |
--zerotrust | zerotrust | — |
--jwt_public_key | jwt_public_key | — |
--jwt_private_key | jwt_private_key | — |
Cluster-plane keys
Section titled “Cluster-plane keys”Read through the config client from the resolved cluster node, the bootstrap file merges over that node, so all four may also be written top-level in the bootstrap.
| Key | Type | Default | Effect |
|---|---|---|---|
bootstrap.token | string | "" | One-shot secret gating POST /auth/bootstrap (first-superadmin creation). Empty disables the endpoint. Compared in constant time; the empty string is always rejected. |
notify.url | string | "" | Base URL of notify.svc for password-reset and magic-link code delivery. Unset means a no-op sink: the endpoints still return 200 and the codes are dropped. |
reuseport | bool | true | Bind the listener with SO_REUSEPORT. |
realtimedebug | bool | false | When true, the tenant middleware honours per-request debug switches (_debug_log_ query parameter or X-Debug-Log header). |
api.key | string | "" | API key used when building a tenant’s remote meta client. Best-effort, the client falls back to vault-issued JWT auth when empty. |
Tenant-plane keys
Section titled “Tenant-plane keys”One node per tenant, addressed by CEPT (customer:env:product:tenant). Nodes resolve by
prefix-merge, so a node at path: default also satisfies lookups for
default:dev:iam:default. Reserved names are superadmin and shared; a tenant slot of
superadmin addresses the control plane for that
customer, product and environment.
Node identity
Section titled “Node identity”| Key | Type | Effect |
|---|---|---|
name | string | Tenant display name on the node. |
active | bool | Tenant active flag. |
domain | list[string] | Domains for domain-based tenant resolution upstream. |
Datastores and pools
Section titled “Datastores and pools”| Key | Type | Default | Effect |
|---|---|---|---|
datastores.<name>.dbpool | string | — | Names which pool the datastore uses, the service’s default datastore is main; a request may select another with ?datastore=<name>. |
dbpools.<name>.type | string | postgres (empty and postgresql both resolve to it) | Backend dialect. Only postgres has a pool factory here; anything else fails the build. |
dbpools.<name>.dbserver | string | required | host:port; a bare host defaults to :5432. |
dbpools.<name>.database | string | required | Database name. The database must already exist. This service never creates databases. |
dbpools.<name>.dbuser | string | required | Database user. |
dbpools.<name>.password | string | "" | A vault secret name when a vault client is registered, otherwise a literal password. |
dbpools.<name>.schema | string | the tenant slot name | The physical PostgreSQL schema every statement, the migration ledger and the extensions are qualified with. |
dbpools.<name>.maxconnections | int | backend default | Pool max open connections. |
dbpools.<name>.connect_timeout | duration or int seconds | backend default | Connect timeout. |
dbpools.<name>.connection_life_time | duration | backend default | Max connection lifetime, e.g. 30m. |
dbpools.<name>.isolation | string | "" | dedicated or shared: declared isolation posture. |
dbpools.<name>.tlsconfig | string | "" | disable, require, verify-ca or verify-full. |
dbpools.<name>.dsn | string | "" | Explicit DSN escape hatch; bypasses the dbuser/dbserver/database validation. |
hives: tenant: - path: default name: default active: true domain: - localhost - 127.0.0.1 datastores: main: dbpool: mainpool dbpools: mainpool: type: postgres dbserver: localhost:5432 database: erpdb dbuser: erpadmin password: erpadmin schema: iam maxconnections: 10 connect_timeout: 5Signing key
Section titled “Signing key”| Key | Type | Default | Effect |
|---|---|---|---|
jwt.signing.key | string | absent | A PKCS#8 Ed25519 PEM literal (the value starts with -----BEGIN), or the name of a vault secret holding that PEM. Absent means a keypair is generated into the tenant’s own jwt_signing_key table on first use. |
Externally sourced keys are never written to the database, and their key id is the RFC 7638 JWK thumbprint. Naming a vault secret with no vault client registered is a hard error, not a silent fallback to self-minting. Rotation of a generated key exists only as an internal API, there is no endpoint and no schedule.
Session posture
Section titled “Session posture”The tenant node’s session: block is the highest-precedence behavior layer. It wins over the
product and tenant-extension files.
| Key | Type | Default | Effect |
|---|---|---|---|
session.preset | string | standard | standard, hipaa or fedramp. Selects model, crypto suite and timeout defaults, an unknown value falls back to standard. |
session.model | string | preset-derived | Override: stateless or managed. |
session.crypto_suite | string | preset-derived | Override: v4 or v3. |
session.max_concurrent_sessions | int | 0 (unlimited) | Cap on concurrent sessions per principal. 1 is single-session. Enforced atomically under an advisory lock keyed on tenant and principal. |
session.on_limit | string | evict_oldest | reject (the new login fails) or evict_oldest (the new login evicts the oldest session). |
session.idle_timeout_s | int seconds | 0 standard / 900 hipaa / 600 fedramp | Idle timeout, enforced at exchange. |
session.absolute_expiry_s | int seconds | 0 standard / 43200 hipaa / 28800 fedramp | Absolute session lifetime. |
Preset resolution: standard is stateless with suite v4 and no timeouts; hipaa is managed
with suite v4, 15-minute idle and 12-hour absolute; fedramp is managed with suite v3,
10-minute idle and 8-hour absolute. Explicit fields always win over preset defaults.
Managed mode changes the login response, see Sessions.
Product source
Section titled “Product source”| Key | Type | Default | Effect |
|---|---|---|---|
meta.url | string | "" | Meta base URL supplying this tenant’s product layer. |
meta.localdir | string | "" | The product repository on disk (or a folder of products, products/<product>/), read exactly as meta serves it: the development path for the product layer. |
meta.isexternal | bool | false | Marks the meta source as external. |
With neither set, the tenant runs on the service’s base schema and built-in behavior. That is a normal condition, not an error.
Either way the service reads the same files of the product: the seven definition files below under
iam/, and the product layer of its schema, the entity files under iam/extend/.
The seven product definition files
Section titled “The seven product definition files”A product ships behavior for this service as seven files under iam/ in its meta tree. Every one
is optional.
| File | Declares | Keys |
|---|---|---|
iam/realms.yaml | Authentication realms and feature gates | realms.<name>.entity, .active, .default, .providers; realmconfig.enable.* |
iam/providers.yaml | Auth-method declarations | providers[].name, .type, .template |
iam/token.yaml | Access-token policy | token.expiry, token.claims.<name>, token.claims_from.<name> |
iam/access.yaml | Named authorization predicates | authorization.<rule>.language, .rule, .rules |
iam/config.yaml | Free-form service properties | properties.* |
iam/session.yaml | Session posture | the session.* keys above |
iam/apps.yaml | App connections | apps.enabled, apps.token_ttl, apps.keys.default_ttl, apps.keys.max_ttl, apps.scopes.<name>.description, .requires, .roles, .tools |
The parser accepts every one of these top-level keys in any of the seven files. The file name is used only in error messages. The split is a convention.
| Key | Type | Default | Effect |
|---|---|---|---|
realms.<name>.entity | string | users | The schema entity holding this realm’s identities. |
realms.<name>.active | bool | true | Inactive realms refuse login. |
realms.<name>.default | bool | false | Marks the realm used when a request names none. |
realms.<name>.providers | list[string] | [password] | Provider names this realm allows. |
realmconfig.enable.agents | bool | true | false removes the iamagents entity from the tenant’s composed schema entirely. |
realmconfig.enable.bots | bool | true | false removes iambots. |
realmconfig.enable.delegations | bool | true | false removes iamdelegations. |
providers[].name | string | — | Provider name a realm can list. |
providers[].type | string | — | challenge (password) or magic (magic link). |
providers[].template | string | — | Notification template for a magic provider. |
token.expiry | duration string | 15m handler default | Access-token lifetime. A parse failure refuses the engine build. |
token.claims.<name> | any | — | Static custom claims stamped into every access token. |
token.claims_from.<name> | string | — | Maps a claim to a dotted path on the authenticated identity row, e.g. department: properties.department, a missing path omits the claim; an empty path is a parse error. |
authorization.<rule>.language | string | expr | Expression language for the predicate. |
authorization.<rule>.rule | string or bool | — | The predicate body. |
authorization.<rule>.rules | list[string] | — | Multi-predicate form, evaluated as OR. |
properties.* | map | — | Free-form properties kept as a map for consumers that understand them. |
Disabling a feature removes the surface completely: /rest/iamagents returns 404 and migration
plans omit the table.
How the layers compose
Section titled “How the layers compose”Two independent compositions run per tenant on the first request.
Entity schema, three layers:
embedded base schema (namespace iam) ⊕ product layer (entities and fields from meta) ⊕ tenant_extensions rows (the tenant's own table) → feature gating → scope subsettingLayers add entities and fields and never shadow a base field, a missing tenant_extensions
table is tolerated as a fresh tenant and the extensions activate on the next build, a malformed
or base-shadowing extension row fails the build for that tenant.
Behavior, four layers, last wins:
built-in defaults (one active `users` realm over the users entity, password provider, all features on, standard session) ⊕ product iam/<type>.yaml files ⊕ tenant_extensions rows at /token.yaml /realms.yaml /providers.yaml /access.yaml /config.yaml /session.yaml /apps.yaml ⊕ the tenant node's `session:` block → session preset resolutionA missing layer is normal. A malformed layer refuses the engine build, because serving a tenant without behavior it declared would silently change authentication semantics.
The control plane takes neither composition: it serves the plane schema alone, with no product layer and no tenant extensions.
Each build logs a breadcrumb, pool built, tenant overlay applied, behavior composed, engine
built, at info level, so raise log.level when diagnosing which layers a tenant actually got.
Field encryption at rest
Section titled “Field encryption at rest”Encryption hooks (AES-256-GCM) for fields marked as encrypted, TOTP shared secrets and
per-tenant signing-key PEMs, are installed only when a vault client
is registered for the service, the field’s declared key name is the vault secret name, defaulting
to default; its value is any passphrase, normalised with SHA-256 to a 32-byte key and cached
per key name for the process lifetime. Rotation requires a restart.
There is exactly one listener, at host:port. All five shipped profiles use 5030. There is no
metrics port, no pprof port and no admin port. The operator surfaces are routes on the same
listener. TLS is configured on it only when zerotrust is true.
iam service started at 127.0.0.1:5030Health and readiness
Section titled “Health and readiness”| Route | Auth | Success | Failure |
|---|---|---|---|
GET /ready | none | 200, body ready:true | 500, body ready:false |
GET /health | none | 200, JSON body | 500 |
{ "healthy":true,"dependencies": {}, "memstats": { "Alloc":2056, "HeapAlloc":2056, "HeapSys":2067, "HeapIdle":9, "HeapInUse":2058, "TotalAlloc":2058, "Sys":2077, "NumGC":3}, "version": "" }Both are registered before the routes and are exempt from the tenant middleware, so they need no
CEPT headers. Memory figures are in MB. The version field is always empty, nothing sets it.
Graceful shutdown
Section titled “Graceful shutdown”SIGINT or SIGTERM runs:
Stop signal received → close the HTTP listener → drain per-tenant scopes (engine close → script runtime close → pool close) → stop tenant meta client monitors → drain the shared scope, sweep orphaned pools, close the script schedulershutdown complete.Config profiles
Section titled “Config profiles”Five profiles are embedded in the binary.
iam.svc config listiam.svc config generate bootstrap-dev > .iam.yamliam.svc config generate all -d /etc/iam| Profile | Shape |
|---|---|
bootstrap-dev | Local single-file boot: embedded tenant hive, local PostgreSQL, zero-trust off. |
bootstrap-managed-session | The dev profile with the hipaa preset on the tenant node. |
bootstrap-config-server | Tenants, datastores and pools come from the config server, not inline. |
bootstrap-vault | Vault-backed database credentials plus zero-trust mTLS; pool passwords are vault secret names. |
bootstrap-full | Config server, vault, zero-trust mTLS and service discovery together. |
The reserved name all writes every profile and requires -d. Each profile’s one-line
description in config list is taken from the file’s first comment line, so a longer comment is
truncated mid-sentence.
For the routes these keys govern, see the endpoint reference; for the failures they produce, see Errors.