gabriel / musehub public
secrets.sh bash
202 lines 8.6 KB
Raw
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