browser-vault-gui/KV_VERSIONS.md
2025-10-20 18:45:52 +02:00

7.5 KiB

HashiCorp Vault KV Secret Engine Versions

Overview

HashiCorp Vault has two versions of the Key-Value (KV) secret engine:

  • KV v1: Simple key-value storage (legacy)
  • KV v2: Versioned secrets with metadata (recommended)

This application supports both versions and automatically handles the path differences.

Key Differences

Feature KV v1 KV v2
Versioning No Yes
Metadata No Yes
Soft Delete No Yes
Path Structure Simple Nested (data/metadata)
API Calls Direct Through /data or /metadata
Rollback No Yes
Check-and-Set No Yes

Path Structures

KV v1

secret/myapp/database
secret/myapp/api-keys
secret/team/config

Simple, direct paths. What you see is what you get.

KV v2

secret/data/myapp/database      # For reading/writing secrets
secret/metadata/myapp/database  # For metadata operations

KV v2 uses special path prefixes:

  • /data/ for reading and writing secret data
  • /metadata/ for listing and metadata operations

How Our Client Handles This

Automatic Path Transformation

Our VaultClient automatically transforms paths based on the KV version:

// You write this:
await client.read('secret/myapp/database');

// KV v1: Uses path as-is
// → GET /v1/secret/myapp/database

// KV v2: Automatically adds /data/
// → GET /v1/secret/data/myapp/database

List Operations

KV v1:

await client.list('secret/myapp/');
// → LIST /v1/secret/myapp/?list=true

KV v2:

await client.list('secret/myapp/');
// → LIST /v1/secret/metadata/myapp/?list=true
//   (uses /metadata/ for listing)

Read Operations

KV v1:

const data = await client.read('secret/myapp/config');
// Returns: { username: '...', password: '...' }

KV v2:

const data = await client.read('secret/myapp/config');
// Automatically unwraps: data.data.data → { username: '...', password: '...' }

Write Operations

KV v1:

await client.write('secret/myapp/config', {
  username: 'admin',
  password: 'secret'
});
// POST /v1/secret/myapp/config
// Body: { username: '...', password: '...' }

KV v2:

await client.write('secret/myapp/config', {
  username: 'admin',
  password: 'secret'
});
// POST /v1/secret/data/myapp/config
// Body: { data: { username: '...', password: '...' } }
//       (wrapped in data object)

Configuring KV Version

In the UI

When adding a Vault server:

  1. Click "Add Server"
  2. Fill in the server details
  3. Select KV Secret Engine Version:
    • KV v2 (recommended) - Default, most common
    • KV v1 (legacy) - For older Vault installations

Detecting KV Version

If you're unsure which version your Vault uses:

# Check your Vault server
vault secrets list -detailed

# Look for the "Options" column
# version=2 means KV v2
# No version or version=1 means KV v1

Or use the API:

curl -H "X-Vault-Token: $VAULT_TOKEN" \
  $VAULT_ADDR/v1/sys/internal/ui/mounts/secret

Look for "options": {"version": "2"} in the response.

When to Use KV v1 vs KV v2

  • Starting a new Vault installation
  • You need versioning and rollback
  • You want soft delete (undelete capability)
  • You need to track secret history
  • You want check-and-set operations

Use KV v1 Only When:

  • Legacy Vault installation that can't be upgraded
  • Specific requirement to not version secrets
  • Very simple use case without versioning needs

Example Workflows

Reading a Secret

// Same code works for both versions!
const data = await client.read('secret/myapp/database');
console.log(data.username);
console.log(data.password);

Listing Secrets

// Same code works for both versions!
const keys = await client.list('secret/myapp/');
console.log(keys);
// ['config', 'database/', 'api-keys/']

Searching Recursively

// Uses list() internally, works with both versions
const results = await vaultApi.searchPaths(
  server,
  credentials,
  'secret/',
  'database'
);

KV v2 Specific Features

Metadata Operations

// Only available in KV v2
const metadata = await client.readMetadata('secret/myapp/database');

console.log(metadata.current_version);  // 5
console.log(metadata.versions);
// {
//   "1": { created_time: "...", destroyed: false },
//   "2": { created_time: "...", destroyed: false },
//   "3": { created_time: "...", destroyed: true },
//   "4": { created_time: "...", destroyed: false },
//   "5": { created_time: "...", destroyed: false }
// }

Version History

KV v2 keeps track of all versions:

  • Can read previous versions
  • Can undelete soft-deleted secrets
  • Can permanently destroy specific versions
  • Can destroy all versions

Soft Delete vs Hard Delete

Soft Delete (KV v2):

await client.delete('secret/myapp/database');
// Secret is "deleted" but can be undeleted
// Metadata still exists

Hard Delete (KV v1):

await client.delete('secret/myapp/database');
// Secret is permanently gone

Troubleshooting

Error: "no handler for route"

Error: Vault API error: no handler for route 'secret/data/...'

Cause: Your Vault is using KV v1, but the client is configured for KV v2.

Solution: Edit the server configuration and change KV version to v1.

Error: "1 error occurred: * permission denied"

This can happen if:

  1. You don't have permission to the path
  2. You're using the wrong KV version (v1 paths on v2 or vice versa)

Solution:

  • Check your Vault policies
  • Verify the KV version in server settings

Paths Look Wrong

If you see paths like secret/data/data/myapp:

Cause: You're manually adding /data/ when the client already does it for KV v2.

Solution: Use simple paths like secret/myapp. The client adds /data/ or /metadata/ automatically.

Migration: KV v1 → KV v2

If you're migrating from KV v1 to KV v2:

  1. Backup all secrets from KV v1
  2. Enable KV v2 on a new mount point
  3. Migrate secrets to new paths
  4. Update application to use new KV version
  5. Test thoroughly
  6. Switch traffic to new mount

In this GUI:

  1. Add a new server entry with the same URL
  2. Set KV version to v2
  3. Use new mount path (e.g., secretv2/ instead of secret/)

Best Practices

  1. Use KV v2 for new installations
  2. Configure correct version when adding servers
  3. Don't mix KV v1 and v2 mount points on same server without proper labeling
  4. Use simple paths - let the client handle /data/ and /metadata/ prefixes
  5. Document which mounts use which version for your team

API Reference

All examples work transparently with both versions:

// List
const keys = await client.list('secret/myapp/');

// Read
const data = await client.read('secret/myapp/config');

// Write
await client.write('secret/myapp/config', { key: 'value' });

// Delete
await client.delete('secret/myapp/config');

// Metadata (KV v2 only)
const meta = await client.readMetadata('secret/myapp/config');

Summary

Both versions are fully supported Paths are automatically transformed Select correct version when adding server Use KV v2 for new installations Code is the same for both versions

The client handles all the complexity, so you can focus on managing your secrets!