#!/usr/bin/env bash # MuseHub secrets bootstrap — fetch from AWS SSM Parameter Store, write .env # # Runs on the EC2 instance BEFORE deploy.sh. Pulls every secret from SSM # Parameter Store (SecureString, AES-256 at rest via KMS) and writes a fresh # /opt/musehub/.env. The .env on disk is the runtime injection point for # all Docker containers (--env-file). # # Why SSM instead of a static .env: # - Secrets never travel through source control or build artifacts. # - Access is audited via CloudTrail (who fetched what, when). # - Rotation updates SSM; next deploy.sh run picks up the new value. # - IAM role on the EC2 instance grants read access — no AWS keys on disk. # # SSM parameter layout (all SecureString, KMS-encrypted): # /musehub//DB_PASSWORD # /musehub//WEBHOOK_SECRET_KEY # /musehub//RUNNER_TOKEN # /musehub//BLOB_STORAGE_ACCESS_KEY_ID # /musehub//BLOB_STORAGE_SECRET_ACCESS_KEY # /musehub//WORKER_INTERNAL_KEY (shared secret for Cloudflare Worker → MuseHub callbacks) # /musehub//MPACK_WORKER_URL (public URL of the CF mpack-receiver Worker; optional) # # Prerequisites: # - AWS CLI v2 installed on the EC2 instance # - EC2 instance profile with IAM policy: # ssm:GetParameter, ssm:GetParametersByPath # on arn:aws:ssm:::parameter/musehub//* # - KMS decrypt on the CMK used for the SecureString parameters # # Usage: # MUSEHUB_ENV=production bash deploy/secrets.sh # MUSEHUB_ENV=staging bash deploy/secrets.sh # # After this script writes .env, run deploy.sh as usual. # # Fallback (no SSM / local dev): # If AWS CLI is not available or SSM fetch fails, the script exits non-zero # so deploy.sh does not start with stale/missing secrets. For local dev, # manage .env manually — never run this script on a dev laptop. set -euo pipefail MUSEHUB_ENV="${MUSEHUB_ENV:-production}" APP_DIR="${APP_DIR:-/opt/musehub}" ENV_FILE="$APP_DIR/.env" REGION="${AWS_REGION:-us-east-1}" SSM_PREFIX="/musehub/${MUSEHUB_ENV}" log() { echo "[secrets] $*"; } die() { echo "[secrets] ERROR: $*" >&2; exit 1; } # ── Preflight ───────────────────────────────────────────────────────────────── command -v aws > /dev/null 2>&1 || die "AWS CLI not installed. Install: sudo apt-get install -y awscli" # Verify we can reach SSM (IAM role check) — use GetParameter on DB_PASSWORD # (always required) rather than GetParametersByPath (requires broader permission). aws ssm get-parameter \ --name "$SSM_PREFIX/DB_PASSWORD" \ --region "$REGION" \ --with-decryption \ --query 'Parameter.Value' \ --output text > /dev/null 2>&1 \ || die "Cannot read $SSM_PREFIX/DB_PASSWORD from SSM — check the EC2 instance IAM role." log "Fetching secrets from SSM: $SSM_PREFIX (region=$REGION)" # ── Fetch each parameter ────────────────────────────────────────────────────── _get() { local name="$1" local required="${2:-true}" local value value=$(aws ssm get-parameter \ --name "$SSM_PREFIX/$name" \ --region "$REGION" \ --with-decryption \ --query 'Parameter.Value' \ --output text 2>/dev/null) || { if [ "$required" = "true" ]; then die "Required parameter $SSM_PREFIX/$name not found in SSM" fi echo "" return } echo "$value" } DB_PASSWORD=$(_get "DB_PASSWORD") WEBHOOK_SECRET_KEY=$(_get "WEBHOOK_SECRET_KEY") RUNNER_TOKEN=$(_get "RUNNER_TOKEN" false) BLOB_STORAGE_ACCESS_KEY_ID=$(_get "BLOB_STORAGE_ACCESS_KEY_ID" false) BLOB_STORAGE_SECRET_ACCESS_KEY=$(_get "BLOB_STORAGE_SECRET_ACCESS_KEY" false) WORKER_INTERNAL_KEY=$(_get "WORKER_INTERNAL_KEY" false) PACK_WORKER_URL=$(_get "PACK_WORKER_URL" false) # ── Resolve per-environment non-secret config ───────────────────────────────── if [ "$MUSEHUB_ENV" = "staging" ]; then PUBLIC_URL="https://staging.musehub.ai" CORS_ORIGINS='["https://staging.musehub.ai"]' BLOB_STORAGE_BUCKET="musehub-staging" BLOB_STORAGE_ENDPOINT="https://bed873d46de5273abf843468a7833f09.r2.cloudflarestorage.com" BLOB_STORAGE_REGION="auto" elif [ "$MUSEHUB_ENV" = "production" ]; then PUBLIC_URL="https://musehub.ai" CORS_ORIGINS='["https://musehub.ai", "https://www.musehub.ai"]' BLOB_STORAGE_BUCKET="musehub-prod" BLOB_STORAGE_ENDPOINT="https://bed873d46de5273abf843468a7833f09.r2.cloudflarestorage.com" BLOB_STORAGE_REGION="auto" else die "Unknown MUSEHUB_ENV='$MUSEHUB_ENV'. Must be 'staging' or 'production'." fi # ── Write .env ──────────────────────────────────────────────────────────────── log "Writing $ENV_FILE (env=$MUSEHUB_ENV, public_url=$PUBLIC_URL)" # Back up the existing .env if present if [ -f "$ENV_FILE" ]; then cp "$ENV_FILE" "${ENV_FILE}.bak.$(date +%Y%m%d_%H%M%S)" log "Previous .env backed up" fi # Write new .env — mode 600, owner musehub umask 177 cat > "$ENV_FILE" << EOF # Generated by deploy/secrets.sh at $(date -u +%Y-%m-%dT%H:%M:%SZ) # Secrets sourced from AWS SSM Parameter Store: $SSM_PREFIX # DO NOT edit manually — re-run secrets.sh to refresh from SSM. MUSE_ENV=${MUSEHUB_ENV} DEBUG=false PUBLIC_URL=${PUBLIC_URL} CORS_ORIGINS=${CORS_ORIGINS} DB_PASSWORD=${DB_PASSWORD} BLOB_STORAGE_BUCKET=${BLOB_STORAGE_BUCKET} BLOB_STORAGE_ENDPOINT=${BLOB_STORAGE_ENDPOINT} BLOB_STORAGE_REGION=${BLOB_STORAGE_REGION} WEBHOOK_SECRET_KEY=${WEBHOOK_SECRET_KEY} EOF if [ -n "$RUNNER_TOKEN" ]; then echo "RUNNER_TOKEN=${RUNNER_TOKEN}" >> "$ENV_FILE" fi if [ -n "$BLOB_STORAGE_ACCESS_KEY_ID" ]; then echo "BLOB_STORAGE_ACCESS_KEY_ID=${BLOB_STORAGE_ACCESS_KEY_ID}" >> "$ENV_FILE" echo "BLOB_STORAGE_SECRET_ACCESS_KEY=${BLOB_STORAGE_SECRET_ACCESS_KEY}" >> "$ENV_FILE" fi if [ -n "$WORKER_INTERNAL_KEY" ]; then echo "WORKER_INTERNAL_KEY=${WORKER_INTERNAL_KEY}" >> "$ENV_FILE" fi if [ -n "$PACK_WORKER_URL" ]; then echo "PACK_WORKER_URL=${PACK_WORKER_URL}" >> "$ENV_FILE" fi chown musehub:musehub "$ENV_FILE" 2>/dev/null || true log ".env written ($(wc -l < "$ENV_FILE") lines, mode 600)" # ── Sanity check — no weak values leaked into env ──────────────────────────── WEAK_PASSWORDS=("musehub" "changeme123" "password" "postgres" "secret" "") for WEAK in "${WEAK_PASSWORDS[@]}"; do if [ "$DB_PASSWORD" = "$WEAK" ]; then die "DB_PASSWORD from SSM is a known weak value ($WEAK). Rotate it immediately." fi done if [ ${#DB_PASSWORD} -lt 16 ]; then die "DB_PASSWORD from SSM is too short (${#DB_PASSWORD} chars). Minimum is 16." fi log "Secrets sanity check passed." log "Run 'bash deploy/deploy.sh' to deploy."