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