Home/Blog/Vault KV v2 Secrets Engine: Complete Versioning and Management Guide
Secrets Management

Vault KV v2 Secrets Engine: Complete Versioning and Management Guide

Master HashiCorp Vault KV v2 secrets engine with versioning, soft delete, metadata operations, and check-and-set. Complete guide for secret lifecycle management and migration from KV v1.

By InventiveHQ Team
Vault KV v2 Secrets Engine: Complete Versioning and Management Guide

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

FeatureKV v1KV v2
VersioningNoYes (default: 10 versions)
Soft deleteNoYes (recoverable)
MetadataNoYes (per-secret)
Custom metadataNoYes (key-value pairs)
Check-and-setNoYes (CAS)
CLI commandsvault read/writevault kv get/put
API path/v1/secret/path/v1/secret/data/path
Delete all dataSingle deleteRequires 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

OptionDescriptionDefault
max_versionsMaximum versions per secret10
cas_requiredRequire CAS for all writesfalse
delete_version_afterAuto-delete versions after duration0 (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

CommandDescription
vault kv put PATH KEY=VALUEWrite/update secret
vault kv get PATHRead latest version
vault kv get -version=N PATHRead specific version
vault kv get -field=KEY PATHRead single field
vault kv list PATHList secrets at path
vault kv delete PATHSoft delete latest version
vault kv delete -versions=N PATHSoft delete specific versions
vault kv undelete -versions=N PATHRecover soft-deleted versions
vault kv destroy -versions=N PATHPermanently destroy versions
vault kv metadata get PATHRead metadata and version history
vault kv metadata put PATHUpdate metadata/settings
vault kv metadata delete PATHDelete all versions and metadata
vault kv enable-versioning PATHUpgrade KV v1 to v2

Next Steps

For more Vault secrets management guides, explore our complete HashiCorp Vault series.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.