HashiCorp Vault — Datakey & Envelope Encryption
For encrypting large data (files, database dumps, disk volumes), sending everything to Vault is impractical. Envelope encryption solves this: Vault generates a one-time data key, the application encrypts locally, then Vault manages the key.
Why Envelope Encryption?
Direct Encryption (bad for large data):
App → sends 10GB file → Vault → returns 10GB ciphertext
⚠️ Network overhead, Vault becomes bottleneck
Envelope Encryption (efficient):
App → requests data key → Vault returns key (32 bytes)
App → encrypts 10GB locally with data key → stores ciphertext
App → stores encrypted data key alongside ciphertext
✅ Vault only handles 32 bytes, app handles bulk crypto
How Envelope Encryption Works
┌──────────┐ ① Generate datakey ┌──────────────┐
│ App │───────────────────────────▶│ Vault │
│ │ │ Transit │
│ │ ② plaintext key + │ │
│ │ encrypted key │ │
│ │◀───────────────────────────│ │
│ │ └──────────────┘
│ │
│ ③ Encrypt data locally with plaintext key
│ ④ Discard plaintext key from memory
│ ⑤ Store: encrypted data + encrypted key
│
│ To decrypt later:
│ ⑥ Send encrypted key to Vault
│ ⑦ Vault returns plaintext key
│ ⑧ Decrypt data locally
└──────────┘
Generate a Datakey
# Generate a datakey (returns both plaintext and encrypted versions)
vault write transit/datakey/plaintext/my-app-key
Expected output:
Key Value
--- -----
ciphertext vault:v1:abc123...encrypted-key...
key_version 1
plaintext dGhpcyBpcyBhIDMyIGJ5dGUga2V5Li4u
| Field | Description |
|---|---|
plaintext |
Base64-encoded data encryption key (use this to encrypt your data) |
ciphertext |
Vault-encrypted version of the key (store this alongside your data) |
Wrapped-Only Datakey
If you don’t need the plaintext immediately:
# Only returns the encrypted key (no plaintext)
vault write transit/datakey/wrapped/my-app-key
Decrypt the Datakey
When you need to decrypt data, send the encrypted key back to Vault:
vault write transit/decrypt/my-app-key \
ciphertext="vault:v1:abc123...encrypted-key..."
This returns the plaintext data key — use it to decrypt your data locally.
Complete Example — File Encryption
#!/bin/bash
# envelope-encrypt.sh — Encrypt a file using envelope encryption
FILE="$1"
# 1. Get a datakey from Vault
RESPONSE=$(vault write -format=json transit/datakey/plaintext/my-app-key)
PLAINTEXT_KEY=$(echo "$RESPONSE" | jq -r .data.plaintext)
ENCRYPTED_KEY=$(echo "$RESPONSE" | jq -r .data.ciphertext)
# 2. Decode the base64 key to raw bytes
echo "$PLAINTEXT_KEY" | base64 --decode > /tmp/datakey.bin
# 3. Encrypt the file with OpenSSL using the datakey
openssl enc -aes-256-cbc -pbkdf2 -in "$FILE" -out "${FILE}.enc" \
-pass file:/tmp/datakey.bin
# 4. Store the encrypted key alongside the encrypted file
echo "$ENCRYPTED_KEY" > "${FILE}.key"
# 5. Securely delete the plaintext key
rm -f /tmp/datakey.bin
echo "Encrypted: ${FILE}.enc"
echo "Key file: ${FILE}.key"
#!/bin/bash
# envelope-decrypt.sh — Decrypt a file using envelope encryption
FILE="$1" # e.g., myfile.txt (will look for myfile.txt.enc and myfile.txt.key)
# 1. Read the encrypted key
ENCRYPTED_KEY=$(cat "${FILE}.key")
# 2. Decrypt the key via Vault
PLAINTEXT_KEY=$(vault write -field=plaintext transit/decrypt/my-app-key \
ciphertext="$ENCRYPTED_KEY")
# 3. Decode the base64 key
echo "$PLAINTEXT_KEY" | base64 --decode > /tmp/datakey.bin
# 4. Decrypt the file
openssl enc -aes-256-cbc -pbkdf2 -d -in "${FILE}.enc" -out "$FILE" \
-pass file:/tmp/datakey.bin
# 5. Cleanup
rm -f /tmp/datakey.bin
echo "Decrypted: $FILE"
Application Example — Python
import hvac
import base64
from cryptography.fernet import Fernet
client = hvac.Client(url='http://127.0.0.1:8200', token='my-token')
# Generate datakey
result = client.secrets.transit.generate_data_key(
name='my-app-key',
key_type='plaintext'
)
plaintext_key = base64.b64decode(result['data']['plaintext'])
encrypted_key = result['data']['ciphertext']
# Use the plaintext key to encrypt data locally
# (Using Fernet for simplicity — use AES-GCM in production)
fernet_key = base64.urlsafe_b64encode(plaintext_key[:32])
f = Fernet(fernet_key)
ciphertext = f.encrypt(b"large amount of sensitive data...")
# Store ciphertext + encrypted_key in database
# Discard plaintext_key from memory
del plaintext_key
# Later: decrypt the datakey via Vault
result = client.secrets.transit.decrypt_data(
name='my-app-key',
ciphertext=encrypted_key
)
recovered_key = base64.b64decode(result['data']['plaintext'])
# Decrypt data locally
fernet_key = base64.urlsafe_b64encode(recovered_key[:32])
f = Fernet(fernet_key)
plaintext = f.decrypt(ciphertext)
When to Use What
| Method | Use When |
|---|---|
| Direct Transit encrypt | Small data (<1MB): passwords, tokens, API keys |
| Envelope encryption (datakey) | Large data: files, database backups, media |
| Convergent encryption | Need to search encrypted data |
Quick Reference
| Command | Description |
|---|---|
vault write transit/datakey/plaintext/KEY |
Generate datakey (plaintext + encrypted) |
vault write transit/datakey/wrapped/KEY |
Generate wrapped-only datakey |
vault write transit/decrypt/KEY ciphertext=CT |
Decrypt datakey |
Next: Sign & Verify