Last updated: July 2026
Keycloak should not have secrets like the database password or SMTP credentials sitting in plaintext configuration. Keycloak’s vault SPI lets you reference secrets with the ${vault.key} syntax in supported fields — such as realm SMTP settings and identity-provider credentials — resolving them at runtime from a file-based vault or an external KeyStore vault. This keeps sensitive values out of keycloak.conf, environment variable dumps, and realm exports, which is exactly where you do not want them.
Scope note: This post is about managing the secrets that Keycloak itself uses (SMTP passwords, LDAP bind passwords, IdP client secrets). It is not about using Keycloak as an SSO provider for HashiCorp Vault. If you need to add OIDC login to Vault, see our separate guide: Keycloak + HashiCorp Vault OIDC SSO.
Why plaintext secrets in Keycloak config are a real risk
When you configure Keycloak for production, several credentials end up referenced somewhere in your setup:
- The database password (
KC_DB_PASSWORD) - The SMTP account password for outbound email
- LDAP or Active Directory bind passwords
- Client secrets for upstream identity providers (Google, Entra ID, Okta)
The temptation is to drop these into keycloak.conf, .env files, or directly into the Admin Console form fields. Each of those paths creates a class of exposure that is easy to overlook until it causes a real problem.
Realm exports are the most common leak vector. When you export a realm from the Admin Console or via the CLI — something operators do regularly for backup, migration, or IaC — Keycloak includes most field values in the exported JSON. An SMTP password typed directly into a realm setting ends up verbatim in that export. Exports get committed to Git, attached to tickets, and emailed between teams.
Environment variable dumps are another risk. Container orchestrators, APM tools, and crash reporters routinely collect environment variables as part of diagnostics. A KC_DB_PASSWORD=supersecret in the process environment is one misconfigured log shipper away from appearing in an observability dashboard. The keycloak.conf file itself often ends up in version control, a persistent risk even with .gitignore rules in place.
The vault SPI addresses the subset of credentials that flow through Keycloak’s configuration system. Understanding which secrets it covers — and which it does not — is the first step to a clean secrets posture.
For a broader hardening review covering network exposure, TLS, realm settings, and brute-force protection, see the Keycloak security audit and hardening checklist.
What the Keycloak vault SPI actually does
Keycloak’s Vault SPI is an extension point that lets Keycloak resolve credential values at runtime rather than storing them statically in the database or configuration files. Instead of typing "mySmtpPassword" into a form field, you write ${vault.smtp_password}. Keycloak calls the configured vault provider to look up smtp_password and uses the resolved value for that operation.
The vault reference syntax is ${vault.<key>}, where <key> is the name of the secret. Keys are realm-scoped — the same expression in two different realms resolves to different backing values. Two built-in providers ship with Keycloak 26.x, both configured through keycloak.conf or equivalent environment variables:
files-plaintext— reads secrets from individual files in a directory on the filesystemkeystore— reads secrets from a Java KeyStore file
The files-plaintext vault provider
The files-plaintext provider is the most practical option for Kubernetes deployments because it maps directly to how Kubernetes secrets work when mounted as files.
How it works
You designate a directory as the vault root. Each secret lives in its own file, and the filename determines the key vault expressions resolve to, following this naming convention:
<realm-name>_<key>
For example, if your realm is named acme and you want to use ${vault.smtp_password} in that realm’s SMTP configuration, the file must be named:
acme_smtp_password
The file contents are the raw secret value. No JSON, no YAML, no quoting — just the secret string. Keycloak reads the file content, trims trailing whitespace and newlines, and uses it as the resolved value.
Configuration
Enable the provider in keycloak.conf:
# keycloak.conf
vault=files-plaintext
vault-dir=/opt/keycloak/vault
Or as environment variables (useful in Docker and Kubernetes):
KC_VAULT=files-plaintext
KC_VAULT_DIR=/opt/keycloak/vault
Restart Keycloak after changing vault configuration. The directory is read on each secret resolution, so rotating file contents does not require a restart.
Directory layout example
For a realm named acme with SMTP and an LDAP bind password:
/opt/keycloak/vault/
acme_smtp_password
acme_ldap_bind_password
acme_google_idp_client_secret
If you have multiple realms, each realm’s secrets are namespaced by realm name in the filename:
/opt/keycloak/vault/
acme_smtp_password
internal_smtp_password
internal_ldap_bind_password
Keycloak will only resolve ${vault.smtp_password} within the acme realm using the acme_smtp_password file. The same expression in the internal realm resolves from internal_smtp_password. This prevents cross-realm secret leakage.
The KeyStore vault provider
The keystore provider reads secrets from a Java KeyStore (JKS or PKCS12), suitable for environments where a PKI or certificate management system already produces KeyStore files.
vault=keystore
vault-file=/opt/keycloak/vault/secrets.jks
vault-pass=changeit
vault-type=JKS
Alias naming follows the same <realm>_<key> convention. Each secret is stored as a SecretKeyEntry with an alias matching that pattern. In practice, the files-plaintext provider is more common for cloud-native deployments because maintaining a JKS file introduces its own rotation and access-control overhead.
Kubernetes: mounting secrets as vault files
The files-plaintext provider maps directly to Kubernetes’ native secret mounting mechanism, making it the natural choice for Kubernetes deployments.
The pattern: create a Kubernetes Secret with one key per Keycloak secret, mount it as a volume at the vault directory path, and set KC_VAULT_DIR to that path.
Step 1: Create the Kubernetes Secret
apiVersion: v1
kind: Secret
metadata:
name: keycloak-vault-secrets
namespace: keycloak
type: Opaque
stringData:
acme_smtp_password: "s3cure-smtp-pw"
acme_ldap_bind_password: "ldap-bind-secret"
acme_google_idp_client_secret: "google-client-secret-value"
Use an operator like External Secrets Operator to sync values from AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault into this in-cluster Secret. Do not commit the manifest with real values to Git.
Step 2: Mount the Secret as files
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: keycloak
spec:
template:
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.x
env:
- name: KC_VAULT
value: "files-plaintext"
- name: KC_VAULT_DIR
value: "/opt/keycloak/vault"
volumeMounts:
- name: vault-secrets
mountPath: /opt/keycloak/vault
readOnly: true
volumes:
- name: vault-secrets
secret:
secretName: keycloak-vault-secrets
When Kubernetes mounts a Secret as a volume, each key becomes a separate file whose filename is the key name — exactly the format the files-plaintext provider expects, with no extra tooling.
For a full production Kubernetes setup including resource limits, probes, and ingress, see the Keycloak Kubernetes production deployment guide.
Where ${vault.key} expressions are supported
This is a critical accuracy point: vault expressions are not supported everywhere in Keycloak. They work in specific fields that the Keycloak team has explicitly wired to the vault resolver.
Supported fields (as of Keycloak 26.x):
| Configuration area | Field | Vault expression works |
|---|---|---|
| Realm SMTP settings | Password | Yes |
| LDAP user federation | Bind Credential | Yes |
| SAML/OIDC Identity Provider | Client Secret | Yes |
| LDAP user federation | Connection URL (not the password field) | No |
| Realm display name | N/A | No |
| Client credentials | Client secret (for clients in the realm) | No — stored in DB |
The Admin Console does not indicate which fields support vault expressions — you need to test by entering ${vault.key} and confirming the integration works (for example, that a test email sends successfully). If the field is unsupported, Keycloak stores the literal string in the database and the integration will fail. Check logs at DEBUG level for vault resolution errors.
What the vault SPI does NOT cover: database and core secrets
The database password is the most sensitive secret in a Keycloak deployment, and it is the one the vault SPI does not handle. Keycloak reads KC_DB_PASSWORD before the vault subsystem initializes, so vault expressions cannot be used there.
The correct way to supply the database password securely is through your runtime environment:
# Kubernetes: inject from a Secret
env:
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-db-credentials
key: password
The admin bootstrap password (KEYCLOAK_ADMIN_PASSWORD) must also be supplied via your container runtime’s secret injection mechanism, not the vault SPI.
For Docker Compose setups, use an .env file that is excluded from version control, and consider using Docker secrets for production-grade Compose deployments. The Keycloak Docker Compose production setup guide covers this in detail.
Using vault references in SMTP configuration
Once the vault provider is configured and the secret file is in place, updating the SMTP configuration to use a vault reference takes one step in the Admin Console.
Navigate to Realm settings > Email > SMTP Password, replace the current value with ${vault.smtp_password}, and save. Keycloak resolves the value at connection time — a successful test email confirms the vault reference is working.
For a complete guide to SMTP configuration including TLS settings and debugging delivery failures, see the Keycloak email configuration guide.
Rotating secrets without a Keycloak restart
A key operational advantage of the files-plaintext provider is that rotation does not require a restart. When you update the Kubernetes Secret and the kubelet syncs the new file contents, Keycloak reads the new value on the next vault resolution — at the next SMTP connection or LDAP sync. No pod restart needed.
Rotation for KC_DB_PASSWORD does require a restart because it is read at startup. Plan database password rotation during a maintenance window.
Secrets checklist: what to get out of plaintext
Use this table as an audit checklist for your Keycloak deployment:
| Secret | Recommended supply method | Vault SPI supported |
|---|---|---|
| Database password | KC_DB_PASSWORD env var from k8s Secret |
No |
| Admin bootstrap password | KEYCLOAK_ADMIN_PASSWORD env var |
No |
| SMTP password | ${vault.smtp_password} via files-plaintext |
Yes |
| LDAP/AD bind password | ${vault.ldap_bind_password} via files-plaintext |
Yes |
| IdP client secret (Google, Entra, Okta) | ${vault.idp_client_secret} via files-plaintext |
Yes |
| TLS/SSL private key | Managed by cert-manager or ingress controller | N/A |
| Keycloak realm export files | Encrypted at rest, access-controlled storage | N/A |
For each secret in the “Yes” column, the migration path is:
- Create the vault file with the current secret value
- Update the Keycloak field to use
${vault.<key>}syntax - Test the integration (send a test email, trigger an LDAP sync, log in via the IdP)
- Remove the plaintext value from any other location (env file, Git history, previous exports)
For a broader production readiness review covering TLS, session handling, brute-force protection, and clustering, see the Keycloak production-ready checklist.
Frequently asked questions
How does Keycloak’s vault SPI work?
Keycloak’s vault SPI is a pluggable abstraction for runtime secret resolution. When Keycloak encounters a value in the format ${vault.key}, it delegates resolution to the configured provider — either files-plaintext (individual files in a directory) or keystore (a Java KeyStore). The resolved value is used in-memory for that operation and is never written back to the database. Configure the provider in keycloak.conf via the vault and vault-dir properties.
Can I store the Keycloak database password in a vault?
No. The database password is read before the vault subsystem initializes, so vault expressions like ${vault.db_password} in keycloak.conf are not supported. The correct approach is to supply KC_DB_PASSWORD as an environment variable injected from a Kubernetes Secret or equivalent runtime secret mechanism. This keeps the value out of your configuration files and version control without relying on the vault SPI.
How do I reference a secret in Keycloak config?
Use the syntax ${vault.<key>} in any Admin Console field that supports vault expressions — primarily SMTP password, LDAP bind credential, and identity provider client secret. The key name maps to a file (for the files-plaintext provider) named <realm>_<key> in the vault directory. For example, ${vault.smtp_password} in a realm named production resolves from a file named production_smtp_password in the vault directory.
Does the files-plaintext vault provider encrypt the secret files?
No — the provider reads plain text files without any encryption layer. Security relies on filesystem permissions and Kubernetes RBAC, pod security contexts, and node isolation. If you need encryption at rest, the keystore provider uses an encrypted KeyStore, or use a secrets manager (AWS Secrets Manager, Azure Key Vault) with the External Secrets Operator to sync encrypted values to in-cluster Kubernetes Secrets mounted as files.
Will a realm export include my vault-referenced secrets?
No — this is the core security benefit. When a field contains ${vault.smtp_password}, Keycloak exports the literal expression string rather than the resolved value. The exported JSON will contain "password": "${vault.smtp_password}" — safe to commit to version control or share for troubleshooting, as long as the vault directory itself is access-controlled.
Conclusion
Keycloak’s vault SPI eliminates the most common class of secrets leakage: credentials that end up in realm exports, environment dumps, and configuration files. The files-plaintext provider is easy to adopt, pairs naturally with Kubernetes secrets-as-files mounting, and supports zero-restart rotation for SMTP and LDAP credentials.
Key points:
- Use
${vault.key}for SMTP passwords, LDAP bind passwords, and IdP client secrets - The database password and admin bootstrap password must use environment variables — the vault SPI does not cover them
- File naming convention:
<realm>_<key>in the vault directory - Realm exports contain the vault expression, not the resolved value — safe to commit
If managing vault configuration, Kubernetes deployments, and secret rotation on top of your core application work is more overhead than you want, Skycloak handles it for you. Our managed Keycloak platform runs on hardened infrastructure with secrets managed out of the box. See Skycloak plans and pricing.