HashiCorp Vault — Project: Encryption as a Service

3 minute read

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

Next: Production Vault Cluster Project