HashiCorp Vault — Datakey & Envelope Encryption

3 minute read

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