Secret Management
Secrets, database passwords, API keys, signing keys, client credentials, must never appear as plaintext in configuration files, environment variables, or version control. The EDK's secret management system solves this by storing secrets in dedicated vaults and referencing them via interpolation syntax in your configuration.
Instead of db.password=hunter2, you write db.password=${secret:vault:myapp/database:password}. At runtime, when the configuration system reads this value, the secret provider resolves the reference by calling the vault, caching the result, and returning the actual secret. The plaintext secret never leaves memory, it's not in config files, not in logs, not in cloud provider storage.
The EDK provides production-grade providers for AWS Secrets Manager, Azure Key Vault, and HashiCorp Vault. Each provider auto-registers, supports multi-tenant secret routing (different tenants can have different vault configurations), and caches resolved secrets to avoid excessive vault API calls.
Quick Comparison
| AWS Secrets Manager | Azure Key Vault | HashiCorp Vault | |
|---|---|---|---|
| Provider ID | aws | azure | vault |
| Required config | None (SDK default chain) | secrets.azure.vault-url | secrets.vault.address |
| Auth methods | Default chain, Access Key, Profile | Default Credential, Client Secret, Managed Identity, CLI | Token, AppRole, Kubernetes |
| Structured secrets | JSON key extraction | Single value only | JSON key extraction |
| Default cache TTL | 5 minutes | 5 minutes | 5 minutes |
| Enterprise features | Regional endpoints | - | Namespace isolation |
AWS Secrets Manager
AWS Secrets Manager is Amazon's managed secret storage service. It handles encryption, rotation, and access control for secrets. The EDK provider uses the AWS SDK for Kotlin (coroutine-native), which means secret resolution is non-blocking and works efficiently in high-throughput applications.
The AWS provider is the easiest to set up because it requires no mandatory configuration when running in an environment with an ambient credential chain, EC2 instance roles, ECS task credentials, Lambda execution roles, and IAM Roles for Service Accounts (IRSA) in EKS all work out of the box. Just add the module to your classpath and reference secrets.
For multi-tenant deployments, the provider supports per-tenant AWS account routing. Different tenants can have their secrets in different AWS accounts or regions. The provider maintains a concurrent pool of SecretsManagerClient instances, one per distinct configuration, so tenant isolation doesn't add configuration overhead.
Configuration Properties
| Property | Default | Description |
|---|---|---|
secrets.aws.region | SDK default | AWS region for Secrets Manager requests |
secrets.aws.endpoint | - | Endpoint override (for LocalStack or VPC endpoints) |
secrets.aws.auth-method | DEFAULT | Authentication method: DEFAULT, ACCESS_KEY, or PROFILE |
secrets.aws.access-key-id | - | AWS access key ID (required for ACCESS_KEY) |
secrets.aws.secret-access-key | - | AWS secret access key (required for ACCESS_KEY) |
secrets.aws.session-token | - | Session token for temporary credentials |
secrets.aws.profile-name | - | Named profile from ~/.aws/credentials (required for PROFILE) |
secrets.aws.cache-ttl | 300 | Cache TTL in seconds |
Authentication Methods
- Default Chain
- Access Key
- Named Profile
Uses the standard AWS SDK credential chain. No additional configuration needed, the SDK checks environment variables, ~/.aws/credentials, IAM instance roles, ECS task credentials, and web identity tokens in order.
secrets.aws.auth-method=DEFAULT
secrets.aws.region=eu-west-1
Authenticates with static credentials. Suitable for development or environments where IAM roles are not available.
secrets.aws.auth-method=ACCESS_KEY
secrets.aws.region=eu-west-1
secrets.aws.access-key-id=${secret:env:AWS_ACCESS_KEY_ID}
secrets.aws.secret-access-key=${secret:env:AWS_SECRET_ACCESS_KEY}
# Optional: for temporary credentials from STS
secrets.aws.session-token=${secret:env:AWS_SESSION_TOKEN}
Uses a named profile from the local AWS credentials file.
secrets.aws.auth-method=PROFILE
secrets.aws.profile-name=production
Referencing Secrets
AWS Secrets Manager stores secrets as either plaintext strings or JSON objects. The provider supports both:
# Plaintext secret — returns the raw value
api.key=${secret:aws:prod/api-key}
# JSON secret — extract a specific field
db.password=${secret:aws:prod/database:password}
db.username=${secret:aws:prod/database:username}
When no key is specified and the stored value is JSON, the full JSON string is returned.
Local Development with LocalStack
Override the endpoint to point at a local LocalStack instance:
secrets.aws.endpoint=http://localhost:4566
secrets.aws.region=us-east-1
Azure Key Vault
Azure Key Vault is Microsoft's cloud secret management service, commonly used alongside Azure App Configuration. While App Configuration holds non-sensitive settings (feature flags, endpoints, timeouts), Key Vault holds the secrets those settings reference (database passwords, API keys, certificates).
The EDK provider uses Azure's async SDK with a coroutine bridge for non-blocking resolution. Like the AWS provider, it supports per-tenant vault routing, each tenant can have its own Key Vault instance, and the provider maintains a client pool per vault URL.
The provider requires a secrets.azure.vault-url to be configured. Without it, the auto-registration check returns false and the provider stays disabled.
config.providers.azure-keyvault-secrets.enabled=true
secrets.azure.vault-url=https://my-vault.vault.azure.net/
Configuration Properties
| Property | Default | Description |
|---|---|---|
secrets.azure.vault-url | - | Key Vault URL (required) |
secrets.azure.auth-method | DEFAULT_CREDENTIAL | Authentication method: DEFAULT_CREDENTIAL, CLIENT_SECRET, MANAGED_IDENTITY, or CLI |
secrets.azure.tenant-id | - | Azure AD tenant ID (required for CLIENT_SECRET) |
secrets.azure.client-id | - | Service principal client ID (required for CLIENT_SECRET) |
secrets.azure.client-secret | - | Service principal secret (required for CLIENT_SECRET) |
secrets.azure.cache-ttl | 300 | Cache TTL in seconds |
Authentication Methods
- Default Credential
- Client Secret
- Managed Identity
- Azure CLI
Uses DefaultAzureCredential, which tries environment variables, managed identity, Visual Studio auth, Azure CLI, and shared token cache in sequence.
secrets.azure.auth-method=DEFAULT_CREDENTIAL
secrets.azure.vault-url=https://my-vault.vault.azure.net/
Authenticates as a service principal with a client secret. Typical for CI/CD and non-Azure hosted workloads.
secrets.azure.auth-method=CLIENT_SECRET
secrets.azure.vault-url=https://my-vault.vault.azure.net/
secrets.azure.tenant-id=12345678-1234-1234-1234-123456789012
secrets.azure.client-id=abcdefgh-abcd-abcd-abcd-abcdefghijkl
secrets.azure.client-secret=${secret:env:AZURE_CLIENT_SECRET}
Uses the system-assigned managed identity of the Azure resource (VMs, App Service, Container Apps).
secrets.azure.auth-method=MANAGED_IDENTITY
secrets.azure.vault-url=https://my-vault.vault.azure.net/
Uses Azure CLI credentials. Intended for local development only.
secrets.azure.auth-method=CLI
secrets.azure.vault-url=https://my-vault.vault.azure.net/
Referencing Secrets
Azure Key Vault stores each secret as a single string value. The key segment in the reference syntax is not used, the path maps directly to the Key Vault secret name.
# Retrieve a secret named "database-password"
db.password=${secret:azure:database-password}
# Retrieve a secret named "api-key"
api.key=${secret:azure:api-key}
Azure Key Vault secret names may only contain alphanumeric characters and dashes. If your secret name does not match this constraint, rename it in Key Vault.
HashiCorp Vault
HashiCorp Vault is the most flexible option, it's cloud-agnostic, self-hosted, and supports advanced features like dynamic secrets, secret rotation, and audit logging at the vault level. It's the natural choice for on-premises deployments, multi-cloud environments, and organizations that need full control over their secret infrastructure.
The EDK provider communicates directly with Vault's KV v2 HTTP API using Ktor, no external Vault SDK or agent is required. It supports three authentication methods: direct token (simplest), AppRole (for automated systems), and Kubernetes service account JWT (for pods in Kubernetes clusters).
For Vault Enterprise, the provider supports namespace isolation, allowing different tenants to access different Vault namespaces. Like the other providers, it maintains a token cache per configuration and supports per-tenant vault routing.
The Vault provider requires secrets.vault.address to be configured.
config.providers.vault-secrets.enabled=true
secrets.vault.address=https://vault.example.com:8200
Configuration Properties
| Property | Default | Description |
|---|---|---|
secrets.vault.address | - | Vault server URL (required) |
secrets.vault.auth-method | TOKEN | Authentication method: TOKEN, APPROLE, or KUBERNETES |
secrets.vault.token | - | Vault token (required for TOKEN) |
secrets.vault.approle.role-id | - | AppRole role ID (required for APPROLE) |
secrets.vault.approle.secret-id | - | AppRole secret ID (required for APPROLE) |
secrets.vault.approle.mount | approle | AppRole auth mount path |
secrets.vault.kubernetes.role | - | Kubernetes auth role (required for KUBERNETES) |
secrets.vault.kubernetes.jwt-path | /var/run/secrets/kubernetes.io/serviceaccount/token | Path to the Kubernetes service account JWT |
secrets.vault.kubernetes.mount | kubernetes | Kubernetes auth mount path |
secrets.vault.mount | secret | KV v2 secrets engine mount path |
secrets.vault.namespace | - | Vault Enterprise namespace |
secrets.vault.cache-ttl | 300 | Cache TTL in seconds |
Authentication Methods
- Token
- AppRole
- Kubernetes
Direct token authentication. The simplest method, suitable for development or when tokens are injected by an external process.
secrets.vault.auth-method=TOKEN
secrets.vault.token=${secret:env:VAULT_TOKEN}
Machine-oriented authentication using a role ID and secret ID. The provider authenticates and caches the resulting token automatically.
secrets.vault.auth-method=APPROLE
secrets.vault.approle.role-id=db27de19-1af8-a67c-7c1a-123456789012
secrets.vault.approle.secret-id=${secret:env:VAULT_SECRET_ID}
# Optional: custom mount path
secrets.vault.approle.mount=approle
Authenticates using the Kubernetes service account token mounted into the pod. The provider reads the JWT from the filesystem and exchanges it for a Vault token.
secrets.vault.auth-method=KUBERNETES
secrets.vault.kubernetes.role=my-app
# Defaults to the standard K8s mount path
secrets.vault.kubernetes.jwt-path=/var/run/secrets/kubernetes.io/serviceaccount/token
secrets.vault.kubernetes.mount=kubernetes
Referencing Secrets
Vault's KV v2 engine stores secrets as JSON objects. You can retrieve the entire object or extract a specific key:
# Extract a single key from the secret at path "app/database"
db.password=${secret:vault:app/database:password}
db.username=${secret:vault:app/database:username}
# Retrieve the full JSON object as a string
db.config=${secret:vault:app/database}
When no key is specified, the provider looks for a value field in the secret data. If that field does not exist, the full data object is returned as a JSON string.
Vault Enterprise Namespaces
Set the namespace property to isolate secrets per Vault Enterprise namespace. The provider sends the X-Vault-Namespace header with every request.
secrets.vault.namespace=team-payments
Custom Secrets Engine Mount
If your KV v2 engine is mounted at a non-default path, specify it with the mount property:
secrets.vault.mount=kv
# Secrets are now read from /v1/kv/data/<path>
Multi-Tenant Configuration
All three providers support multi-tenant deployments. When a secret is resolved, the provider reads its configuration through the PropertyResolver passed at resolution time. This means each tenant can override provider settings, for example, pointing at a different Vault address or AWS region.
A typical setup stores tenant-specific overrides in the settings database:
# App-level defaults
secrets.vault.address=https://vault.example.com:8200
secrets.vault.auth-method=APPROLE
# Tenant "acme" overrides (stored at TENANT scope)
secrets.vault.address=https://vault-eu.example.com:8200
secrets.vault.namespace=acme
Each unique combination of provider configuration gets its own cached client connection, so tenants sharing the same backend reuse a single connection.
Caching
All three providers cache resolved secrets in memory with a configurable TTL (default: 5 minutes). The cache is thread-safe and per-provider.
To force a fresh fetch for a specific secret, use the bypassCache option programmatically:
provider.getSecret(
path = "app/database",
key = "password",
options = SecretOptions(bypassCache = true)
)
To invalidate the entire cache (e.g. after a secret rotation event):
provider.invalidateCache() // clear all
provider.invalidateCache("app/database") // clear a specific path
Health Checks
Each provider exposes a healthCheck() method that verifies connectivity to the backend:
| Provider | Health check mechanism |
|---|---|
| AWS | Attempts a describe call against Secrets Manager |
| Azure | Lists secret properties in the configured vault |
| Vault | Calls GET /v1/sys/health |
Use these in your application's readiness probe to detect secret backend outages early.
Disabling a Provider
To prevent a provider from being registered at startup, set its contribution flag to false:
config.providers.aws-secrets.enabled=false
config.providers.azure-keyvault-secrets.enabled=false
config.providers.vault-secrets.enabled=false