Configuration & Secrets
The EDK enterprise containers share a configuration model with mounted YAML, environment variables, platform configuration, and per-tenant configuration stored in the tenant workload database. Secrets are kept out of those layers through 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. - Platform config service. Runtime services resolve platform-owned configuration through the platform service. In the enterprise deployment this peer call is internal and uses the platform route, normally over gRPC to
enterprise-platform:9090. - Per-tenant workload-database config. The
TenantConfigPropertySourcereadstenant_config_propertyrows from the schema for the currently resolved tenant in the tenant workload database. This is the layer that tenant administrators write through platform administration and configuration APIs.
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
It ships permissive rather than locked-down so a first docker run reaches the health endpoint without configuring JWT issuance up front. A production deployment overrides server.rest.auth to enabled: true and configures the JWT issuer the admin REST validates bearer tokens against.
Layered onto this, a typical production overlay sets:
- The PostgreSQL JDBC URLs and credentials for the platform database and the tenant workload database (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.
The Helm chart sets the standard image and platform values under global and injects per-service environment overrides through services.<role>.env:
global:
imageRegistry: nexus.sphereon.com/edk-docker
imageTag: "<enterprise-version>"
imagePullPolicy: IfNotPresent
imagePullSecrets: []
platformBaseDomain: example.com
services:
issuer:
env:
- name: APP_PROFILE
value: production
Use the exact global.imageTag supplied by Sphereon for the delivered enterprise release. The tag is the product version for that delivery; the source revision is reported separately through image metadata and /version. Use global.imagePullSecrets for authenticated Nexus pulls. Do not put Docker credentials in services.<role>.env.
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 the Workload Database
Per-tenant configuration lives in tenant_config_property inside the resolved tenant's schema in the tenant workload database. 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 on the tenant workload database 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 enterprise deployment has two database roles. The platform service owns the platform database. Tenant-KMS, DID, tenant-AS, issuer, and verifier use the tenant workload database, with schema-per-tenant isolation:
database:
platform:
url: jdbc:postgresql://platform-postgres:5432/edk_platform
username: edk_platform
password: ${secret:vault:edk/postgres/platform/password}
tenant:
url: jdbc:postgresql://tenant-postgres:5432/edk_tenant
username: edk_tenant
password: ${secret:vault:edk/postgres/tenant/password}
isolation: schema
schemaPattern: tenant_{id}
At tenant registration time, the platform provisions the workload schema for the tenant. A request bound to the acme tenant routes repository calls to the tenant workload database with the search path set to that tenant's schema. Replicas of a runtime service share the same workload database and route by the resolved tenant.
When a tenant needs stronger isolation than schema-per-tenant, the lib-data-store-db-routing-config and lib-data-store-db-routing-pooling modules can route that tenant to a dedicated database or PostgreSQL instance. Connection pooling is per-target via HikariCP, and routing changes take effect on the next resolver cache miss and cross-replica invalidation without restarting the container.
Service-to-Service Auth Configuration
Each runtime container is also a client of the KMS, the DID resolver, and potentially the AS or other runtime services. 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.
Peer Transport Configuration
Peer transport is separate from peer authentication. The enterprise images include generated gRPC client stubs and routed remote command dependencies. Platform and tenant-KMS run the inbound gRPC receivers; the other runtime services use outbound routes to them:
grpc:
enabled: true
port: 9090
authMode: service-jwt
Keep gRPC east-west only. Runtime services route platform configuration commands to the platform service and KMS commands to tenant-KMS. The public gateway routes only HTTPS REST/protocol traffic.
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.