HashiCorp Vault — Project: Encryption as a Service
Build an Encryption as a Service platform using Vault’s Transit engine — encrypt application data, rotate keys, and implement envelope encryption.
Project Overview
┌──────────┐ Encrypt API ┌───────┐
│ Web App │─────────────────►│ Vault │
│ │◄─────────────────│Transit │
│ (stores │ Ciphertext │ │
│ encrypted│ │ │
│ data) │ Decrypt API │ │
│ │─────────────────►│ │
└──────────┘ └───────┘
│
▼
┌──────────┐
│ Database │ (stores only ciphertext)
└──────────┘
Step 1 — Enable and Configure Transit
# Enable Transit secrets engine
vault secrets enable transit
# Create encryption keys for different data types
vault write -f transit/keys/customer-data
vault write -f transit/keys/payment-info type=aes256-gcm96
vault write -f transit/keys/pii convergent_encryption=true derived=true
# Create a signing key
vault write -f transit/keys/api-signing type=ecdsa-p256
Step 2 — Create Access Policies
# encrypt-only.hcl — microservice that only encrypts
path "transit/encrypt/customer-data" {
capabilities = ["update"]
}
# decrypt-only.hcl — microservice that only decrypts
path "transit/decrypt/customer-data" {
capabilities = ["update"]
}
# full-access.hcl — backend service
path "transit/encrypt/*" {
capabilities = ["update"]
}
path "transit/decrypt/*" {
capabilities = ["update"]
}
path "transit/datakey/*" {
capabilities = ["update"]
}
vault policy write encrypt-only encrypt-only.hcl
vault policy write decrypt-only decrypt-only.hcl
vault policy write transit-full full-access.hcl
Step 3 — Encrypt and Decrypt Data
# Encrypt customer data
vault write transit/encrypt/customer-data \
plaintext=$(echo -n "John Doe, SSN: 123-45-6789" | base64)
# Returns: ciphertext: vault:v1:abc...
# Decrypt
vault write transit/decrypt/customer-data \
ciphertext="vault:v1:abc..."
# Returns: plaintext (base64 encoded)
# Decode the base64
echo "base64-result" | base64 -d
# Returns: John Doe, SSN: 123-45-6789
Batch Encryption
# Encrypt multiple items at once
vault write transit/encrypt/customer-data \
batch_input='[
{"plaintext": "'$(echo -n "record-1" | base64)'"},
{"plaintext": "'$(echo -n "record-2" | base64)'"},
{"plaintext": "'$(echo -n "record-3" | base64)'"}
]'
Step 4 — Implement Envelope Encryption
For encrypting large files or blobs:
# Generate a data key (DEK)
vault write -f transit/datakey/plaintext/customer-data
# Returns:
# plaintext: base64-encoded-DEK (use this to encrypt locally)
# ciphertext: vault:v1:wrapped-DEK (store this alongside encrypted data)
# Your application:
# 1. Uses the plaintext DEK to encrypt data locally (AES)
# 2. Stores the ciphertext (wrapped DEK) alongside the encrypted data
# 3. Discards the plaintext DEK from memory
# To decrypt later:
# 1. Retrieve the wrapped DEK from storage
# 2. Ask Vault to unwrap it
vault write transit/decrypt/customer-data \
ciphertext="vault:v1:wrapped-DEK"
# 3. Use the plaintext DEK to decrypt data locally
Python Envelope Encryption Example
import hvac
import base64
from cryptography.fernet import Fernet
client = hvac.Client(url='http://127.0.0.1:8200', token='my-token')
# Generate a data key
response = client.secrets.transit.generate_data_key(
name='customer-data',
key_type='plaintext'
)
plaintext_key = base64.b64decode(response['data']['plaintext'])
wrapped_key = response['data']['ciphertext']
# Encrypt data locally with the DEK
fernet_key = base64.urlsafe_b64encode(plaintext_key[:32])
f = Fernet(fernet_key)
encrypted_data = f.encrypt(b"Sensitive customer record...")
# Store encrypted_data + wrapped_key in database
# Discard plaintext_key from memory
del plaintext_key
# --- Later, to decrypt ---
# Unwrap the DEK via Vault
response = client.secrets.transit.decrypt_data(
name='customer-data',
ciphertext=wrapped_key
)
plaintext_key = base64.b64decode(response['data']['plaintext'])
fernet_key = base64.urlsafe_b64encode(plaintext_key[:32])
f = Fernet(fernet_key)
original_data = f.decrypt(encrypted_data)
Step 5 — Key Rotation
# Rotate the encryption key
vault write -f transit/keys/customer-data/rotate
# Check key versions
vault read transit/keys/customer-data
# Shows: latest_version = 2
# New encryptions use key v2
# Old ciphertexts (v1) can still be decrypted
# Rewrap old ciphertexts to latest key version
vault write transit/rewrap/customer-data \
ciphertext="vault:v1:old-ciphertext..."
# Returns: vault:v2:new-ciphertext...
# After rewrapping all data, set minimum version
vault write transit/keys/customer-data \
min_decryption_version=2 \
min_encryption_version=2
# Old key versions are now unusable
Step 6 — Digital Signatures
# Sign data
vault write transit/sign/api-signing \
input=$(echo -n "important-api-payload" | base64)
# Returns: signature: vault:v1:MEUCIQDx...
# Verify signature
vault write transit/verify/api-signing \
input=$(echo -n "important-api-payload" | base64) \
signature="vault:v1:MEUCIQDx..."
# Returns: valid: true
Complete Architecture
| Component | Key | Purpose |
|---|---|---|
| Customer PII | customer-data |
Encrypt/decrypt personal info |
| Payment data | payment-info |
Credit card encryption |
| Searchable PII | pii (convergent) |
Same input → same ciphertext |
| API integrity | api-signing (ECDSA) |
Sign and verify payloads |