Configuration & Secrets
The five EDK containers share a single configuration model with three layers: the YAML file shipped in the image, the environment- and operator-supplied overlay, and the per-tenant configuration stored in Postgres. Secrets are kept out of all three through the EDK secret-reference interpolation, with the actual values resolved at runtime from the configured secret backend.
The Layered Configuration Model
Configuration in an EDK container is the result of a chain of PropertySource instances composed by the IDK config system and enriched by the EDK overlay:
- Shipped
application.yaml. Lives at/app/config/application.yamlinside the image. Carries safe defaults for local testing. - Mounted YAML overlays. A deployment mounts additional YAML files (or replaces the shipped one) at known paths under
/app/config/. Files are merged in lexical order. - Environment variables. Standard EDK env-var mapping translates
OAUTH2__SERVERS__ACME__ISSUER_URIinto theoauth2.servers.acme.issuer_uriproperty. - Cloud config providers. Optional Azure App Configuration or REST Config providers are wired through
lib-conf-azure-appconfigorlib-conf-rest-config. Both layer in as standard property sources. - Per-tenant Postgres config. The
TenantConfigPropertySourcereadstenant_config_propertyrows for the currently resolved tenant. This is the layer that tenant administrators write through/api/v1/tenants/{id}/config/....
Resolution within a request is App → Tenant → Principal (the three ConfigService scopes). A property that exists at both the app and tenant level resolves to the tenant value within a request bound to that tenant, and to the app value otherwise.
The Shipped application.yaml
The application.yaml shipped in each container is deliberately minimal and permissive, for local testing:
server:
rest:
port: 8080
auth:
enabled: false
anonymous:
allowed: true
The reason it ships permissive rather than locked-down is that the typical first interaction with the image is docker run for a quick smoke test, and forcing the operator to configure JWT issuance before they can even see the health endpoint is unhelpful. A production deployment is expected to override server.rest.auth to enabled: true and configure the JWT issuer the admin REST will validate bearer tokens against.
Layered onto this, a typical production overlay sets:
- The Postgres JDBC URL and credentials (referenced through secret interpolation).
- The KMS endpoint and the service-JWT key alias the container uses to authenticate to it.
- The tenant resolution settings: the platform base host, the trusted reverse-proxy hop count, the cache TTL.
- The container-specific routing: the AS issuer URL and JWKS for the issuer container, the trust source registry refresh schedule for the verifier container, the DID method allowlist for the DID container.
Environment Variables
Env-var mapping follows the EDK convention: double-underscore for property-path separators, single underscore inside a single path segment, uppercase. database.url becomes DATABASE__URL. oauth2.servers.acme.issuer_uri becomes OAUTH2__SERVERS__ACME__ISSUER_URI. Standard JVM ergonomics for container-friendly configuration.
Two env vars are read directly by the container startup code and never via the property resolver, because they need to be available before the DI graph is composed:
APP_PROFILE: the profile name passed into the Metro app graph factory. Used by config sources that key on profile.PORT: the HTTP listen port. Defaults to 8080 if not set.
Secret References
Secrets in YAML and env vars are stored as references, never as plaintext, through the EDK secret-interpolation syntax:
database:
url: jdbc:postgresql://postgres:5432/edk
username: edk_app
password: ${secret:vault:edk/postgres/app-password}
${secret:vault:...} is resolved at startup by the configured SecretProvider SPI implementation. The EDK ships three production-grade implementations:
- HashiCorp Vault. Configured against any KV v2 secret engine. Authentication via AppRole, Kubernetes auth, or token. Module:
lib-conf-vault. - AWS Secrets Manager. Configured against the regional Secrets Manager endpoint. Authentication via the AWS credential chain (IAM role for service accounts on EKS, environment, profile). Module:
lib-conf-aws-secrets. - Azure Key Vault. Configured against a vault URL. Authentication via the Azure credential chain (managed identity, service principal, environment). Module:
lib-conf-azure-keyvault.
When the deployment standardises on Kubernetes secrets, secrets are mounted as files and referenced through the ${file:...} interpolation rather than ${secret:...}. The result is the same: no plaintext secret in the YAML, no plaintext secret in the env var, no plaintext secret in the image.
A deployment may have multiple secret providers wired simultaneously, distinguished by the prefix after secret: (secret:vault:, secret:aws:, secret:azure:). Per-tenant secret backend selection is supported through the TenantConfigSecretClassifier.
Per-Tenant Configuration in Postgres
Per-tenant configuration lives in the shared tenant_config_property table. Each row is (tenant_id, key, value, secret_reference, updated_at). The runtime reads these rows through TenantConfigPropertySource when a tenant is in scope on the call.
Tenant administrators write to this table indirectly, through the typed admin REST per configuration domain. The EDK does not expose a raw JSONB config endpoint; every config domain has a typed REST surface (the AS instance admin, the issuer config admin, the verifier config admin, the DID method admin, the trust source admin, the federation provider admin, the integration registry, the webhook admin). Behind those surfaces, the values land in tenant_config_property as typed properties.
Secret values written through the admin REST never land in tenant_config_property as plaintext. The TenantConfigSecretClassifier recognises secret-bearing properties and persists only a secret reference; the actual value lands in the configured secret backend under a key path that includes the tenant id.
Cross-replica invalidation when a tenant administrator updates configuration goes through the shared event subsystem: the admin command emits an application.tenant.config-updated event, a Postgres LISTEN/NOTIFY bridge fans out, and each replica's TenantConfigPropertySource cache invalidates for the affected tenant. A TTL fallback covers missed notifications.
Tenant Resolution Settings
The tenant resolution stack reads its settings from the top of the property resolver chain (the app-scope, not per-tenant, because tenant resolution runs before any tenant is in scope). The relevant properties:
tenant.resolution.platform_base_host: the host suffix that subdomain resolution treats as the platform base. Subdomains of this resolve to tenant slugs.tenant.resolution.trusted_proxy_hop_count: how manyX-Forwarded-Hosthops the resolver trusts. Important behind reverse proxies and CDNs.tenant.resolution.cache_ttl_seconds: fallback TTL on the in-memory tenant routing cache. Cross-replica invalidation handles the typical case; the TTL covers missed notifications.tenant.resolution.well_known_path_modes: which.well-knownURL forms each protocol supports (spec form only, spec + legacy, or custom). The defaults match the discovery URL forms described in the topology page.
The TenantResolutionSettingsBinder reads these properties and feeds them into the Ktor TenantResolutionPlugin at server startup.
Database Routing
The shared Postgres is sufficient for most deployments. When per-tenant database isolation is required, the lib-data-store-db-routing-config and lib-data-store-db-routing-pooling modules read tenant-to-database routing from configuration:
database:
routing:
default:
url: jdbc:postgresql://postgres-shared:5432/edk
username: edk_app
password: ${secret:vault:edk/postgres/shared/password}
tenants:
acme:
url: jdbc:postgresql://postgres-acme:5432/edk
username: edk_app_acme
password: ${secret:vault:edk/postgres/acme/password}
A request bound to the acme tenant routes its repository calls to the postgres-acme target. Requests bound to any tenant without an explicit entry go to default. Connection pooling is per-target via HikariCP.
The routing table is itself a tenant-config-source contribution, so adding a new tenant routing entry through the admin REST takes effect on the next resolver cache miss and on the cross-replica invalidation, without restarting the container.
Service-to-Service Auth Configuration
Each data-plane container is also a client of the KMS, the DID resolver, and potentially the AS or other data planes. Outbound service calls carry either a service JWT or mTLS, configured under peer.auth:
peer:
auth:
mode: service_jwt # or 'mtls'
service_jwt:
issuer: https://issuer.example.com
key_alias: enterprise-issuer__peer-signing
ttl_seconds: 60
The key_alias references a KMS key the container holds the signing right for. The receiving container validates the JWT against the KMS-published JWKS. When the deployment runs on a cluster mesh, switching mode to mtls defers the auth to the mesh's mTLS and the JWT becomes optional.
Configuration Hot-Reload
The boundary for hot-reload is the property source. Property sources that support change notifications (TenantConfigPropertySource, the Azure App Configuration provider, the REST config provider) propagate changes into the running container without a restart. The shipped application.yaml, mounted YAML files, and environment variables are read at startup only.
Tenant administrators changing per-tenant config through the admin REST is the most common hot-reload path. App-level config changes typically require a rolling restart of the affected container.