Skip to content
Talk to our solutions team

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.

PlaneSourceScopeChanges at runtime
BootThe -f YAML file (default .iam.yaml)The processNo — restart
ClusterThe config client’s resolved cluster node, with the bootstrap merged on topEvery tenant this process servesYes, on config refresh
TenantThe config client’s resolved tenant node, keyed by CPETOne tenantYes — refresh drains that tenant’s scope
Productiam/*.yaml files fetched from meta, plus tenant_extensions rowsOne tenantYes — 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.

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 / SIGTERM

Nothing 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.

KeyTypeDefaultEffect
namestringPresent in every shipped profile as name: iam, but no reader. Documentary only.
hoststring"" (binds all interfaces)Listener bind address.
portstring or intrequiredListener port. Empty in both flag and YAML aborts boot with invalid/empty port provided.
datacenterstringdefaultDeployment tag: log field, service-discovery scoping, config-client datacenter, request context.
clusterstringdefaultSame, for the cluster axis.
sidstring<hostname>-<service>-<port>Unique instance id: log tag, service-discovery registration id, config instance leaf.
instancestringfalls back to sidThe config client’s instance leaf — the deepest config resolution level.
labelsmap[string]stringCohort labels passed to the config client for match/cohort resolution.
tagsmap[string]stringLegacy alias for labels. labels wins.
KeyTypeDefaultEffect
zerotrustbooltrueWhen true the listener resolves a TLS config at start and fails if cert material is missing. Also gates outbound mTLS.
svccertstring""Service certificate path. TLS is configured only when both svccert and svckey are non-empty.
svckeystring""Service private key path.
rootcacertstring""Root CA certificate path for the trust chain.
rootcakeystring""Root CA key path. No CLI flag.
insecureskipverifyboolfalseSkip peer certificate verification.
auto_renew_certificatesbooltrueChassis certificate auto-renewal.
jwt_public_keystring""File path loaded into the chassis TLS object as the JWT public key. Unrelated to the per-tenant signing keyring.
jwt_private_keystring""File path loaded as the chassis JWT private key.
KeyTypeDefaultEffect
jwks.issuers.<issuer>map[string]stringMaps 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.

KeyTypeDefaultEffect
log.levelstringerrorSets the service logger and the root logger level.
routinepool.sizeint100Global goroutine-pool size — the pool that runs the HTTP serve loop and scheduled work.
routinepool.queueint100Goroutine-pool queue depth.
routinepool.spawnint50Goroutine-pool spawn count.

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.

KeyTypeDefaultEffect
config.urlstring""Remote config server base URL. Requires config.apikey or svccert, otherwise boot aborts with error: either config.apikey or svccert should be present.
config.pathstring""Local folder of flat-path YAML. Ignored with a warning when config.url is set.
config.apikeystring""API key for the remote config server.
config.cachestring""Path for the warm-start config cache.
KeyTypeDefaultEffect
boot.retry_windowduration or int seconds90sHow 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.urlstring""Master switch for vault. Empty means no vault client is registered at all.
vault.apikeystring""API key for the vault client.
vault.filestring""Local vault file, used when vault.url is empty.
vault.<auth block>mapThe whole vault: map is handed to the vault client initializer, so token:, approle: {roleid, secretid} and kubernetes: sub-blocks are consumed there.
meta.urlstring""Meta URL for the service’s own boot-plane meta client.
meta.apikeystring""API key for the boot-plane meta client.
meta.dirstring""Local directory for the boot-plane meta client, used when meta.url is empty.
servicediscovery_urlstring""Service-registry URL. When set, either servicediscovery_apikey or svccert must be present or boot aborts.
servicediscovery_apikeystring""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.

Each accepts a bare integer of seconds or a Go duration string.

KeyTypeDefaultEffect
timeouts.readduration or int seconds15shttp.Server read timeout.
timeouts.writeduration or int seconds15shttp.Server write timeout.
timeouts.readheaderduration or int seconds5shttp.Server read-header timeout.

Per-request deadlines come from the chassis timeout middleware, not from a handler wrapper.

KeyTypeDefaultEffect
v1-onlyboolfalseDisables the entire route surface.
KeyWhere it appearsReality
enablecacheAll four server-shaped profilesNo reader anywhere. Inert.
telemetry.enabledThe full profileTelemetry 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.

FlagMaps toDefault
-f, --configthe bootstrap file path.iam.yaml
-p, --portport
-l, --loglevelnothing at serve time (see above)error
--sidsid
--datacenterdatacenter
--clustercluster
--config_urlconfig.url
--config_apikeyconfig.apikey
--servicediscovery_urlservicediscovery_url
--servicediscovery_apikeyservicediscovery_apikey
--svccertsvccert
--svckeysvckey
--rootcacertrootcacert
--zerotrustzerotrust
--jwt_public_keyjwt_public_key
--jwt_private_keyjwt_private_key

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.

KeyTypeDefaultEffect
bootstrap.tokenstring""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.urlstring""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.
reuseportbooltrueBind the listener with SO_REUSEPORT.
realtimedebugboolfalseWhen true, the tenant middleware honours per-request debug switches (_debug_log_ query parameter or X-Debug-Log header).
api.keystring""API key used when building a tenant’s remote meta client. Best-effort — the client falls back to vault-issued JWT auth when empty.

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.

KeyTypeEffect
namestringTenant display name on the node.
activeboolTenant active flag.
domainlist[string]Domains for domain-based tenant resolution upstream.
KeyTypeDefaultEffect
datastores.<name>.dbpoolstringNames which pool the datastore uses. The service’s default datastore is main; a request may select another with ?datastore=<name>.
dbpools.<name>.typestringpostgres (empty and postgresql both resolve to it)Backend dialect. Only postgres has a pool factory here; anything else fails the build.
dbpools.<name>.dbserverstringrequiredhost:port; a bare host defaults to :5432.
dbpools.<name>.databasestringrequiredDatabase name. The database must already exist — this service never creates databases.
dbpools.<name>.dbuserstringrequiredDatabase user.
dbpools.<name>.passwordstring""A vault secret name when a vault client is registered, otherwise a literal password.
dbpools.<name>.schemastringthe tenant slot nameThe physical PostgreSQL schema every statement, the migration ledger and the extensions are qualified with.
dbpools.<name>.maxconnectionsintbackend defaultPool max open connections.
dbpools.<name>.connect_timeoutduration or int secondsbackend defaultConnect timeout.
dbpools.<name>.connection_life_timedurationbackend defaultMax connection lifetime, e.g. 30m.
dbpools.<name>.isolationstring""dedicated or shared — declared isolation posture.
dbpools.<name>.tlsconfigstring""disable, require, verify-ca or verify-full.
dbpools.<name>.dsnstring""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: 5
KeyTypeDefaultEffect
jwt.signing.keystringabsentA 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.

The tenant node’s session: block is the highest-precedence behavior layer — it wins over the product and tenant-extension files.

KeyTypeDefaultEffect
session.presetstringstandardstandard, hipaa or fedramp. Selects model, crypto suite and timeout defaults. An unknown value falls back to standard.
session.modelstringpreset-derivedOverride: stateless or managed.
session.crypto_suitestringpreset-derivedOverride: v4 or v3.
session.max_concurrent_sessionsint0 (unlimited)Cap on concurrent sessions per principal. 1 is single-session. Enforced atomically under an advisory lock keyed on tenant and principal.
session.on_limitstringevict_oldestreject (the new login fails) or evict_oldest (the new login evicts the oldest session).
session.idle_timeout_sint seconds0 standard / 900 hipaa / 600 fedrampIdle timeout, enforced at exchange.
session.absolute_expiry_sint seconds0 standard / 43200 hipaa / 28800 fedrampAbsolute 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.

KeyTypeDefaultEffect
meta.urlstring""Meta base URL supplying this tenant’s product layer.
meta.localdirstring""Local directory mirroring meta’s path layout — the development path for the product layer.
meta.isexternalboolfalseMarks 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>/file

A product ships behavior for this service as six files under iam/ in its meta tree. Every one is optional.

FileDeclaresKeys
iam/realms.yamlAuthentication realms and feature gatesrealms.<name>.entity, .active, .default, .providers; realmconfig.enable.*
iam/providers.yamlAuth-method declarationsproviders[].name, .type, .template
iam/token.yamlAccess-token policytoken.expiry, token.claims.<name>, token.claims_from.<name>
iam/access.yamlNamed authorization predicatesauthorization.<rule>.language, .rule, .rules
iam/config.yamlFree-form service propertiesproperties.*
iam/session.yamlSession posturethe 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.

KeyTypeDefaultEffect
realms.<name>.entitystringusersThe schema entity holding this realm’s identities.
realms.<name>.activebooltrueInactive realms refuse login.
realms.<name>.defaultboolfalseMarks the realm used when a request names none.
realms.<name>.providerslist[string][password]Provider names this realm allows.
realmconfig.enable.agentsbooltruefalse removes the iamagents entity from the tenant’s composed schema entirely.
realmconfig.enable.botsbooltruefalse removes iambots.
realmconfig.enable.delegationsbooltruefalse removes iamdelegations.
providers[].namestringProvider name a realm can list.
providers[].typestringchallenge (password) or magic (magic link).
providers[].templatestringNotification template for a magic provider.
token.expiryduration string15m handler defaultAccess-token lifetime. A parse failure refuses the engine build.
token.claims.<name>anyStatic custom claims stamped into every access token.
token.claims_from.<name>stringMaps 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>.languagestringexprExpression language for the predicate.
authorization.<rule>.rulestring or boolThe predicate body.
authorization.<rule>.ruleslist[string]Multi-predicate form, evaluated as OR.
properties.*mapFree-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.

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 subsetting

Layers 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 resolution

A 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.

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:5030
RouteAuthSuccessFailure
GET /readynone200, body ready:true500, body ready:false
GET /healthnone200, JSON body500
{ "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.

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 scheduler
shutdown complete.

Five profiles are embedded in the binary.

Terminal window
iam.svc config list
iam.svc config generate bootstrap-dev > .iam.yaml
iam.svc config generate all -d /etc/iam
ProfileShape
bootstrap-devLocal single-file boot — embedded tenant hive, local PostgreSQL, zero-trust off.
bootstrap-managed-sessionThe dev profile with the hipaa preset on the tenant node.
bootstrap-config-serverTenants, datastores and pools come from the config server, not inline.
bootstrap-vaultVault-backed database credentials plus zero-trust mTLS; pool passwords are vault secret names.
bootstrap-fullConfig 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.