README
¶
Vault Plugin: Keycloak Secrets Engine
A HashiCorp Vault secrets engine plugin for Keycloak. Performs on-demand, audit-logged user password rotation via the Keycloak Admin REST API. Each rotation generates a cryptographically random password, sets it on the Keycloak account, and returns the new value — with no credential stored inside Vault.
Contents
- Vault Plugin: Keycloak Secrets Engine
What this plugin does
This plugin mounts as a Vault secrets engine and provides endpoints to:
- List and read users in the target Keycloak realm.
- Rotate a user password on demand via
users/<username>/rotate(fire-and-forget — no lease, no expiration). The new password is returned to the caller and remains valid in Keycloak until the next explicit rotation. - Sync rotated passwords to a KV v2 secret (v0.2.0+) — optionally PATCH the new password into another Vault KV v2 path after rotation (useful for Kubernetes secret operators).
- Issue ephemeral, lease-bound credentials via
creds/<role>(alpha). The password is returned with a Vault lease; on lease expiry or explicit revocation, the plugin resets the Keycloak password to a random discarded value, invalidating both sides.
What this plugin does not do
- Create or delete Keycloak users. The plugin only manages passwords for existing users.
- Auto-rotate passwords on a schedule. There is no background task or periodic rotation. All rotations are triggered by an explicit API call.
- Store passwords inside Vault. Rotated passwords are returned to the caller and (optionally) synced to a KV v2 path, but the plugin itself retains no record of them.
Process flow
flowchart TD
A[Operator calls Vault path] --> B{Path}
B -->|keycloak/config| C[Store config in Vault storage]
C --> D[Test Keycloak connection via admin token]
B -->|keycloak/roles/name| E[Store role → keycloak_username mapping]
B -->|keycloak/creds/role| F[Load role and config]
F --> G[Generate random password]
G --> H[Call Keycloak Admin API reset-password]
H --> I{KV sync configured?}
I -->|yes| I2[PATCH password into KV v2 secret]
I2 --> I3[Return username and new password]
I -->|no| I3
B -->|keycloak/users| J[List users in target realm]
B -->|keycloak/users/username| K[Read user details]
B -->|keycloak/users/username/rotate| L[Generate password + reset in Keycloak]
L --> M[Return username and new password]
Compatibility
Every change is tested in CI against a matrix of Vault and Keycloak versions; the full integration suite (configure, rotate, verify, KV sync) runs against each pair. The plugin tracks the last MPL-2.0 Vault line, the latest 1.x, and the latest 2.x, plus the latest Keycloak.
| Component | Tested versions |
|---|---|
| Vault | 1.14.10 (last MPL-2.0), 1.21.4 (latest 1.x), 2.0.2 (latest 2.x) |
| Keycloak | 26.6.3 (latest) |
The exact pinned image tags are maintained in
tests/versions.env. Other versions may work but are not
exercised by the suite.
Installation
Download pre-built binaries
Pre-built binaries for Linux, macOS, Windows, and FreeBSD (amd64, arm64, and 386 where applicable) are published on the Releases page.
Each release is signed with cosign keyless
signing: checksums.txt is signed (the signature bundle is
checksums.txt.sigstore.json), and every binary is listed in checksums.txt.
Verify the signature (provenance) first, then the checksum (integrity).
The binary file name embeds the release version
(vault-plugin-secrets-keycloak_<version>_<os>_<arch>), so resolve the latest
version first:
# Example: Linux amd64 (requires cosign and jq)
VERSION=$(curl -fsSL https://api.github.com/repos/RoFz/vault-plugin-secrets-keycloak/releases/latest | jq -r .tag_name)
BINARY="vault-plugin-secrets-keycloak_${VERSION#v}_linux_amd64"
BASE="https://github.com/RoFz/vault-plugin-secrets-keycloak/releases/download/${VERSION}"
curl -fLO "${BASE}/${BINARY}"
curl -fLO "${BASE}/checksums.txt"
curl -fLO "${BASE}/checksums.txt.sigstore.json"
# 1. Provenance: verify checksums.txt was signed by this repo's release workflow.
cosign verify-blob \
--bundle checksums.txt.sigstore.json \
--certificate-identity "https://github.com/RoFz/vault-plugin-secrets-keycloak/.github/workflows/release-please.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txt
# 2. Integrity: verify the downloaded binary against the signed checksums.
sha256sum --check --ignore-missing checksums.txt
The signature uses the Sigstore bundle format (
checksums.txt.sigstore.json); the command above is verified with cosign v2.4.3 and v3.0.6.
Build from source
Requires Go 1.26+.
git clone https://github.com/RoFz/vault-plugin-secrets-keycloak.git
cd vault-plugin-secrets-keycloak
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -o vault-plugin-secrets-keycloak ./cmd/vault-plugin-secrets-keycloak
Adjust GOOS and GOARCH for your target platform.
Deploy to a running Vault instance
The plugin binary must reside in Vault's plugin directory.
Requirements before deploying:
- A running Vault instance with a writable plugin directory (e.g.
/vault/plugins). - A Vault token with permission to register and enable plugins.
- Keycloak admin credentials for the target realm.
Kubernetes / FluxCD (recommended)
Manage the plugin volume using your FluxCD Kustomization and a HelmRelease patch.
- Add a PVC manifest to the same Flux-managed folder used by your Vault release.
- Add a patch file targeting your Vault HelmRelease to mount the PVC at
/vault/plugins. - Reference both in the Kustomization (
resources+patches/patchesStrategicMerge). - Commit and push, then reconcile Flux.
Example HelmRelease patch snippet:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: vault
namespace: vault
spec:
values:
server:
extraVolumes:
- name: plugin-dir
persistentVolumeClaim:
claimName: vault-plugin-pvc
volumeMounts:
- name: plugin-dir
mountPath: /vault/plugins
Example Flux reconcile command:
flux reconcile kustomization <vault-kustomization-name> -n flux-system
Direct manifest method (fallback)
Example PVC manifest for plugin storage:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vault-plugin-pvc
namespace: vault
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Example StatefulSet volume wiring (required so /vault/plugins exists in the pod):
spec:
template:
spec:
containers:
- name: vault
volumeMounts:
- name: plugin-dir
mountPath: /vault/plugins
volumes:
- name: plugin-dir
persistentVolumeClaim:
claimName: vault-plugin-pvc
Copy binary to the Vault pod
Copy binary to the active Vault pod:
kubectl cp ./vault-plugin-secrets-keycloak vault/vault-0:/vault/plugins/vault-plugin-secrets-keycloak
kubectl exec -n vault vault-0 -- chmod 0755 /vault/plugins/vault-plugin-secrets-keycloak
Compute SHA256 from inside the pod (used for plugin registration):
kubectl exec -n vault vault-0 -- sha256sum /vault/plugins/vault-plugin-secrets-keycloak
In an HA cluster with a shared plugin volume, verify all Vault server pods see the same binary:
for pod in $(kubectl get pods -n vault -l component=server -o name); do
echo "$pod:"
kubectl exec -n vault "$pod" -- sha256sum /vault/plugins/vault-plugin-secrets-keycloak
done
All SHA256 values must match before proceeding.
Register and enable
Register and enable the plugin (the -version flag must match the version
reported by the binary):
SHA256=$(kubectl exec -n vault vault-0 -- sha256sum /vault/plugins/vault-plugin-secrets-keycloak | cut -d' ' -f1)
VERSION="vX.Y.Z"
vault plugin register -sha256="$SHA256" -version="$VERSION" secret vault-plugin-secrets-keycloak
vault secrets enable -path=keycloak vault-plugin-secrets-keycloak
Upgrade or remove
To upgrade or remove the plugin, first disable the secrets engine, then deregister the plugin with the version it was registered under:
vault secrets disable keycloak/
vault plugin deregister -version="$VERSION" secret vault-plugin-secrets-keycloak
Configuration
Write the plugin configuration (one config per mount):
vault write keycloak/config \
url="https://keycloak.example.com" \
realm="master" \
target_realm="myrealm" \
master_admin_username="admin" \
master_admin_password='<admin-password>'
KV v2 sync (optional)
When a password is rotated via creds/<role> or users/<username>/rotate,
the plugin can optionally PATCH the new password into a KV v2 secret in
Vault. This is useful for syncing rotated credentials to Kubernetes secrets
(via the Vault Secrets Operator or External Secrets Operator).
To enable KV sync, add the KV fields to the config:
vault write keycloak/config \
url="https://keycloak.example.com" \
master_admin_username="admin" \
master_admin_password='<admin-password>' \
kv_mount_path="k8s" \
kv_secret_path="keycloak/realm-users" \
kv_token="hvs.<token>" \
kv_api_addr="https://vault.vault.svc.cluster.local:8200"
Then set kv_password_key on each role:
vault write keycloak/roles/myuser \
keycloak_username="myuser" \
kv_password_key="myuser-password"
After each vault read keycloak/creds/myuser, the plugin PATCHes
k8s/data/keycloak/realm-users with { "myuser-password": "<new-pw>" }.
If the KV secret does not yet exist, a PUT (create) is used instead.
For ad-hoc rotations via the users path, pass it as a parameter:
vault write keycloak/users/myuser/rotate kv_password_key="myuser-password"
KV sync failures are non-fatal — the rotation still succeeds and a warning is returned in the response.
Creating the KV sync token
The KV sync token needs create, update, and patch capabilities on the
target KV data path. Create a scoped policy and an orphan token:
vault policy write keycloak-kv-sync - <<'POLICY'
path "k8s/data/keycloak/realm-users" {
capabilities = ["create", "update", "patch"]
}
POLICY
vault token create \
-policy=keycloak-kv-sync \
-orphan \
-explicit-max-ttl=8760h \
-ttl=8760h \
-display-name=keycloak-kv-sync
explicit-max-ttlvsmax_lease_ttl: The token auth mount has amax_lease_ttl(default 768h / 32 days) that caps the initial TTL. The-explicit-max-ttlflag sets the absolute maximum lifetime of the token, up to which it can be renewed. The token must be renewed before its current TTL expires. For example, with-ttl=8760hand a mountmax_lease_ttlof 768h, the token is created with a 768h TTL but can be renewed repeatedly until theexplicit-max-ttlof 8760h is reached.
Adjust the policy path to match your kv_mount_path and kv_secret_path.
Multiple Keycloak contexts (untested)
Untested: multiple mount paths are expected to work based on how Vault handles plugin mounts, but this has not been validated against multiple Keycloak realms or deployments.
The plugin stores one config per mount path. To manage multiple Keycloak deployments or realms, enable the plugin at multiple mount paths:
vault secrets enable -path=keycloak-appA vault-plugin-secrets-keycloak
vault secrets enable -path=keycloak-appB vault-plugin-secrets-keycloak
vault write keycloak-appA/config \
url="https://keycloak.example.com" \
realm="master" \
target_realm="appA" \
master_admin_username="admin" \
master_admin_password='<appA-admin-password>'
vault write keycloak-appB/config \
url="https://keycloak-b.example.com" \
realm="master" \
target_realm="appB" \
master_admin_username="admin" \
master_admin_password='<appB-admin-password>'
Expected logs
Check logs from the active Vault pod:
kubectl logs -n vault vault-0 --tail=200
Filter only plugin-relevant messages:
kubectl logs -n vault vault-0 --tail=500 \
| grep -E 'keycloak|password rotated|failed to create Keycloak client|connection test failed|failed to initialise'
Operational/healthy examples:
keycloak secrets engine loaded successfullykeycloak config saved and connection test succeededpassword rotated successfullywith fields such asroleandkeycloak_usernamekv secret updated successfullywith fields such askv_secret_pathandkv_password_key
Error examples:
keycloak secrets engine failed to initialisefailed to create Keycloak clientkeycloak config saved but connection test failedfailed to rotate passwordkv sync failed after password rotation
API reference
All paths below are relative to the mount point (default keycloak/).
config
Configure the Keycloak backend. The plugin authenticates as an admin user via the Resource Owner Password Credentials (ROPC) grant.
| Method | Vault CLI |
|---|---|
| Create / Update | vault write keycloak/config ... |
| Read | vault read keycloak/config |
| Delete | vault delete keycloak/config |
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url |
string | yes | — | Base URL of the Keycloak server. |
realm |
string | no | master |
Auth realm used to obtain admin tokens. |
target_realm |
string | no | value of realm |
Realm whose users will be managed. |
client_id |
string | no | admin-cli |
OIDC client used for the ROPC grant. |
master_admin_username |
string | yes | — | Username of the master realm admin. |
master_admin_password |
string | yes | — | Password of the master realm admin. |
kv_mount_path |
string | no | — | KV v2 mount name for KV sync after rotation. |
kv_secret_path |
string | no | — | Path within the KV v2 mount to PATCH. |
kv_api_addr |
string | no | https://127.0.0.1:8200 |
Vault API address for KV sync requests. |
kv_tls_skip_verify |
bool | no | false |
Skip TLS verification for the KV API. |
kv_token |
string | no | — | Vault token with create/update/patch on the KV data path. |
roles/<name>
Map a Vault role name to a Keycloak username. Used by the alpha
creds/<name> lease-based path.
| Method | Vault CLI |
|---|---|
| Create / Update | vault write keycloak/roles/<name> ... |
| Read | vault read keycloak/roles/<name> |
| Delete | vault delete keycloak/roles/<name> |
| List | vault list keycloak/roles |
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | yes | — | Vault role name. |
keycloak_username |
string | yes | — | Keycloak username whose password will be rotated. |
ttl |
duration | no | 3600 (1 h) |
Lease duration before automatic revocation. |
max_ttl |
duration | no | 86400 (24 h) |
Maximum lease duration. |
kv_password_key |
string | no | — | KV v2 key to PATCH with the new password after rotation via creds/<role>. |
users/
| Method | Vault CLI |
|---|---|
| List | vault list keycloak/users |
Returns the usernames of all users in the target realm (up to 500).
users/<username>
| Method | Vault CLI |
|---|---|
| Read | vault read keycloak/users/<username> |
Returns the user's username, internal Keycloak ID, enabled status, email, first name, and last name.
users/<username>/rotate
| Method | Vault CLI |
|---|---|
| Update | vault write -force keycloak/users/<username>/rotate |
Generates a cryptographically random password (crypto/rand), sets it on
the Keycloak user via the Admin REST API, and returns { username, password }.
The previous password is immediately invalidated. No lease is created.
Optional parameter:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
kv_password_key |
string | no | — | KV v2 key to PATCH with the new password. Omit to skip KV sync. |
creds/<name> (alpha)
| Method | Vault CLI |
|---|---|
| Read | vault read keycloak/creds/<name> |
Generates a password and sets it on the Keycloak user bound to <name>.
Returns { username, password } with a Vault lease. On lease revocation the
password is rotated again to a discarded value, invalidating the issued
credential. On lease renewal the TTL is extended without rotation.
Alpha: the revoke/renew logic is unit-tested, but automatic lease expiry and revocation have not been validated end-to-end against a live Vault lease. See the Credential lifecycle section for caveats.
Usage
List all users in the configured target realm:
vault list keycloak/users
Read a specific user's details:
vault read keycloak/users/<keycloak-username>
Rotate a user's password and return the new value:
vault write -force keycloak/users/<keycloak-username>/rotate
The command generates a cryptographically random password (crypto/rand),
sets it on the Keycloak account via the Admin REST API, and returns
{ username, password }. The previous password is immediately invalidated.
No lease is created; Vault retains no record of the issued credential.
Credential lifecycle
The supported rotation path is fire-and-forget via
vault write -force keycloak/users/<username>/rotate.
The returned password remains valid in Keycloak indefinitely until the next explicit rotation call. Vault retains no record of it and performs no automatic revocation. Every call is recorded in the Vault audit log (caller identity, mount path, timestamp).
Alpha — not recommended for production use yet: The plugin also implements a role-based, lease-bound issuance path (
vault read keycloak/creds/<role>) where Vault manages a TTL and automatically invalidates the credential on expiry by re-rotating the password to a discarded value. The revoke/renew callbacks are unit-tested, but automatic lease expiry and revocation have not been validated end-to-end against a live Vault lease, and are considered alpha. Seepath_credentials.goin the source for implementation details.Alpha caveat — Vault availability at revocation time: Keycloak has no awareness of Vault leases. If Vault is unavailable when a lease TTL expires, the revocation callback is deferred and the issued password remains valid in Keycloak until Vault resumes. This is a known limitation of the alpha lease path and does not affect the supported fire-and-forget rotation path.
Contributing
See CONTRIBUTING.md for development setup, testing, linting, and the Conventional Commits guidelines used in this project.
Security
To report a security vulnerability, please use GitHub Security Advisories rather than a public issue. See SECURITY.md for the full policy.