The Key-Value (KV) secrets engine version 2 is Vault's most commonly used secrets engine for storing static secrets like API keys, database credentials, and configuration data. KV v2 adds powerful versioning, soft delete, and metadata capabilities that make secret management more robust and auditable.
Understanding KV v2
KV v2 stores secrets with full version history, enabling recovery from mistakes and audit of changes over time.
┌─────────────────────────────────────────────────────────────────────┐
│ KV v2 Secret Structure │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ secret/myapp │
│ ├── Metadata │
│ │ ├── created_time: 2024-01-01T10:00:00Z │
│ │ ├── current_version: 3 │
│ │ ├── max_versions: 10 │
│ │ └── custom_metadata: {owner: platform-team} │
│ │ │
│ └── Versions │
│ ├── Version 1 (destroyed) │
│ ├── Version 2 (deleted, recoverable) │
│ └── Version 3 (current) │
│ ├── username: admin │
│ └── password: secret123 │
│ │
└─────────────────────────────────────────────────────────────────────┘
KV v1 vs v2 Comparison
| Feature | KV v1 | KV v2 |
|---|---|---|
| Versioning | No | Yes (default: 10 versions) |
| Soft delete | No | Yes (recoverable) |
| Metadata | No | Yes (per-secret) |
| Custom metadata | No | Yes (key-value pairs) |
| Check-and-set | No | Yes (CAS) |
| CLI commands | vault read/write | vault kv get/put |
| API path | /v1/secret/path | /v1/secret/data/path |
| Delete all data | Single delete | Requires metadata delete |
Enabling KV v2
New Installation
# Enable KV v2 at a path
vault secrets enable -version=2 -path=secret kv
# With custom configuration
vault secrets enable \
-version=2 \
-path=app-secrets \
-description="Application secrets with versioning" \
kv
Upgrade Existing KV v1
# Upgrade KV v1 to v2 (preserves data as version 1)
vault kv enable-versioning secret/
# Verify the upgrade
vault secrets list -detailed | grep secret
Warning: Upgrading to v2 is irreversible. You cannot downgrade back to v1.
Writing Secrets
Basic Write Operations
# Write a single key-value pair
vault kv put secret/myapp password='supersecret'
# Write multiple key-value pairs
vault kv put secret/database \
username='dbadmin' \
password='dbpassword123' \
host='db.example.com' \
port='5432'
Writing from Files
# Create a JSON file
cat > db-config.json << 'EOF'
{
"username": "admin",
"password": "secret",
"connection_string": "postgres://host:5432/db"
}
EOF
# Write from file
vault kv put secret/database @db-config.json
# Clean up
rm db-config.json
Writing from Standard Input
# Pipe from environment (password not in command history)
echo -n "$DB_PASSWORD" | vault kv put secret/database password=-
# Generate and store random secret
openssl rand -base64 32 | vault kv put secret/api-key value=-
Reading Secrets
Basic Read Operations
# Read latest version
vault kv get secret/myapp
# Output:
# ====== Secret Path ======
# secret/data/myapp
#
# ======= Metadata =======
# Key Value
# --- -----
# created_time 2024-01-15T10:30:00.000000Z
# custom_metadata <nil>
# deletion_time n/a
# destroyed false
# version 3
#
# ====== Data ======
# Key Value
# --- -----
# password supersecret
Read Specific Version
# Read version 2
vault kv get -version=2 secret/myapp
# Read the first version ever written
vault kv get -version=1 secret/myapp
Read Specific Fields
# Get only the password field
vault kv get -field=password secret/myapp
# Use in scripts
DB_PASS=$(vault kv get -field=password secret/database)
export DB_PASS
JSON Output
# Full JSON output
vault kv get -format=json secret/myapp
# Parse with jq
vault kv get -format=json secret/myapp | jq -r '.data.data.password'
# Get all data as JSON object
vault kv get -format=json secret/myapp | jq '.data.data'
Version Management
Viewing Version History
# Get metadata including all version info
vault kv metadata get secret/myapp
# Output:
# ========== Metadata ==========
# Key Value
# --- -----
# cas_required false
# created_time 2024-01-10T10:00:00.000000Z
# current_version 3
# custom_metadata <nil>
# delete_version_after 0s
# max_versions 10
# oldest_version 1
# updated_time 2024-01-15T10:30:00.000000Z
#
# ====== Version 1 ======
# Key Value
# --- -----
# created_time 2024-01-10T10:00:00.000000Z
# deletion_time n/a
# destroyed true
#
# ====== Version 2 ======
# Key Value
# --- -----
# created_time 2024-01-12T14:00:00.000000Z
# deletion_time 2024-01-13T09:00:00.000000Z
# destroyed false
#
# ====== Version 3 ======
# Key Value
# --- -----
# created_time 2024-01-15T10:30:00.000000Z
# deletion_time n/a
# destroyed false
Rollback to Previous Version
# Read old version
OLD_DATA=$(vault kv get -format=json -version=2 secret/myapp | jq '.data.data')
# Write it as new version
echo "$OLD_DATA" | vault kv put secret/myapp -
Deleting Secrets
Soft Delete (Recoverable)
# Delete latest version (soft delete)
vault kv delete secret/myapp
# Delete specific versions
vault kv delete -versions=1,2 secret/myapp
Undelete (Recover Soft-Deleted)
# Recover specific versions
vault kv undelete -versions=2,3 secret/myapp
# Verify recovery
vault kv get -version=2 secret/myapp
Destroy (Permanent)
# Permanently destroy specific versions
vault kv destroy -versions=1,2 secret/myapp
# Verify destruction
vault kv metadata get secret/myapp
# Shows: destroyed = true for those versions
Delete Everything
# Delete all versions and metadata (cannot be undone)
vault kv metadata delete secret/myapp
Metadata Operations
Reading Metadata
vault kv metadata get secret/myapp
Setting Custom Metadata
# Add custom metadata
vault kv metadata put \
-custom-metadata=environment=production \
-custom-metadata=owner=platform-team \
-custom-metadata=rotation-period=30d \
secret/myapp
# Read to verify
vault kv metadata get secret/myapp
Configuring Secret Settings
# Set maximum versions to keep
vault kv metadata put -max-versions=20 secret/myapp
# Enable check-and-set requirement
vault kv metadata put -cas-required=true secret/myapp
# Auto-delete versions after time period
vault kv metadata put -delete-version-after=90d secret/myapp
Check-and-Set (CAS)
Check-and-set prevents concurrent write conflicts by requiring version verification.
How CAS Works
Client A: Read secret (version 3)
Client B: Read secret (version 3)
Client A: Write with -cas=3 ✓ (creates version 4)
Client B: Write with -cas=3 ✗ (fails - current is now 4)
Using CAS
# Check current version
vault kv metadata get secret/myapp | grep current_version
# Write only if version matches
vault kv put -cas=3 secret/myapp password='newpassword'
# If version doesn't match:
# Error: check-and-set parameter did not match the current version
Requiring CAS for a Secret
# Require CAS for all writes to this secret
vault kv metadata put -cas-required=true secret/myapp
# Now writes without -cas flag fail:
vault kv put secret/myapp password='test'
# Error: check-and-set parameter required for this call
# Must specify expected version:
vault kv put -cas=4 secret/myapp password='test'
Mount-Level Configuration
Tuning the Secrets Engine
# Set default max versions for all secrets
vault secrets tune -options=max_versions=5 secret/
# Set default delete behavior
vault secrets tune -options=delete_version_after=180d secret/
# View current configuration
vault secrets list -detailed | grep secret
Configuration Options
| Option | Description | Default |
|---|---|---|
max_versions | Maximum versions per secret | 10 |
cas_required | Require CAS for all writes | false |
delete_version_after | Auto-delete versions after duration | 0 (disabled) |
Listing Secrets
# List secrets at a path
vault kv list secret/
# List secrets in subdirectory
vault kv list secret/production/
# JSON output for scripting
vault kv list -format=json secret/
API Usage
Write via API
curl -X POST \
-H "X-Vault-Token: $VAULT_TOKEN" \
-d '{"data": {"username": "admin", "password": "secret"}}' \
$VAULT_ADDR/v1/secret/data/myapp
Read via API
# Read latest version
curl -s \
-H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/secret/data/myapp | jq '.data.data'
# Read specific version
curl -s \
-H "X-Vault-Token: $VAULT_TOKEN" \
"$VAULT_ADDR/v1/secret/data/myapp?version=2" | jq '.data.data'
Delete via API
# Soft delete
curl -X DELETE \
-H "X-Vault-Token: $VAULT_TOKEN" \
$VAULT_ADDR/v1/secret/data/myapp
# Destroy specific versions
curl -X POST \
-H "X-Vault-Token: $VAULT_TOKEN" \
-d '{"versions": [1, 2]}' \
$VAULT_ADDR/v1/secret/destroy/myapp
Migration from KV v1 to v2
In-Place Upgrade
# 1. Check current secrets engine version
vault secrets list -detailed | grep -E "^secret/"
# 2. Upgrade to v2 (preserves data as version 1)
vault kv enable-versioning secret/
# 3. Verify upgrade
vault kv get secret/myapp
# Should show version: 1
Side-by-Side Migration
For zero-downtime migration:
# 1. Enable new KV v2 mount
vault secrets enable -version=2 -path=secret-v2 kv
# 2. Copy secrets (script example)
for path in $(vault kv list -format=json secret/ | jq -r '.[]'); do
vault kv get -format=json "secret/$path" | \
jq '.data.data' | \
vault kv put "secret-v2/$path" -
done
# 3. Update applications to use new path
# 4. Disable old mount when ready
vault secrets disable secret/
# 5. Optionally rename new mount
vault secrets move secret-v2/ secret/
Best Practices
1. Use Descriptive Paths
# Good: Hierarchical, meaningful paths
vault kv put secret/production/database/postgres password=...
vault kv put secret/staging/api/stripe api_key=...
# Avoid: Flat, unclear paths
vault kv put secret/db1 password=...
vault kv put secret/key1 value=...
2. Set Appropriate Version Limits
# High-change secrets: fewer versions
vault kv metadata put -max-versions=5 secret/ephemeral-token
# Critical secrets: more versions
vault kv metadata put -max-versions=25 secret/production/database
3. Use Custom Metadata for Organization
vault kv metadata put \
-custom-metadata=team=security \
-custom-metadata=last-rotated=2024-01-15 \
-custom-metadata=rotation-policy=monthly \
secret/production/credentials
4. Enable CAS for Critical Secrets
vault kv metadata put -cas-required=true secret/production/master-key
5. Implement Policies with Version Awareness
# Policy allowing read of current but not delete/destroy
path "secret/data/production/*" {
capabilities = ["read"]
}
# Policy for admins who can manage versions
path "secret/data/production/*" {
capabilities = ["create", "read", "update"]
}
path "secret/delete/production/*" {
capabilities = ["update"]
}
path "secret/undelete/production/*" {
capabilities = ["update"]
}
path "secret/destroy/production/*" {
capabilities = ["update"]
}
path "secret/metadata/production/*" {
capabilities = ["read", "list"]
}
Troubleshooting
"No Value Found" Error
No value found at secret/data/myapp
Causes:
- Secret doesn't exist at that path
- Using v1 commands with v2 engine
- Version was destroyed
Solution:
# List available secrets
vault kv list secret/
# Check if using correct command
vault kv get secret/myapp # v2
vault read secret/myapp # v1
"Invalid Path" for Versioned Engine
Error: Invalid path for a versioned K/V secrets engine
Cause: Using vault read/write with KV v2
Solution: Use vault kv get/put commands for KV v2
"Check-and-Set Parameter Required"
Error: check-and-set parameter required for this call
Cause: CAS is required for this secret
Solution:
# Get current version
VERSION=$(vault kv metadata get -format=json secret/myapp | jq -r '.data.current_version')
# Write with CAS
vault kv put -cas=$VERSION secret/myapp password='new'
Command Reference
| Command | Description |
|---|---|
vault kv put PATH KEY=VALUE | Write/update secret |
vault kv get PATH | Read latest version |
vault kv get -version=N PATH | Read specific version |
vault kv get -field=KEY PATH | Read single field |
vault kv list PATH | List secrets at path |
vault kv delete PATH | Soft delete latest version |
vault kv delete -versions=N PATH | Soft delete specific versions |
vault kv undelete -versions=N PATH | Recover soft-deleted versions |
vault kv destroy -versions=N PATH | Permanently destroy versions |
vault kv metadata get PATH | Read metadata and version history |
vault kv metadata put PATH | Update metadata/settings |
vault kv metadata delete PATH | Delete all versions and metadata |
vault kv enable-versioning PATH | Upgrade KV v1 to v2 |
Next Steps
- Learn Reading and Writing Secrets for basic operations
- Configure Vault Policies for access control
- Set up AppRole Authentication for automation
For more Vault secrets management guides, explore our complete HashiCorp Vault series.


