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 CPET | 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 CPET.
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 CPET (customer:product:env:tenant). Nodes resolve by
prefix-merge, so a node at path: default also satisfies lookups for
default:iam:dev: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 | "" | Local directory mirroring meta’s path layout — 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.
The local-directory layout is:
<dir>/products/<product>/service/iam/config data.defaultdatastore data.datastore.<ds>.files.data: [<yaml>, …]<dir>/products/<product>/service/iam/type/<token|realms|providers|access|config|session>/fileThe six product definition files
Section titled “The six product definition files”A product ships behavior for this service as six 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 |
The parser accepts every one of these top-level keys in any of the six 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 ⊕ 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
CPET 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.