gabriel / musehub public
secrets.sh bash
149 lines 5.6 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 153 days 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>/WEBHOOK_SECRET_KEY
18 # /musehub/<env>/RUNNER_TOKEN
19 # /musehub/<env>/R2_ACCESS_KEY_ID (if using Cloudflare R2)
20 # /musehub/<env>/R2_SECRET_ACCESS_KEY (if using Cloudflare R2)
21 # /musehub/<env>/WORKER_INTERNAL_KEY (shared secret for Cloudflare Worker → MuseHub callbacks)
22 #
23 # Prerequisites:
24 # - AWS CLI v2 installed on the EC2 instance
25 # - EC2 instance profile with IAM policy:
26 # ssm:GetParameter, ssm:GetParametersByPath
27 # on arn:aws:ssm:<region>:<account>:parameter/musehub/<env>/*
28 # - KMS decrypt on the CMK used for the SecureString parameters
29 #
30 # Usage:
31 # MUSEHUB_ENV=production bash deploy/secrets.sh
32 # MUSEHUB_ENV=staging bash deploy/secrets.sh
33 #
34 # After this script writes .env, run deploy.sh as usual.
35 #
36 # Fallback (no SSM / local dev):
37 # If AWS CLI is not available or SSM fetch fails, the script exits non-zero
38 # so deploy.sh does not start with stale/missing secrets. For local dev,
39 # manage .env manually — never run this script on a dev laptop.
40
41 set -euo pipefail
42
43 MUSEHUB_ENV="${MUSEHUB_ENV:-production}"
44 APP_DIR="${APP_DIR:-/opt/musehub}"
45 ENV_FILE="$APP_DIR/.env"
46 REGION="${AWS_REGION:-us-east-1}"
47 SSM_PREFIX="/musehub/${MUSEHUB_ENV}"
48
49 log() { echo "[secrets] $*"; }
50 die() { echo "[secrets] ERROR: $*" >&2; exit 1; }
51
52 # ── Preflight ─────────────────────────────────────────────────────────────────
53
54 command -v aws > /dev/null 2>&1 || die "AWS CLI not installed. Install: sudo apt-get install -y awscli"
55
56 # Verify we can reach SSM (IAM role check)
57 aws ssm get-parameters-by-path \
58 --path "$SSM_PREFIX" \
59 --region "$REGION" \
60 --with-decryption \
61 --query 'Parameters[].Name' \
62 --output text > /dev/null 2>&1 \
63 || die "Cannot read from SSM at $SSM_PREFIX — check the EC2 instance IAM role."
64
65 log "Fetching secrets from SSM: $SSM_PREFIX (region=$REGION)"
66
67 # ── Fetch each parameter ──────────────────────────────────────────────────────
68
69 _get() {
70 local name="$1"
71 local required="${2:-true}"
72 local value
73 value=$(aws ssm get-parameter \
74 --name "$SSM_PREFIX/$name" \
75 --region "$REGION" \
76 --with-decryption \
77 --query 'Parameter.Value' \
78 --output text 2>/dev/null) || {
79 if [ "$required" = "true" ]; then
80 die "Required parameter $SSM_PREFIX/$name not found in SSM"
81 fi
82 echo ""
83 return
84 }
85 echo "$value"
86 }
87
88 DB_PASSWORD=$(_get "DB_PASSWORD")
89 WEBHOOK_SECRET_KEY=$(_get "WEBHOOK_SECRET_KEY" false)
90 RUNNER_TOKEN=$(_get "RUNNER_TOKEN" false)
91 R2_ACCESS_KEY_ID=$(_get "R2_ACCESS_KEY_ID" false)
92 R2_SECRET_ACCESS_KEY=$(_get "R2_SECRET_ACCESS_KEY" false)
93 WORKER_INTERNAL_KEY=$(_get "WORKER_INTERNAL_KEY" false)
94
95 # ── Write .env ────────────────────────────────────────────────────────────────
96
97 log "Writing $ENV_FILE"
98
99 # Back up the existing .env if present
100 if [ -f "$ENV_FILE" ]; then
101 cp "$ENV_FILE" "${ENV_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
102 log "Previous .env backed up"
103 fi
104
105 # Write new .env — mode 600, owner musehub
106 umask 177
107 cat > "$ENV_FILE" << EOF
108 # Generated by deploy/secrets.sh at $(date -u +%Y-%m-%dT%H:%M:%SZ)
109 # Secrets sourced from AWS SSM Parameter Store: $SSM_PREFIX
110 # DO NOT edit manually — re-run secrets.sh to refresh from SSM.
111
112 MUSE_ENV=${MUSEHUB_ENV}
113 DEBUG=false
114 DB_PASSWORD=${DB_PASSWORD}
115 CORS_ORIGINS=["https://musehub.ai", "https://www.musehub.ai"]
116 EOF
117
118 if [ -n "$WEBHOOK_SECRET_KEY" ]; then
119 echo "WEBHOOK_SECRET_KEY=${WEBHOOK_SECRET_KEY}" >> "$ENV_FILE"
120 fi
121 if [ -n "$RUNNER_TOKEN" ]; then
122 echo "RUNNER_TOKEN=${RUNNER_TOKEN}" >> "$ENV_FILE"
123 fi
124 if [ -n "$R2_ACCESS_KEY_ID" ]; then
125 echo "R2_ACCESS_KEY_ID=${R2_ACCESS_KEY_ID}" >> "$ENV_FILE"
126 echo "R2_SECRET_ACCESS_KEY=${R2_SECRET_ACCESS_KEY}" >> "$ENV_FILE"
127 fi
128 if [ -n "$WORKER_INTERNAL_KEY" ]; then
129 echo "WORKER_INTERNAL_KEY=${WORKER_INTERNAL_KEY}" >> "$ENV_FILE"
130 fi
131
132 chown musehub:musehub "$ENV_FILE" 2>/dev/null || true
133 log ".env written ($(wc -l < "$ENV_FILE") lines, mode 600)"
134
135 # ── Sanity check — no weak values leaked into env ────────────────────────────
136
137 WEAK_PASSWORDS=("musehub" "changeme123" "password" "postgres" "secret" "")
138 for WEAK in "${WEAK_PASSWORDS[@]}"; do
139 if [ "$DB_PASSWORD" = "$WEAK" ]; then
140 die "DB_PASSWORD from SSM is a known weak value ($WEAK). Rotate it immediately."
141 fi
142 done
143
144 if [ ${#DB_PASSWORD} -lt 16 ]; then
145 die "DB_PASSWORD from SSM is too short (${#DB_PASSWORD} chars). Minimum is 16."
146 fi
147
148 log "Secrets sanity check passed."
149 log "Run 'bash deploy/deploy.sh' to deploy."
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor 153 days ago