Files
stack/docker/openbao/init.sh
Jason Woltje d4d1e59885
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat(#357): Add OpenBao to Docker Compose with turnkey setup
Implements secure credential storage using OpenBao Transit encryption.

Features:
- Auto-initialization on first run (1-of-1 Shamir key for dev)
- Auto-unseal on container restart with verification and retry logic
- Transit secrets engine with 4 named encryption keys
- AppRole authentication with Transit-only policy
- Localhost-only API binding for security
- Comprehensive integration test suite (22 tests, all passing)

Security:
- API bound to 127.0.0.1 (localhost only, no external access)
- Unseal verification with 3-attempt retry logic
- Sanitized error messages in tests (no secret leakage)
- Volume-based secret reading (doesn't require running container)

Files:
- docker/openbao/config.hcl: Server configuration
- docker/openbao/init.sh: Auto-init/unseal script
- docker/docker-compose.yml: OpenBao and init services
- tests/integration/openbao.test.ts: Full test coverage
- .env.example: OpenBao configuration variables

Closes #357

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:40:24 -06:00

295 lines
8.8 KiB
Bash
Executable File

#!/bin/sh
set -e
# OpenBao Auto-Init Script
# Auto-initializes, unseals, and configures OpenBao on first run
# Idempotent - safe to run multiple times
INIT_DIR="/openbao/init"
UNSEAL_KEY_FILE="${INIT_DIR}/unseal-key"
ROOT_TOKEN_FILE="${INIT_DIR}/root-token"
APPROLE_CREDS_FILE="${INIT_DIR}/approle-credentials"
VAULT_ADDR="http://openbao:8200"
export VAULT_ADDR
# Ensure init directory exists
mkdir -p "${INIT_DIR}"
echo "=== OpenBao Auto-Init Script ==="
echo "Vault Address: ${VAULT_ADDR}"
# Wait for OpenBao to be ready
echo "Waiting for OpenBao API to be available..."
MAX_RETRIES=30
RETRY_COUNT=0
while ! wget -q -O- "${VAULT_ADDR}/v1/sys/init" >/dev/null 2>&1; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ ${RETRY_COUNT} -ge ${MAX_RETRIES} ]; then
echo "ERROR: OpenBao API not available after ${MAX_RETRIES} attempts"
exit 1
fi
echo "Waiting for OpenBao... (${RETRY_COUNT}/${MAX_RETRIES})"
sleep 2
done
echo "OpenBao API is available"
# Check initialization status
INIT_STATUS=$(wget -qO- "${VAULT_ADDR}/v1/sys/init" 2>/dev/null || echo '{"initialized":false}')
IS_INITIALIZED=$(echo "${INIT_STATUS}" | grep -o '"initialized":[^,}]*' | cut -d':' -f2)
if [ "${IS_INITIALIZED}" = "true" ]; then
echo "OpenBao is already initialized"
# Check if sealed
SEAL_STATUS=$(wget -qO- "${VAULT_ADDR}/v1/sys/seal-status" 2>/dev/null)
IS_SEALED=$(echo "${SEAL_STATUS}" | grep -o '"sealed":[^,}]*' | cut -d':' -f2)
if [ "${IS_SEALED}" = "true" ]; then
echo "OpenBao is sealed - unsealing..."
if [ ! -f "${UNSEAL_KEY_FILE}" ]; then
echo "ERROR: Unseal key not found at ${UNSEAL_KEY_FILE}"
exit 1
fi
UNSEAL_KEY=$(cat "${UNSEAL_KEY_FILE}")
# Unseal with retry logic
MAX_UNSEAL_RETRIES=3
UNSEAL_RETRY=0
UNSEAL_SUCCESS=false
while [ ${UNSEAL_RETRY} -lt ${MAX_UNSEAL_RETRIES} ]; do
UNSEAL_RESPONSE=$(wget -qO- --header="Content-Type: application/json" --post-data="{\"key\":\"${UNSEAL_KEY}\"}" "${VAULT_ADDR}/v1/sys/unseal" 2>&1)
# Verify unseal was successful by checking sealed status
sleep 1
VERIFY_STATUS=$(wget -qO- "${VAULT_ADDR}/v1/sys/seal-status" 2>/dev/null || echo '{"sealed":true}')
VERIFY_SEALED=$(echo "${VERIFY_STATUS}" | grep -o '"sealed":[^,}]*' | cut -d':' -f2)
if [ "${VERIFY_SEALED}" = "false" ]; then
UNSEAL_SUCCESS=true
echo "OpenBao unsealed successfully"
break
fi
UNSEAL_RETRY=$((UNSEAL_RETRY + 1))
echo "Unseal attempt ${UNSEAL_RETRY} failed, retrying..."
sleep 2
done
if [ "${UNSEAL_SUCCESS}" = "false" ]; then
echo "ERROR: Failed to unseal OpenBao after ${MAX_UNSEAL_RETRIES} attempts"
exit 1
fi
else
echo "OpenBao is already unsealed"
fi
# Verify Transit engine and AppRole are configured
if [ -f "${ROOT_TOKEN_FILE}" ]; then
export VAULT_TOKEN=$(cat "${ROOT_TOKEN_FILE}")
# Check Transit engine
if ! bao secrets list | grep -q "transit/"; then
echo "Transit engine not found - configuring..."
bao secrets enable transit
echo "Transit secrets engine enabled"
else
echo "Transit secrets engine already enabled"
fi
# Check AppRole
if ! bao auth list | grep -q "approle/"; then
echo "AppRole not found - configuring..."
bao auth enable approle
echo "AppRole auth method enabled"
else
echo "AppRole auth method already enabled"
fi
fi
echo "Initialization check complete - OpenBao is ready"
exit 0
fi
echo "OpenBao is not initialized - initializing with 1-of-1 key shares..."
# Initialize with 1 key share, threshold 1 (turnkey mode)
INIT_OUTPUT=$(bao operator init -key-shares=1 -key-threshold=1 -format=json)
# Extract unseal key and root token from JSON output
# First collapse multi-line JSON to single line, then parse
INIT_JSON=$(echo "${INIT_OUTPUT}" | tr -d '\n' | tr -d ' ')
UNSEAL_KEY=$(echo "${INIT_JSON}" | grep -o '"unseal_keys_b64":\["[^"]*"' | cut -d'"' -f4)
ROOT_TOKEN=$(echo "${INIT_JSON}" | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)
# Save to files
echo "${UNSEAL_KEY}" > "${UNSEAL_KEY_FILE}"
echo "${ROOT_TOKEN}" > "${ROOT_TOKEN_FILE}"
chmod 600 "${UNSEAL_KEY_FILE}" "${ROOT_TOKEN_FILE}"
echo "Initialization complete"
echo "Unseal key saved to ${UNSEAL_KEY_FILE}"
echo "Root token saved to ${ROOT_TOKEN_FILE}"
# Unseal with retry logic
echo "Unsealing OpenBao..."
MAX_UNSEAL_RETRIES=3
UNSEAL_RETRY=0
UNSEAL_SUCCESS=false
while [ ${UNSEAL_RETRY} -lt ${MAX_UNSEAL_RETRIES} ]; do
UNSEAL_RESPONSE=$(wget -qO- --header="Content-Type: application/json" --post-data="{\"key\":\"${UNSEAL_KEY}\"}" "${VAULT_ADDR}/v1/sys/unseal" 2>&1)
# Verify unseal was successful by checking sealed status
sleep 1
VERIFY_STATUS=$(wget -qO- "${VAULT_ADDR}/v1/sys/seal-status" 2>/dev/null || echo '{"sealed":true}')
VERIFY_SEALED=$(echo "${VERIFY_STATUS}" | grep -o '"sealed":[^,}]*' | cut -d':' -f2)
if [ "${VERIFY_SEALED}" = "false" ]; then
UNSEAL_SUCCESS=true
echo "OpenBao unsealed successfully"
break
fi
UNSEAL_RETRY=$((UNSEAL_RETRY + 1))
echo "Unseal attempt ${UNSEAL_RETRY} failed, retrying..."
sleep 2
done
if [ "${UNSEAL_SUCCESS}" = "false" ]; then
echo "ERROR: Failed to unseal OpenBao after ${MAX_UNSEAL_RETRIES} attempts"
exit 1
fi
# Configure with root token
export VAULT_TOKEN="${ROOT_TOKEN}"
# Enable Transit secrets engine
echo "Enabling Transit secrets engine..."
bao secrets enable transit
echo "Transit secrets engine enabled"
# Create Transit encryption keys
echo "Creating Transit encryption keys..."
bao write -f transit/keys/mosaic-credentials type=aes256-gcm96
echo "Created key: mosaic-credentials"
bao write -f transit/keys/mosaic-account-tokens type=aes256-gcm96
echo "Created key: mosaic-account-tokens"
bao write -f transit/keys/mosaic-federation type=aes256-gcm96
echo "Created key: mosaic-federation"
bao write -f transit/keys/mosaic-llm-config type=aes256-gcm96
echo "Created key: mosaic-llm-config"
echo "All Transit keys created"
# Enable AppRole auth method
echo "Enabling AppRole auth method..."
bao auth enable approle
echo "AppRole auth method enabled"
# Create Transit-only policy
echo "Creating Transit-only policy..."
cat > /tmp/transit-policy.hcl <<EOF
# Transit-only policy for Mosaic API
# Allows encrypt/decrypt operations only
path "transit/encrypt/*" {
capabilities = ["update"]
}
path "transit/decrypt/*" {
capabilities = ["update"]
}
# Deny all other paths
path "*" {
capabilities = ["deny"]
}
EOF
bao policy write mosaic-transit-policy /tmp/transit-policy.hcl
echo "Transit policy created"
# Create AppRole
echo "Creating AppRole: mosaic-transit..."
bao write auth/approle/role/mosaic-transit \
token_policies="mosaic-transit-policy" \
token_ttl=1h \
token_max_ttl=4h
echo "AppRole created"
# Get AppRole credentials
echo "Generating AppRole credentials..."
ROLE_ID_JSON=$(bao read -format=json auth/approle/role/mosaic-transit/role-id | tr -d '\n' | tr -d ' ')
ROLE_ID=$(echo "${ROLE_ID_JSON}" | grep -o '"role_id":"[^"]*"' | cut -d'"' -f4)
SECRET_ID_JSON=$(bao write -format=json -f auth/approle/role/mosaic-transit/secret-id | tr -d '\n' | tr -d ' ')
SECRET_ID=$(echo "${SECRET_ID_JSON}" | grep -o '"secret_id":"[^"]*"' | cut -d'"' -f4)
# Save credentials to file
cat > "${APPROLE_CREDS_FILE}" <<EOF
{
"role_id": "${ROLE_ID}",
"secret_id": "${SECRET_ID}"
}
EOF
chmod 600 "${APPROLE_CREDS_FILE}"
echo "AppRole credentials saved to ${APPROLE_CREDS_FILE}"
echo "=== OpenBao Configuration Complete ==="
echo "Status:"
bao status
echo ""
echo "Transit Keys:"
bao list transit/keys
echo ""
echo "Auth Methods:"
bao auth list
echo ""
echo "Initialization complete - OpenBao is ready for use"
# Watch loop to handle unsealing after container restarts
echo ""
echo "Starting unseal watch loop (checks every 30 seconds)..."
while true; do
sleep 30
# Check if OpenBao is sealed
SEAL_STATUS=$(wget -qO- "${VAULT_ADDR}/v1/sys/seal-status" 2>/dev/null || echo '{"sealed":false}')
IS_SEALED=$(echo "${SEAL_STATUS}" | grep -o '"sealed":[^,}]*' | cut -d':' -f2)
if [ "${IS_SEALED}" = "true" ]; then
echo "OpenBao is sealed - unsealing..."
if [ -f "${UNSEAL_KEY_FILE}" ]; then
UNSEAL_KEY=$(cat "${UNSEAL_KEY_FILE}")
# Try to unseal with verification
UNSEAL_RESPONSE=$(wget -qO- --header="Content-Type: application/json" --post-data="{\"key\":\"${UNSEAL_KEY}\"}" "${VAULT_ADDR}/v1/sys/unseal" 2>&1)
# Verify unseal was successful
sleep 1
VERIFY_STATUS=$(wget -qO- "${VAULT_ADDR}/v1/sys/seal-status" 2>/dev/null || echo '{"sealed":true}')
VERIFY_SEALED=$(echo "${VERIFY_STATUS}" | grep -o '"sealed":[^,}]*' | cut -d':' -f2)
if [ "${VERIFY_SEALED}" = "false" ]; then
echo "OpenBao unsealed successfully"
else
echo "WARNING: Unseal operation completed but OpenBao is still sealed"
fi
else
echo "WARNING: Unseal key not found, cannot auto-unseal"
fi
fi
done