HashiCorp Vault — Key Rotation

2 minute read

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

Next: Datakey & Envelope Encryption