Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
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>
295 lines
8.8 KiB
Bash
Executable File
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
|