secrets.sh
bash
sha256:3c96607d926f5697959645ade1ba132b84d033f19b53eef454d40bc0fad07234
Merge 'infra/database-phase1-rds-secrets' into 'dev' — prop…
Human
23 hours ago
| 1 | #!/usr/bin/env bash |
| 2 | # MuseHub secrets bootstrap — fetch from AWS SSM Parameter Store, write .env |
| 3 | # |
| 4 | # Runs on the EC2 instance BEFORE deploy.sh. Pulls every secret from SSM |
| 5 | # Parameter Store (SecureString, AES-256 at rest via KMS) and writes a fresh |
| 6 | # /opt/musehub/.env. The .env on disk is the runtime injection point for |
| 7 | # all Docker containers (--env-file). |
| 8 | # |
| 9 | # Why SSM instead of a static .env: |
| 10 | # - Secrets never travel through source control or build artifacts. |
| 11 | # - Access is audited via CloudTrail (who fetched what, when). |
| 12 | # - Rotation updates SSM; next deploy.sh run picks up the new value. |
| 13 | # - IAM role on the EC2 instance grants read access — no AWS keys on disk. |
| 14 | # |
| 15 | # SSM parameter layout (all SecureString, KMS-encrypted): |
| 16 | # /musehub/<env>/DB_PASSWORD |
| 17 | # /musehub/<env>/DATABASE_URL (optional; full connection string override — see below) |
| 18 | # /musehub/<env>/WEBHOOK_SECRET_KEY |
| 19 | # /musehub/<env>/RUNNER_TOKEN |
| 20 | # /musehub/<env>/BLOB_STORAGE_ACCESS_KEY_ID |
| 21 | # /musehub/<env>/BLOB_STORAGE_SECRET_ACCESS_KEY |
| 22 | # /musehub/<env>/WORKER_INTERNAL_KEY (shared secret for Cloudflare Worker → MuseHub callbacks) |
| 23 | # /musehub/<env>/MPACK_WORKER_URL (public URL of the CF mpack-receiver Worker; optional) |
| 24 | # /musehub/<env>/BACKUP_R2_BUCKET (R2 bucket for deploy/backup.sh off-disk backups; optional, plain String not SecureString) |
| 25 | # |
| 26 | # DATABASE_URL resolution (see docs/database-architecture.md for the canonical picture): |
| 27 | # If /musehub/<env>/DATABASE_URL is set in SSM, it wins verbatim — this is how an |
| 28 | # environment on managed AWS RDS (staging, as of 2026-09-08) points at its real |
| 29 | # instance rather than the self-hosted default below. |
| 30 | # Otherwise, DATABASE_URL is constructed from DB_PASSWORD assuming a self-hosted |
| 31 | # Postgres container on the same Docker network (production's current setup): |
| 32 | # postgresql+asyncpg://musehub:<DB_PASSWORD>@postgres:5432/musehub |
| 33 | # This script previously never wrote DATABASE_URL at all — every environment's |
| 34 | # working DATABASE_URL came only from setup-ec2*.sh's one-time initial .env write, |
| 35 | # silently lost the moment secrets.sh was ever re-run. This fixes that gap for both |
| 36 | # environments, not just staging. |
| 37 | # |
| 38 | # Prerequisites: |
| 39 | # - AWS CLI v2 installed on the EC2 instance |
| 40 | # - EC2 instance profile with IAM policy: |
| 41 | # ssm:GetParameter, ssm:GetParametersByPath |
| 42 | # on arn:aws:ssm:<region>:<account>:parameter/musehub/<env>/* |
| 43 | # - KMS decrypt on the CMK used for the SecureString parameters |
| 44 | # |
| 45 | # Usage: |
| 46 | # MUSEHUB_ENV=production bash deploy/secrets.sh |
| 47 | # MUSEHUB_ENV=staging bash deploy/secrets.sh |
| 48 | # |
| 49 | # After this script writes .env, run deploy.sh as usual. |
| 50 | # |
| 51 | # Fallback (no SSM / local dev): |
| 52 | # If AWS CLI is not available or SSM fetch fails, the script exits non-zero |
| 53 | # so deploy.sh does not start with stale/missing secrets. For local dev, |
| 54 | # manage .env manually — never run this script on a dev laptop. |
| 55 | |
| 56 | set -euo pipefail |
| 57 | |
| 58 | MUSEHUB_ENV="${MUSEHUB_ENV:-production}" |
| 59 | APP_DIR="${APP_DIR:-/opt/musehub}" |
| 60 | ENV_FILE="$APP_DIR/.env" |
| 61 | REGION="${AWS_REGION:-us-east-1}" |
| 62 | SSM_PREFIX="/musehub/${MUSEHUB_ENV}" |
| 63 | |
| 64 | log() { echo "[secrets] $*"; } |
| 65 | die() { echo "[secrets] ERROR: $*" >&2; exit 1; } |
| 66 | |
| 67 | # ── Preflight ───────────────────────────────────────────────────────────────── |
| 68 | |
| 69 | command -v aws > /dev/null 2>&1 || die "AWS CLI not installed. Install: sudo apt-get install -y awscli" |
| 70 | |
| 71 | # Verify we can reach SSM (IAM role check) — use GetParameter on DB_PASSWORD |
| 72 | # (always required) rather than GetParametersByPath (requires broader permission). |
| 73 | aws ssm get-parameter \ |
| 74 | --name "$SSM_PREFIX/DB_PASSWORD" \ |
| 75 | --region "$REGION" \ |
| 76 | --with-decryption \ |
| 77 | --query 'Parameter.Value' \ |
| 78 | --output text > /dev/null 2>&1 \ |
| 79 | || die "Cannot read $SSM_PREFIX/DB_PASSWORD from SSM — check the EC2 instance IAM role." |
| 80 | |
| 81 | log "Fetching secrets from SSM: $SSM_PREFIX (region=$REGION)" |
| 82 | |
| 83 | # ── Fetch each parameter ────────────────────────────────────────────────────── |
| 84 | |
| 85 | _get() { |
| 86 | local name="$1" |
| 87 | local required="${2:-true}" |
| 88 | local value |
| 89 | value=$(aws ssm get-parameter \ |
| 90 | --name "$SSM_PREFIX/$name" \ |
| 91 | --region "$REGION" \ |
| 92 | --with-decryption \ |
| 93 | --query 'Parameter.Value' \ |
| 94 | --output text 2>/dev/null) || { |
| 95 | if [ "$required" = "true" ]; then |
| 96 | die "Required parameter $SSM_PREFIX/$name not found in SSM" |
| 97 | fi |
| 98 | echo "" |
| 99 | return |
| 100 | } |
| 101 | echo "$value" |
| 102 | } |
| 103 | |
| 104 | DB_PASSWORD=$(_get "DB_PASSWORD") |
| 105 | DATABASE_URL_OVERRIDE=$(_get "DATABASE_URL" false) |
| 106 | WEBHOOK_SECRET_KEY=$(_get "WEBHOOK_SECRET_KEY") |
| 107 | RUNNER_TOKEN=$(_get "RUNNER_TOKEN" false) |
| 108 | BLOB_STORAGE_ACCESS_KEY_ID=$(_get "BLOB_STORAGE_ACCESS_KEY_ID" false) |
| 109 | BLOB_STORAGE_SECRET_ACCESS_KEY=$(_get "BLOB_STORAGE_SECRET_ACCESS_KEY" false) |
| 110 | WORKER_INTERNAL_KEY=$(_get "WORKER_INTERNAL_KEY" false) |
| 111 | PACK_WORKER_URL=$(_get "PACK_WORKER_URL" false) |
| 112 | BACKUP_R2_BUCKET=$(_get "BACKUP_R2_BUCKET" false) |
| 113 | |
| 114 | # ── Resolve per-environment non-secret config ───────────────────────────────── |
| 115 | |
| 116 | if [ "$MUSEHUB_ENV" = "staging" ]; then |
| 117 | PUBLIC_URL="https://staging.musehub.ai" |
| 118 | CORS_ORIGINS='["https://staging.musehub.ai"]' |
| 119 | BLOB_STORAGE_BUCKET="musehub-staging" |
| 120 | BLOB_STORAGE_ENDPOINT="https://bed873d46de5273abf843468a7833f09.r2.cloudflarestorage.com" |
| 121 | BLOB_STORAGE_REGION="auto" |
| 122 | elif [ "$MUSEHUB_ENV" = "production" ]; then |
| 123 | PUBLIC_URL="https://musehub.ai" |
| 124 | CORS_ORIGINS='["https://musehub.ai", "https://www.musehub.ai"]' |
| 125 | BLOB_STORAGE_BUCKET="musehub-prod" |
| 126 | BLOB_STORAGE_ENDPOINT="https://bed873d46de5273abf843468a7833f09.r2.cloudflarestorage.com" |
| 127 | BLOB_STORAGE_REGION="auto" |
| 128 | else |
| 129 | die "Unknown MUSEHUB_ENV='$MUSEHUB_ENV'. Must be 'staging' or 'production'." |
| 130 | fi |
| 131 | |
| 132 | if [ -n "$DATABASE_URL_OVERRIDE" ]; then |
| 133 | DATABASE_URL="$DATABASE_URL_OVERRIDE" |
| 134 | log "Using DATABASE_URL override from SSM (managed database, e.g. RDS)" |
| 135 | else |
| 136 | DATABASE_URL="postgresql+asyncpg://musehub:${DB_PASSWORD}@postgres:5432/musehub" |
| 137 | log "No DATABASE_URL override in SSM — constructing self-hosted Postgres URL" |
| 138 | fi |
| 139 | |
| 140 | # ── Write .env ──────────────────────────────────────────────────────────────── |
| 141 | |
| 142 | log "Writing $ENV_FILE (env=$MUSEHUB_ENV, public_url=$PUBLIC_URL)" |
| 143 | |
| 144 | # Back up the existing .env if present |
| 145 | if [ -f "$ENV_FILE" ]; then |
| 146 | cp "$ENV_FILE" "${ENV_FILE}.bak.$(date +%Y%m%d_%H%M%S)" |
| 147 | log "Previous .env backed up" |
| 148 | fi |
| 149 | |
| 150 | # Write new .env — mode 600, owner musehub |
| 151 | umask 177 |
| 152 | cat > "$ENV_FILE" << EOF |
| 153 | # Generated by deploy/secrets.sh at $(date -u +%Y-%m-%dT%H:%M:%SZ) |
| 154 | # Secrets sourced from AWS SSM Parameter Store: $SSM_PREFIX |
| 155 | # DO NOT edit manually — re-run secrets.sh to refresh from SSM. |
| 156 | |
| 157 | MUSE_ENV=${MUSEHUB_ENV} |
| 158 | DEBUG=false |
| 159 | PUBLIC_URL=${PUBLIC_URL} |
| 160 | CORS_ORIGINS=${CORS_ORIGINS} |
| 161 | DB_PASSWORD=${DB_PASSWORD} |
| 162 | DATABASE_URL=${DATABASE_URL} |
| 163 | BLOB_STORAGE_BUCKET=${BLOB_STORAGE_BUCKET} |
| 164 | BLOB_STORAGE_ENDPOINT=${BLOB_STORAGE_ENDPOINT} |
| 165 | BLOB_STORAGE_REGION=${BLOB_STORAGE_REGION} |
| 166 | WEBHOOK_SECRET_KEY=${WEBHOOK_SECRET_KEY} |
| 167 | EOF |
| 168 | if [ -n "$RUNNER_TOKEN" ]; then |
| 169 | echo "RUNNER_TOKEN=${RUNNER_TOKEN}" >> "$ENV_FILE" |
| 170 | fi |
| 171 | if [ -n "$BLOB_STORAGE_ACCESS_KEY_ID" ]; then |
| 172 | echo "BLOB_STORAGE_ACCESS_KEY_ID=${BLOB_STORAGE_ACCESS_KEY_ID}" >> "$ENV_FILE" |
| 173 | echo "BLOB_STORAGE_SECRET_ACCESS_KEY=${BLOB_STORAGE_SECRET_ACCESS_KEY}" >> "$ENV_FILE" |
| 174 | fi |
| 175 | if [ -n "$WORKER_INTERNAL_KEY" ]; then |
| 176 | echo "WORKER_INTERNAL_KEY=${WORKER_INTERNAL_KEY}" >> "$ENV_FILE" |
| 177 | fi |
| 178 | if [ -n "$PACK_WORKER_URL" ]; then |
| 179 | echo "PACK_WORKER_URL=${PACK_WORKER_URL}" >> "$ENV_FILE" |
| 180 | fi |
| 181 | if [ -n "$BACKUP_R2_BUCKET" ]; then |
| 182 | echo "BACKUP_R2_BUCKET=${BACKUP_R2_BUCKET}" >> "$ENV_FILE" |
| 183 | fi |
| 184 | |
| 185 | chown musehub:musehub "$ENV_FILE" 2>/dev/null || true |
| 186 | log ".env written ($(wc -l < "$ENV_FILE") lines, mode 600)" |
| 187 | |
| 188 | # ── Sanity check — no weak values leaked into env ──────────────────────────── |
| 189 | |
| 190 | WEAK_PASSWORDS=("musehub" "changeme123" "password" "postgres" "secret" "") |
| 191 | for WEAK in "${WEAK_PASSWORDS[@]}"; do |
| 192 | if [ "$DB_PASSWORD" = "$WEAK" ]; then |
| 193 | die "DB_PASSWORD from SSM is a known weak value ($WEAK). Rotate it immediately." |
| 194 | fi |
| 195 | done |
| 196 | |
| 197 | if [ ${#DB_PASSWORD} -lt 16 ]; then |
| 198 | die "DB_PASSWORD from SSM is too short (${#DB_PASSWORD} chars). Minimum is 16." |
| 199 | fi |
| 200 | |
| 201 | log "Secrets sanity check passed." |
| 202 | log "Run 'bash deploy/deploy.sh' to deploy." |
File History
4 commits
sha256:3c96607d926f5697959645ade1ba132b84d033f19b53eef454d40bc0fad07234
Merge 'infra/database-phase1-rds-secrets' into 'dev' — prop…
Human
23 hours ago
sha256:f437d5c6fe90cdefc4929b99a6c0b52cfd4340deb42efc226ec17ecd2adc86e2
Merge 'docs/fix-stale-rds-claims' into 'dev' — proposal: do…
Human
1 day ago
sha256:3fadb0439bba9451b89229676971c0d4a40900dec7810e9d5f8791b8d950d505
fix: install.sh version from latest published tarball, not …
Sonnet 4.6
minor
⚠
103 days ago
sha256:763eb2cb8675073b84c19345b27586d2ed939a9aee97c5479b69f502f1a70eff
fix(tests): update test suite to match current implementation
Sonnet 4.6
patch
124 days ago