What is the difference between KV secret engine v1 and v2 in Spring Cloud Vault, and how does configuration differ?
answer
- v1 = flat, no history; v2 = versioned + soft-delete
- v2 path has /data/ (and /metadata/)
- backend-version: 1 vs 2
- response.data.data nesting for v2
- version mismatch -> 404 / wrong keys
basics
~20 sKV v1 stores a single current value per key; KV v2 adds versioning (history, soft-delete) and uses a different read path with a /data/ segment. Spring Cloud Vault handles the path difference when you set the backend version.
solid answer
~40 sThe KV (key/value) engine stores static secrets. **v1** is a plain map: one value per path, overwrites lose history. **v2** adds versioning — it keeps a history of versions, supports soft-delete/undelete and metadata. The wire difference is the API path: v2 reads/writes go to `<mount>/data/<path>` and metadata to `<mount>/metadata/<path>`, whereas v1 uses `<mount>/<path>` directly. In Spring Cloud Vault you don't build those paths manually — you declare the mount and version and it inserts `/data/` for v2 automatically. Configure via `spring.cloud.vault.kv.enabled=true`, `spring.cloud.vault.kv.backend=secret` (the mount), and `spring.cloud.vault.kv.backend-version=2` (default in recent versions; set `1` for legacy mounts). Getting the version wrong is a classic bug: pointing v2 config at a v1 mount (or vice-versa) yields 404s or an unexpected extra `data` nesting in your property keys.
code
java · 16 lines// Reading a versioned (KV v2) secret directly via the template
@Service
class SecretReader {
private final VaultTemplate vaultTemplate;
SecretReader(VaultTemplate vaultTemplate) { this.vaultTemplate = vaultTemplate; }
String currentDbPassword() {
// v2 -> mount "secret", key "myapp"; template inserts /data/ for you
VaultVersionedKeyValueOperations kv =
vaultTemplate.opsForVersionedKeyValue("secret");
Versioned<Map<String, Object>> v = kv.get("myapp");
return v != null && v.hasData()
? (String) v.getRequiredData().get("db.password")
: null;
}
}go deeper
Know v2 is versioned and v1 is not.
Explain the /data/ path difference and the backend-version property, and the version-mismatch pitfall.
Discuss migration between versions, default-context sharing, and why versioning is history not push-refresh.
Weigh KV (static) vs dynamic engines for a service's threat model and operational rotation strategy.
**KV (key/value) engine** is Vault's simplest secret backend — it stores static secrets you write and read (as opposed to *dynamic* engines like database/PKI that generate credentials on demand). It comes in two versions: **KV v1:** - A flat map: each path holds one set of fields; writing overwrites, with no history. - API path is direct: read `GET <mount>/<path>` (e.g. `secret/myapp`). - No soft delete, no metadata, no CAS (check-and-set). **KV v2:** - **Versioned**: every write creates a new version; older versions are retained (configurable retention). You can read a specific version, soft-delete a version, undelete, or permanently `destroy` it. - **Different API layout**: data operations go to `<mount>/data/<path>` and version metadata to `<mount>/metadata/<path>`. The stored fields live under a `data` object in the JSON response (`response.data.data`). - Supports CAS writes and per-secret metadata. **How Spring Cloud Vault handles it:** you never hand-write the `/data/` segment. You configure the mount and version, and the client (`VaultKeyValueOperations` / `VaultVersionedKeyValueTemplate` for v2, `VaultKeyValueTemplate` for v1) inserts the correct segment. Relevant properties: ```yaml spring: cloud: vault: kv: enabled: true backend: secret # the mount point backend-version: 2 # 1 or 2; recent Spring Cloud Vault defaults to 2 default-context: application # shared path read for all apps application-name: myapp # per-app path (defaults to spring.application.name) profile-separator: '/' ``` With the above, Spring reads `secret/myapp`, `secret/myapp/<profile>`, `secret/application`, etc., translating each to the v2 `secret/data/...` path under the hood, and flattens the returned fields into properties. **Edge cases & gotchas:** - **Version mismatch is the #1 problem.** If your mount is v2 but you set `backend-version: 1`, reads hit `secret/myapp` (which doesn't exist for a v2 mount) and you get nothing/404. If your mount is v1 but you set version 2, the extra `/data/` path is wrong. When migrating, check the actual mount version with `vault secrets list -detailed`. - **`enabled: false`** on the generic backend if you only use dynamic engines — otherwise Spring tries to read a KV path that may not exist. - **`fail-fast`** interacts with missing paths: with fail-fast true, a 404 on a required path can abort startup depending on config. - **Not automatically rotated:** KV secrets are static. Changing a value in Vault does not update a running app until you call `/actuator/refresh` (with `@RefreshScope` beans) — versioning in v2 is about history, not push updates. - **`default-context`** lets you share common secrets across services without repeating them per app.
- You migrated a mount from v1 to v2 and now Spring Cloud Vault reads nothing. What's the likely cause?`spring.cloud.vault.kv.backend-version` is still `1`, so Spring reads the direct `secret/myapp` path instead of the v2 `secret/data/myapp` path. Set `backend-version: 2` to match the mount.
- Does KV v2 versioning mean a running app automatically sees a new secret version?No. Versioning only keeps history in Vault. A running app still holds the value read at startup; you need `/actuator/refresh` with `@RefreshScope` beans (or a lease-driven refresh for dynamic engines) to pick up a change.
saying these in an interview costs you the question
- Claiming KV v2 automatically pushes new secret versions to running apps
- Thinking you must manually add /data/ to the path in Spring config
- Believing KV secrets have leases that rotate like database credentials