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:
- Click "Add Server"
- Fill in the server details
- 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
Use KV v2 (Recommended) When:
- ✅ 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:
- You don't have permission to the path
- 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:
- Backup all secrets from KV v1
- Enable KV v2 on a new mount point
- Migrate secrets to new paths
- Update application to use new KV version
- Test thoroughly
- Switch traffic to new mount
In this GUI:
- Add a new server entry with the same URL
- Set KV version to v2
- Use new mount path (e.g.,
secretv2/instead ofsecret/)
Best Practices
- Use KV v2 for new installations
- Configure correct version when adding servers
- Don't mix KV v1 and v2 mount points on same server without proper labeling
- Use simple paths - let the client handle /data/ and /metadata/ prefixes
- 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!