# 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: ```typescript // 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:** ```typescript await client.list('secret/myapp/'); // → LIST /v1/secret/myapp/?list=true ``` **KV v2:** ```typescript await client.list('secret/myapp/'); // → LIST /v1/secret/metadata/myapp/?list=true // (uses /metadata/ for listing) ``` ### Read Operations **KV v1:** ```typescript const data = await client.read('secret/myapp/config'); // Returns: { username: '...', password: '...' } ``` **KV v2:** ```typescript const data = await client.read('secret/myapp/config'); // Automatically unwraps: data.data.data → { username: '...', password: '...' } ``` ### Write Operations **KV v1:** ```typescript await client.write('secret/myapp/config', { username: 'admin', password: 'secret' }); // POST /v1/secret/myapp/config // Body: { username: '...', password: '...' } ``` **KV v2:** ```typescript 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: ```bash # 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: ```bash 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 ```typescript // Same code works for both versions! const data = await client.read('secret/myapp/database'); console.log(data.username); console.log(data.password); ``` ### Listing Secrets ```typescript // Same code works for both versions! const keys = await client.list('secret/myapp/'); console.log(keys); // ['config', 'database/', 'api-keys/'] ``` ### Searching Recursively ```typescript // Uses list() internally, works with both versions const results = await vaultApi.searchPaths( server, credentials, 'secret/', 'database' ); ``` ## KV v2 Specific Features ### Metadata Operations ```typescript // 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):** ```typescript await client.delete('secret/myapp/database'); // Secret is "deleted" but can be undeleted // Metadata still exists ``` **Hard Delete (KV v1):** ```typescript 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: ```typescript // 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!