skip to content

What is the difference between KV secret engine v1 and v2 in Spring Cloud Vault, and how does configuration differ?

level: middleimportance: should knowfreq 45%

answer

  1. v1 = flat, no history; v2 = versioned + soft-delete
  2. v2 path has /data/ (and /metadata/)
  3. backend-version: 1 vs 2
  4. response.data.data nesting for v2
  5. version mismatch -> 404 / wrong keys

basics

~20 s

KV 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 s

The 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
java
// 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

for a junior

Know v2 is versioned and v1 is not.

for a middle

Explain the /data/ path difference and the backend-version property, and the version-mismatch pitfall.

for a senior

Discuss migration between versions, default-context sharing, and why versioning is history not push-refresh.

for a principal

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

context