#!/usr/bin/env bash # Rotate Supabase JWT secrets + re-sign the anon / service-role keys. # # Usage: # infra/scripts/rotate-jwt.sh # generate a fresh secret # infra/scripts/rotate-jwt.sh # re-sign with a given secret # # Output: three lines printed to stdout. Paste them into .env (dev) or # .env.production (prod): # SUPABASE_JWT_SECRET= # PUBLIC_SUPABASE_ANON_KEY= # SUPABASE_SERVICE_ROLE_KEY= # # After replacing in .env, restart the affected containers: # docker compose -f infra/docker-compose.dev.yml up -d --force-recreate kong auth rest realtime # and any existing user sessions signed with the old secret are invalidated — # everyone re-logs once. Document that in the commit when rotating dev. set -euo pipefail SECRET="${1:-$(openssl rand -base64 48 | tr -d '\n')}" sign_jwt() { local role="$1" local header='{"alg":"HS256","typ":"JWT"}' # Long-lived anon/service keys (10 years) — the guardrail is the secret # itself rotating, not the token expiry. local exp=$(( $(date +%s) + 10 * 365 * 24 * 3600 )) local payload="{\"iss\":\"supabase-demo\",\"role\":\"$role\",\"exp\":$exp}" local b64h b64p b64h=$(printf '%s' "$header" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') b64p=$(printf '%s' "$payload" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=') local sig sig=$(printf '%s.%s' "$b64h" "$b64p" \ | openssl dgst -sha256 -hmac "$SECRET" -binary \ | openssl base64 -e -A \ | tr '+/' '-_' | tr -d '=') printf '%s.%s.%s' "$b64h" "$b64p" "$sig" } ANON=$(sign_jwt "anon") SERVICE=$(sign_jwt "service_role") cat <