KV Engine vs Dynamic Secrets Engine
Architecture
flowchart TB
subgraph consumers["Applications / Consumers"]
app1["App A\nvault read generic/creds/my-role"]
app2["App B\nvault read auth0/creds/my-role"]
end
subgraph vault["HashiCorp Vault 2.1+ · TLS :8200"]
engine1["vault-rest-engine\n(Generic REST Plugin)"]
engine3["vault-auth0-engine\n(Auth0 Native Plugin)"]
transit["Transit Engine\ncred-rotation-key\naes256-gcm96"]
pki["PKI Engine\nInternal CA"]
kv["KV v2\nTransit-encrypted\nadapter config"]
end
subgraph broker["Credential Rotation API · mTLS :8443"]
direction LR
a0["Auth0\nAdapter"]
sp["Splunk\nAdapter"]
sq["SonarQube\nAdapter"]
end
subgraph saas["SaaS Providers · TLS"]
auth0["Auth0\nMgmt API"]
splunk["Splunk\nREST API"]
sonar["SonarQube\nWeb API"]
end
app1 -- "TLS" --> engine1
app2 -- "TLS" --> engine3
engine1 -- "mTLS\nclient cert from PKI" --> a0
engine3 -- "TLS" --> auth0
a0 -- "TLS" --> auth0
sp -- "TLS" --> splunk
sq -- "TLS" --> sonar
engine1 & broker -- "Transit encrypt/decrypt\nenvelope encryption" --> transit
broker -- "Transit-decrypt\nadapter config" --> kv
pki -- "issues client cert\nfor vault-rest-engine" --> engine1
The AuthZEN Pattern Parallel
Just as AuthZEN standardises the authorization decision interface so any PDP can implement it, the Credential Rotation API standardises the credential lifecycle interface so any SaaS adapter can implement it.
POST /access/v1/evaluation
{ subject, resource, action, context }
→ { decision: bool }
Any PDP implements this contract. Callers don't know if it's OPA, OpenFGA, or an in-memory PDP.
POST /v1/credentials/rotate
{ provider, resource_id, lease_id }
→ { encrypted_value, expires_at }
Any SaaS adapter implements this contract. Vault doesn't know if it's Auth0, Splunk, or SonarQube.
Security Model — Defence in Depth
All Vault API traffic (consumer → Vault). Enforced by Vault; cannot be disabled in production mode.
Plugin → Credential Rotation API. Both sides present certificates. API rejects any client without a cert signed by the internal PKI CA. The cert IS the plugin's identity — no bearer tokens.
New credentials are Transit-encrypted by the API before being sent to the plugin over mTLS. Each rotate call generates a unique DEK (AES-256-GCM) — the same credential rotated twice produces two unrelated ciphertexts. Correlation and replay attacks blocked at the envelope level.
SaaS adapter credentials (Auth0 mgmt secret, Splunk token) stored in KV encrypted via Transit. The KV backend at rest cannot be read without the Transit master key. Plaintext credentials never touch Vault storage.
Vault's backend storage (Raft) is always encrypted with Vault's own seal key. This baseline applies even before Transit adds its layer.
The plaintext credential exists only: (1) in the SaaS API's TLS response at the adapter, and (2) in the Vault plugin's process memory immediately before returning to the consumer. It is never logged, never stored in KV, never written to disk.
Credential Rotation Flow
sequenceDiagram
participant App as Consumer App
participant Vault as HashiCorp Vault
participant Plugin as vault-rest-engine
participant Transit as Transit Engine
participant API as cred-rotation-api
participant Auth0 as Auth0 Mgmt API
App->>Vault: vault read generic/creds/my-role (TLS)
Vault->>Plugin: dispatch to plugin process (gRPC + mTLS)
Plugin->>API: POST /v1/credentials/rotate (mTLS + client cert)
API->>Auth0: POST /api/v2/clients/{id}/rotate-secret (TLS)
Auth0-->>API: new_client_secret (plaintext, in TLS channel)
API->>Transit: POST /v1/transit/encrypt/cred-rotation-key
Note over API,Transit: Envelope encryption — unique DEK per call
Transit-->>API: vault:v1:AbC123... (ciphertext)
API-->>Plugin: { encrypted_value: "vault:v1:AbC123..." } (mTLS)
Note over API,Plugin: plaintext never leaves API unencrypted
Plugin->>Transit: POST /v1/transit/decrypt/cred-rotation-key
Transit-->>Plugin: plaintext new_secret (in plugin memory only)
Plugin-->>Vault: { secret_value, lease_id, ttl: 86400 }
Vault-->>App: secret (TLS)
POST /v1/credentials/revoke — Auth0 immediately invalidates the old credential.Credential Rotation API Contract
{ provider, resource_id, lease_id, context? }{ provider, resource_id, lease_id, credential_id }Implementation Phases
Quick Start
Go 1.26+, Vault 2.1+ (brew install hashicorp/tap/vault), an Auth0 dev tenant with a Management API M2M application.
# Clone and configure git clone https://github.com/jralmaraz/vault-secrets-broker.git cd vault-secrets-broker cp .env.example .env # edit .env with your Auth0 tenant credentials # Run Phase 1 — starts Vault and configures all engines make setup # Load the Vault environment source .vault-env # Verify the Auth0 client_secret is Transit-encrypted (not plaintext) vault kv get secret/cred-rotation-api/adapters/auth0 # mgmt_client_secret_encrypted = vault:v1:... ← ciphertext ✓ # Verify Transit decrypt round-trip CIPHER=$(vault kv get -field=mgmt_client_secret_encrypted \ secret/cred-rotation-api/adapters/auth0) vault write -field=plaintext transit/decrypt/cred-rotation-key \ ciphertext="$CIPHER" | base64 -d # → your original client_secret ✓
Supply Chain Security
go.sum. Tampered modules fail immediately.