HashiCorp Vault — Key Rotation
Key rotation creates a new version of the encryption key. Old versions are retained for decryption, but new encryption uses the latest version.
How Key Rotation Works
Before rotation:
Key v1 ──── encrypts + decrypts
After rotation:
Key v1 ──── decrypts only (old data)
Key v2 ──── encrypts + decrypts (new data)
After another rotation:
Key v1 ──── decrypts only
Key v2 ──── decrypts only
Key v3 ──── encrypts + decrypts (new data)
Rotate a Key
# Rotate the key
vault write -f transit/keys/my-app-key/rotate
# Verify — latest_version is now 2
vault read transit/keys/my-app-key
# latest_version: 2
# keys: map[1:1705312800 2:1705399200]
Auto-Rotation
# Set automatic rotation every 30 days
vault write transit/keys/my-app-key/config \
auto_rotate_period=720h
# Check current config
vault read transit/keys/my-app-key
# auto_rotate_period: 720h
Encryption After Rotation
After rotation, new encryptions use the latest key version:
# Before rotation — encrypted with v1
vault write transit/encrypt/my-app-key \
plaintext=$(echo -n "secret-before" | base64)
# ciphertext: vault:v1:abc...
# Rotate
vault write -f transit/keys/my-app-key/rotate
# After rotation — encrypted with v2
vault write transit/encrypt/my-app-key \
plaintext=$(echo -n "secret-after" | base64)
# ciphertext: vault:v2:def...
Both vault:v1:... and vault:v2:... can be decrypted — Vault retains old key versions.
Rewrapping
Rewrap re-encrypts existing ciphertext with the latest key version — without revealing the plaintext:
# Old ciphertext encrypted with v1
vault write transit/rewrap/my-app-key \
ciphertext="vault:v1:abc..."
Expected output:
Key Value
--- -----
ciphertext vault:v2:xyz...
key_version 2
Batch Rewrap
curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
--data '{
"batch_input": [
{"ciphertext": "vault:v1:abc..."},
{"ciphertext": "vault:v1:def..."},
{"ciphertext": "vault:v1:ghi..."}
]
}' \
"$VAULT_ADDR/v1/transit/rewrap/my-app-key" | jq
Rewrap Migration Script
#!/bin/bash
# rewrap-secrets.sh — Rewrap all ciphertexts in a database column
# 1. Read old ciphertext from DB
# 2. Rewrap via Vault
# 3. Update DB with new ciphertext
psql -h db.example.com -U app -d mydb -t -c \
"SELECT id, encrypted_ssn FROM users" | while read id ciphertext; do
new_ct=$(vault write -field=ciphertext transit/rewrap/my-app-key \
ciphertext="$ciphertext")
psql -h db.example.com -U app -d mydb -c \
"UPDATE users SET encrypted_ssn='$new_ct' WHERE id=$id"
done
Minimum Decryption Version
After rewrapping all data, restrict decryption to only recent key versions:
# After rewrapping everything to v3, disallow decryption with v1 and v2
vault write transit/keys/my-app-key/config \
min_decryption_version=3
Now vault:v1:... and vault:v2:... ciphertexts cannot be decrypted — they must be rewrapped first.
# Also restrict encryption to latest version
vault write transit/keys/my-app-key/config \
min_encryption_version=3
Key Rotation Best Practices
| Practice | Reasoning |
|---|---|
| Rotate keys regularly (30-90 days) | Limits exposure from compromised keys |
| Use auto-rotation | Eliminates human error |
| Rewrap data after rotation | Ensure all data uses latest key version |
Set min_decryption_version after rewrap |
Prevent use of old key versions |
| Test rewrap in staging first | Verify no data loss |
Quick Reference
| Command | Description |
|---|---|
vault write -f transit/keys/KEY/rotate |
Rotate key |
vault write transit/keys/KEY/config auto_rotate_period=720h |
Auto-rotate |
vault write transit/rewrap/KEY ciphertext=CT |
Rewrap ciphertext |
vault write transit/keys/KEY/config min_decryption_version=N |
Set min version |