Complete documentation for OpenBao Transit encryption covering setup, architecture, production hardening, and operations. Sections: - Overview: Why OpenBao, Transit encryption explained - Architecture: Data flow diagrams, fallback behavior - Default Setup: Turnkey auto-init/unseal, file locations - Environment Variables: Configuration options - Transit Keys: Named keys, rotation procedures - Production Hardening: 10-point security checklist - Operations: Health checks, manual procedures, monitoring - Troubleshooting: Common issues and solutions - Disaster Recovery: Backup/restore procedures Key Topics: - Shamir key splitting upgrade (1-of-1 → 3-of-5) - TLS configuration for production - Audit logging enablement - HA storage backends (Raft/Consul) - External auto-unseal with KMS - Rate limiting via reverse proxy - Network isolation best practices - Key rotation procedures - Backup automation Closes #354 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
24 KiB
OpenBao Integration Guide
Version: 0.0.9 Status: Production Ready Related Issues: #346, #357, #353
Table of Contents
- Overview
- Architecture
- Default Turnkey Setup
- Environment Variables
- Transit Encryption Keys
- Production Hardening
- Operations
- Troubleshooting
- Disaster Recovery
Overview
Why OpenBao?
OpenBao is an open-source secrets management platform forked from HashiCorp Vault after HashiCorp changed to the Business Source License. Key benefits:
- Truly open-source - Linux Foundation project with OSI-approved license
- Drop-in Vault replacement - API-compatible with HashiCorp Vault
- Production-ready - v2.0+ with active development and community support
- Transit encryption - Encrypt data at rest without storing plaintext in OpenBao
What is Transit Encryption?
The Transit secrets engine provides "encryption as a service":
- Encryption/decryption operations via API calls
- Key versioning - Rotate keys without re-encrypting existing data
- No plaintext storage - OpenBao never stores your plaintext data
- Ciphertext format - Versioned format (
vault:v1:...) enables seamless key rotation
Use Case: Encrypt sensitive data (OAuth tokens, API keys, credentials) before storing in PostgreSQL. If the database is compromised, attackers only get encrypted ciphertext.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Mosaic Stack API │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ VaultService │───────>│ CryptoService │ │
│ │ (Primary) │ │ (Fallback) │ │
│ └────────┬───────┘ └──────────────────┘ │
│ │ │
│ │ Transit API │
│ │ (encrypt/decrypt) │
└───────────┼─────────────────────────────────────────────────┘
│
│ HTTP (localhost:8200)
▼
┌───────────────────────────────────────────────────────────────┐
│ OpenBao │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Transit Secrets Engine │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ mosaic- │ │ mosaic- │ + 2 more │ │
│ │ │ credentials │ │ account- │ keys │ │
│ │ │ │ │ tokens │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ AppRole Authentication │ │
│ │ - Role: mosaic-transit │ │
│ │ - Policy: Transit encrypt/decrypt only │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ File Storage Backend │ │
│ │ /openbao/data (Docker volume) │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
│
│ Auto-init / Auto-unseal
▼
┌───────────────────────────────────────────────────────────────┐
│ OpenBao Init Sidecar │
│ - Initializes OpenBao on first run │
│ - Auto-unseals on container restart │
│ - Creates Transit keys and AppRole │
│ - Runs continuously with 30s check loop │
└───────────────────────────────────────────────────────────────┘
Data Flow: Encrypt
- API receives plaintext credential (e.g., OAuth access token)
- VaultService encrypts via Transit:
POST /v1/transit/encrypt/mosaic-account-tokens - OpenBao returns ciphertext:
vault:v1:8SDd3WHDOjf8Fz5MSLXjL... - Ciphertext stored in PostgreSQL
- Plaintext never touches disk
Data Flow: Decrypt
- API reads ciphertext from PostgreSQL
- VaultService decrypts via Transit:
POST /v1/transit/decrypt/mosaic-account-tokens - OpenBao returns plaintext
- API uses plaintext for OAuth/API requests
- Plaintext cleared from memory after use
Fallback Behavior
When OpenBao is unavailable:
- Encryption: VaultService falls back to AES-256-GCM (
CryptoService) - Decryption: Auto-detects format (
vault:v1:vs AES) and uses appropriate service - Logging: ERROR-level logs for visibility into infrastructure issues
- Backward Compatibility: Existing AES-encrypted data remains decryptable
Production Recommendation: Set OPENBAO_REQUIRED=true to fail startup if OpenBao is unavailable (prevents accidental fallback).
Default Turnkey Setup
What Happens on First docker compose up
The openbao-init sidecar automatically:
- Waits for OpenBao server to be healthy
- Initializes OpenBao with 1-of-1 Shamir key split (development mode)
- Unseals OpenBao using the stored unseal key
- Enables Transit secrets engine at
/transit - Creates 4 named encryption keys:
mosaic-credentials- User-provided credentials (API keys, tokens)mosaic-account-tokens- OAuth tokens from BetterAuth accountsmosaic-federation- Federation private keysmosaic-llm-config- LLM provider API keys
- Creates AppRole
mosaic-transitwith Transit-only policy - Generates AppRole credentials (role_id, secret_id)
- Saves credentials to
/openbao/init/approle-credentials - Watches continuously - auto-unseals on container restart every 30s
File Locations
Docker Volumes:
├── mosaic-openbao-data # OpenBao database (encrypted storage)
├── mosaic-openbao-init # Unseal key, root token, AppRole credentials
└── docker/openbao/config.hcl # Server configuration (bind mount)
Credentials Storage
Unseal Key: /openbao/init/unseal-key (plaintext, volume-only access)
Root Token: /openbao/init/root-token (plaintext, volume-only access)
AppRole Credentials: /openbao/init/approle-credentials (JSON, read by API)
Example /openbao/init/approle-credentials:
{
"role_id": "42474304-cb07-edff-f5c8-cae494ba6a51",
"secret_id": "39ccdb6b-4570-a279-022c-36220739ebcf"
}
Security Note: These files are only accessible within the Docker internal network. The OpenBao API is bound to 127.0.0.1:8200 (localhost only).
Environment Variables
Required
None - the turnkey setup works with defaults.
Optional
| Variable | Default | Purpose |
|---|---|---|
OPENBAO_ADDR |
http://openbao:8200 |
OpenBao API address (Docker internal) |
OPENBAO_PORT |
8200 |
Port for localhost binding in docker-compose |
OPENBAO_ROLE_ID |
(read from file) | Override AppRole role_id |
OPENBAO_SECRET_ID |
(read from file) | Override AppRole secret_id |
OPENBAO_REQUIRED |
false |
Fail startup if OpenBao unavailable |
Production Configuration
Development:
OPENBAO_ADDR=http://openbao:8200
OPENBAO_REQUIRED=false
Production:
OPENBAO_ADDR=https://vault.internal.corp:8200
OPENBAO_REQUIRED=true
# AppRole credentials provided by external Vault deployment
OPENBAO_ROLE_ID=<from-external-vault>
OPENBAO_SECRET_ID=<from-external-vault>
Transit Encryption Keys
Named Keys
| Key Name | Purpose | Used By | Example Data |
|---|---|---|---|
mosaic-credentials |
User-provided credentials | UserCredential model | GitHub PAT, AWS access keys |
mosaic-account-tokens |
OAuth tokens | BetterAuth accounts table | access_token, refresh_token, id_token |
mosaic-federation |
Federation private keys | FederatedInstance model | Ed25519 private keys |
mosaic-llm-config |
LLM provider credentials | LLMProviderInstance model | OpenAI API key, Anthropic API key |
Key Properties
- Algorithm: AES-256-GCM96
- Versioning: Enabled (transparent key rotation)
- Deletion: Not allowed (exportable = false)
- Min Decryption Version: 1 (all versions can decrypt)
- Latest Version: Auto-increments on rotation
Key Rotation
OpenBao Transit keys support transparent rotation:
- Rotate key:
bao write -f transit/keys/mosaic-credentials/rotate - New version created (e.g., v2)
- New encryptions use v2
- Old ciphertexts still decrypt with v1
- No data re-encryption needed
Recommendation: Rotate keys annually or after suspected compromise.
Ciphertext Format
vault:v1:8SDd3WHDOjf8Fz5MSLXjLx0AzTdYhLMAAp4W
^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
│ └─ Base64-encoded ciphertext
└───── Key version
Version prefix enables seamless rotation - OpenBao knows which key version to use for decryption.
Production Hardening
Security Checklist
CRITICAL - Do not deploy to production without these:
| Task | Command | Priority |
|---|---|---|
| 1. Upgrade Shamir key splitting | bao operator rekey -key-shares=5 -key-threshold=3 |
P0 |
| 2. Enable TLS on listener | Update config.hcl: tls_disable = 0 |
P0 |
| 3. Revoke root token | bao token revoke -self |
P0 |
| 4. Enable audit logging | bao audit enable file file_path=/bao/logs/audit.log |
P0 |
| 5. Use external auto-unseal | AWS KMS, GCP CKMS, Azure Key Vault | P1 |
| 6. HA storage backend | Raft or Consul | P1 |
| 7. Bind to internal network only | Update docker-compose.yml: remove port exposure | P1 |
| 8. Rate limiting | Use reverse proxy (nginx/Traefik) with rate limits | P2 |
| 9. Rotate AppRole credentials | Regenerate secret_id monthly | P2 |
| 10. Backup automation | Snapshot Raft or file storage daily | P2 |
1. Shamir Key Splitting
Current (Development): 1-of-1 key shares (single unseal key) Production: 3-of-5 key shares (requires 3 of 5 keys to unseal)
Procedure:
# SSH into OpenBao container
docker compose exec openbao sh
# Set environment
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=$(cat /openbao/init/root-token)
# Rekey to 3-of-5
bao operator rekey -init -key-shares=5 -key-threshold=3
# Follow prompts to generate new keys
# Distribute 5 keys to different key holders
# Require 3 keys to unseal
Key Management Best Practices:
- Store each key with a different person/system
- Use hardware security modules (HSMs) for key storage
- Document key recovery procedures
- Test unseal process regularly
2. TLS Configuration
Update docker/openbao/config.hcl:
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0 # Changed from 1
tls_cert_file = "/openbao/config/server.crt"
tls_key_file = "/openbao/config/server.key"
tls_min_version = "tls12"
}
Generate TLS certificates:
# Production: Use certificates from your CA
# Development: Generate self-signed
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
-days 365 -nodes -subj "/CN=openbao.internal"
Update API configuration:
OPENBAO_ADDR=https://openbao.internal:8200
3. Revoke Root Token
After initial setup, the root token should be revoked:
export VAULT_TOKEN=$(cat /openbao/init/root-token)
bao token revoke -self
# Remove root token file
rm /openbao/init/root-token
Recovery: Use the Shamir keys to generate a new root token if needed:
bao operator generate-root -init
4. Audit Logging
Enable audit logging to track all API activity:
bao audit enable file file_path=/openbao/logs/audit.log
# Verify
bao audit list
Mount logs volume in docker-compose.yml:
openbao:
volumes:
- openbao_logs:/openbao/logs
Log Format: JSON, one entry per API request Retention: Rotate daily, retain 90 days minimum
5. External Auto-Unseal
Replace file-based unseal with KMS-based auto-unseal:
AWS KMS Example:
# config.hcl
seal "awskms" {
region = "us-east-1"
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abc123"
}
Benefits:
- No manual unseal required
- Unseal key never touches disk
- Audit trail in KMS service
- Automatic rotation support
6. High Availability
File Storage (Current) - Single node, no HA Raft Storage (Recommended) - Multi-node with leader election
Raft configuration:
storage "raft" {
path = "/openbao/data"
node_id = "node1"
}
Run 3-5 OpenBao nodes with Raft for production HA.
7. Network Isolation
Current: Port bound to 127.0.0.1:8200 (localhost only)
Production: Use internal network only, no public exposure
docker-compose.yml:
openbao:
# Remove port mapping
# ports:
# - "127.0.0.1:8200:8200"
networks:
- internal
API access: Use reverse proxy with authentication/authorization.
8. Rate Limiting
Prevent brute-force attacks on AppRole authentication:
nginx reverse proxy:
http {
limit_req_zone $binary_remote_addr zone=vault_auth:10m rate=10r/s;
server {
listen 8200 ssl;
location /v1/auth/approle/login {
limit_req zone=vault_auth burst=20 nodelay;
proxy_pass http://openbao:8200;
}
location / {
proxy_pass http://openbao:8200;
}
}
}
Operations
Health Checks
OpenBao server:
curl http://localhost:8200/v1/sys/health
Response:
{
"initialized": true,
"sealed": false,
"standby": false,
"server_time_utc": 1738938524
}
VaultService health (API endpoint):
curl http://localhost:3000/health
Response:
{
"status": "ok",
"info": {
"openbao": {
"status": "up"
}
}
}
Manual Unseal
If auto-unseal fails:
# Get unseal key
docker run --rm -v mosaic-openbao-init:/data alpine cat /data/unseal-key
# Unseal OpenBao
docker compose exec openbao bao operator unseal <key>
Check Transit Keys
# Get root token
export VAULT_TOKEN=$(docker run --rm -v mosaic-openbao-init:/data alpine cat /data/root-token)
# List Transit keys
docker compose exec -e VAULT_TOKEN openbao bao list transit/keys
# Read key details
docker compose exec -e VAULT_TOKEN openbao bao read transit/keys/mosaic-credentials
Test Encryption
# Encrypt test data
PLAINTEXT=$(echo -n "test-secret" | base64)
docker compose exec -e VAULT_TOKEN openbao bao write -format=json \
transit/encrypt/mosaic-credentials plaintext=$PLAINTEXT
# Decrypt
docker compose exec -e VAULT_TOKEN openbao bao write -format=json \
transit/decrypt/mosaic-credentials ciphertext=<ciphertext>
Monitor Logs
# OpenBao server logs
docker compose logs -f openbao
# Init sidecar logs
docker compose logs -f openbao-init
# VaultService logs (API)
docker compose logs -f api | grep VaultService
Troubleshooting
OpenBao Won't Start
Symptoms: Container restarts repeatedly, "connection refused" errors
Diagnosis:
docker compose logs openbao
Common Causes:
- Port conflict: Another service on 8200
- Volume permission issues: Can't write to
/openbao/data - Invalid config: Syntax error in
config.hcl
Solutions:
# Check port usage
netstat -tuln | grep 8200
# Fix volume permissions
docker compose down -v
docker volume rm mosaic-openbao-data
docker compose up -d
# Validate config
docker compose exec openbao bao server -config=/openbao/config/config.hcl -test
OpenBao Sealed
Symptoms: API returns 503, "sealed": true in health check
Diagnosis:
docker compose exec openbao bao status
Solution:
# Auto-unseal should handle this
# Force restart init container
docker compose restart openbao-init
# Manual unseal if needed
docker compose exec openbao bao operator unseal <key>
VaultService Falls Back to AES
Symptoms: Logs show "OpenBao unavailable, falling back to AES-256-GCM"
Diagnosis:
# Check OpenBao health
curl http://localhost:8200/v1/sys/health
# Check AppRole credentials
docker run --rm -v mosaic-openbao-init:/data alpine cat /data/approle-credentials
Common Causes:
- OpenBao not running:
docker compose ps openbao - Wrong OPENBAO_ADDR: Check environment variable
- AppRole credentials missing: Reinitialize with
docker compose restart openbao-init - Network issue: Check Docker network connectivity
Solutions:
# Restart OpenBao stack
docker compose restart openbao openbao-init
# Verify connectivity from API container
docker compose exec api curl http://openbao:8200/v1/sys/health
Authentication Failures
Symptoms: "Token renewal failed", "Authentication failed"
Diagnosis:
# Check AppRole
export VAULT_TOKEN=$(docker run --rm -v mosaic-openbao-init:/data alpine cat /data/root-token)
docker compose exec -e VAULT_TOKEN openbao bao read auth/approle/role/mosaic-transit
Solutions:
# Regenerate AppRole credentials
docker compose exec -e VAULT_TOKEN openbao bao write -format=json \
-f auth/approle/role/mosaic-transit/secret-id > new-credentials.json
# Update credentials file
docker run --rm -v mosaic-openbao-init:/data -v $(pwd):/host alpine \
cp /host/new-credentials.json /data/approle-credentials
# Restart API
docker compose restart api
Decrypt Failures
Symptoms: "Failed to decrypt data", encrypted tokens visible in database
Diagnosis:
# Check ciphertext format
psql -h localhost -U mosaic -d mosaic \
-c "SELECT access_token FROM accounts LIMIT 1;"
Ciphertext formats:
vault:v1:...- Transit encryption (requires OpenBao)iv:tag:encrypted- AES fallback (works without OpenBao)
Solutions:
- Transit ciphertext + OpenBao unavailable: Start OpenBao
- Corrupted ciphertext: Check database integrity
- Wrong encryption key: Verify
ENCRYPTION_KEYhasn't changed (for AES)
Disaster Recovery
Backup
Critical Data:
- Unseal key:
/openbao/init/unseal-key(Shamir keys in production) - Root token:
/openbao/init/root-token(revoke in production, document recovery) - AppRole credentials:
/openbao/init/approle-credentials - OpenBao data:
/openbao/data(encrypted storage)
Backup Procedure:
# Backup volumes
docker run --rm -v mosaic-openbao-init:/data -v $(pwd):/backup \
alpine tar czf /backup/openbao-init-$(date +%Y%m%d).tar.gz /data
docker run --rm -v mosaic-openbao-data:/data -v $(pwd):/backup \
alpine tar czf /backup/openbao-data-$(date +%Y%m%d).tar.gz /data
# Backup to remote storage
aws s3 cp openbao-*.tar.gz s3://backups/openbao/
Schedule: Daily automated backups with 30-day retention.
Restore
Full Recovery:
# Download backups
aws s3 cp s3://backups/openbao/openbao-init-20260207.tar.gz .
aws s3 cp s3://backups/openbao/openbao-data-20260207.tar.gz .
# Stop containers
docker compose down
# Restore volumes
docker volume create mosaic-openbao-init
docker volume create mosaic-openbao-data
docker run --rm -v mosaic-openbao-init:/data -v $(pwd):/backup \
alpine tar xzf /backup/openbao-init-20260207.tar.gz -C /data --strip-components=1
docker run --rm -v mosaic-openbao-data:/data -v $(pwd):/backup \
alpine tar xzf /backup/openbao-data-20260207.tar.gz -C /data --strip-components=1
# Restart
docker compose up -d
Key Recovery
Lost Unseal Key:
- Development (1-of-1): Restore from backup
- Production (3-of-5): Collect 3 of 5 Shamir keys from key holders
Lost Root Token:
# Generate new root token using Shamir keys
bao operator generate-root -init
bao operator generate-root -nonce=<nonce> <key1>
bao operator generate-root -nonce=<nonce> <key2>
bao operator generate-root -nonce=<nonce> <key3>
Lost AppRole Credentials:
# Use root token to regenerate
export VAULT_TOKEN=<root-token>
bao write -format=json -f auth/approle/role/mosaic-transit/secret-id \
> new-approle-credentials.json
Testing Recovery
Quarterly DR test:
- Backup production OpenBao
- Restore to test environment
- Verify all Transit keys accessible
- Test encrypt/decrypt operations
- Document any issues
Additional Resources
- OpenBao Documentation: https://openbao.org/docs/
- Transit Secrets Engine: https://openbao.org/docs/secrets/transit/
- AppRole Auth: https://openbao.org/docs/auth/approle/
- Production Hardening: https://openbao.org/docs/guides/production/
- Design Document:
docs/design/credential-security.md
Support
For issues with Mosaic Stack's OpenBao integration:
- Repository: https://git.mosaicstack.dev/mosaic/stack
- Issues: https://git.mosaicstack.dev/mosaic/stack/issues
- Milestone: M9-CredentialSecurity
For OpenBao itself:
- GitHub: https://github.com/openbao/openbao
- Discussions: https://github.com/openbao/openbao/discussions