Skip to main content
Version: v0.25.0 (Latest)

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 ManagerAzure Key VaultHashiCorp Vault
Provider IDawsazurevault
Required configNone (SDK default chain)secrets.azure.vault-urlsecrets.vault.address
Auth methodsDefault chain, Access Key, ProfileDefault Credential, Client Secret, Managed Identity, CLIToken, AppRole, Kubernetes
Structured secretsJSON key extractionSingle value onlyJSON key extraction
Default cache TTL5 minutes5 minutes5 minutes
Enterprise featuresRegional 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

PropertyDefaultDescription
secrets.aws.regionSDK defaultAWS region for Secrets Manager requests
secrets.aws.endpoint-Endpoint override (for LocalStack or VPC endpoints)
secrets.aws.auth-methodDEFAULTAuthentication 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-ttl300Cache TTL in seconds

Authentication Methods

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

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

PropertyDefaultDescription
secrets.azure.vault-url-Key Vault URL (required)
secrets.azure.auth-methodDEFAULT_CREDENTIALAuthentication 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-ttl300Cache TTL in seconds

Authentication Methods

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/

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}
note

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

PropertyDefaultDescription
secrets.vault.address-Vault server URL (required)
secrets.vault.auth-methodTOKENAuthentication 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.mountapproleAppRole auth mount path
secrets.vault.kubernetes.role-Kubernetes auth role (required for KUBERNETES)
secrets.vault.kubernetes.jwt-path/var/run/secrets/kubernetes.io/serviceaccount/tokenPath to the Kubernetes service account JWT
secrets.vault.kubernetes.mountkubernetesKubernetes auth mount path
secrets.vault.mountsecretKV v2 secrets engine mount path
secrets.vault.namespace-Vault Enterprise namespace
secrets.vault.cache-ttl300Cache TTL in seconds

Authentication Methods

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}

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:

ProviderHealth check mechanism
AWSAttempts a describe call against Secrets Manager
AzureLists secret properties in the configured vault
VaultCalls 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