#!/bin/sh # # Add asymmetric key pair and opaque API keys to a self-hosted Supabase installation. # # Reads JWT_SECRET from .env and generates: # - EC P-256 key pair (JWT_KEYS, JWT_JWKS) # - Opaque API keys (SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY) # - Internal: ES256 JWT API keys (ANON_KEY_ASYMMETRIC, SERVICE_ROLE_KEY_ASYMMETRIC) # # Usage: # sh add-new-auth-keys.sh # Interactive: prints keys, prompts to update .env # sh add-new-auth-keys.sh --update-env # Prints keys and writes them to .env # sh add-new-auth-keys.sh | tee keys # Non-interactive: prints keys only # # Prerequisites: # - .env file with JWT_SECRET set (run generate-keys.sh first) # - node (>= 16) or docker # set -e node_ok() { command -v node >/dev/null 2>&1 || return 1 major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1) [ -n "$major" ] && [ "$major" -ge 16 ] 2>/dev/null } # Resolve how to run node: local install (>= 16) preferred, docker fallback. if node_ok; then node_runner="node" else if command -v node >/dev/null 2>&1; then echo "Local node $(node -v) is too old (need >= 16), falling back to docker." fi if ! command -v docker >/dev/null 2>&1; then echo "Error: requires either node (>= 16) or docker." exit 1 fi if ! docker info >/dev/null 2>&1; then echo "Error: docker is installed but the daemon is not running." exit 1 fi if ! docker image inspect node:22-alpine >/dev/null 2>&1; then echo "Pulling node:22-alpine (first-run only)..." docker pull node:22-alpine fi node_runner="docker run --rm node:22-alpine node" fi # Read JWT_SECRET from .env if [ ! -f .env ]; then echo "Error: .env file not found. Run generate-keys.sh first." exit 1 fi jwt_secret=$(grep '^JWT_SECRET=' .env | cut -d= -f2- | tr -d '\r') if [ -z "$jwt_secret" ]; then echo "Error: JWT_SECRET not found in .env. Run generate-keys.sh first." exit 1 fi tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT # Node.js does the crypto-heavy work: # - EC P-256 keypair generation # - JWKS construction (with symmetric key included) # - ES256 JWT signing # - Opaque API key generation with checksum $node_runner -e ' const crypto = require("crypto"); const jwtSecret = process.argv[1]; // Generate EC P-256 keypair and export as JWK const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); const jwkPrivate = privateKey.export({ format: "jwk" }); const kid = crypto.randomUUID(); // Symmetric key as JWK (base64url-encoded) const octKey = { kty: "oct", k: Buffer.from(jwtSecret).toString("base64url"), alg: "HS256" }; // JWKS with private key (for Auth to sign tokens) const jwksKeypair = { keys: [ { kty: "EC", kid, use: "sig", key_ops: ["sign", "verify"], alg: "ES256", ext: true, crv: jwkPrivate.crv, x: jwkPrivate.x, y: jwkPrivate.y, d: jwkPrivate.d }, octKey ]}; // JWKS with public key only (for PostgREST, Realtime, Storage to verify) const jwksPublic = { keys: [ { kty: "EC", kid, use: "sig", key_ops: ["verify"], alg: "ES256", ext: true, crv: jwkPrivate.crv, x: jwkPrivate.x, y: jwkPrivate.y }, octKey ]}; // Sign ES256 JWT function signES256(payload) { const header = { alg: "ES256", typ: "JWT", kid }; const b64Header = Buffer.from(JSON.stringify(header)).toString("base64url"); const b64Payload = Buffer.from(JSON.stringify(payload)).toString("base64url"); const data = b64Header + "." + b64Payload; const sig = crypto.sign("SHA256", Buffer.from(data), { key: privateKey, dsaEncoding: "ieee-p1363" }).toString("base64url"); return data + "." + sig; } const iat = Math.floor(Date.now() / 1000); const exp = iat + 5 * 365 * 24 * 3600; // 5 years const anonJwt = signES256({ role: "anon", iss: "supabase", iat, exp }); const serviceJwt = signES256({ role: "service_role", iss: "supabase", iat, exp }); // Generate opaque API keys with checksum const PROJECT_REF = "supabase-self-hosted"; function generateOpaqueKey(prefix) { const random = crypto.randomBytes(17).toString("base64url").slice(0, 22); const intermediate = prefix + random; const checksum = crypto.createHash("sha256") .update(PROJECT_REF + "|" + intermediate) .digest("base64url") .slice(0, 8); return intermediate + "_" + checksum; } const publishableKey = generateOpaqueKey("sb_publishable_"); const secretKey = generateOpaqueKey("sb_secret_"); // Output as KEY=value lines for shell to parse console.log("SUPABASE_PUBLISHABLE_KEY=" + publishableKey); console.log("SUPABASE_SECRET_KEY=" + secretKey); console.log("ANON_KEY_ASYMMETRIC=" + anonJwt); console.log("SERVICE_ROLE_KEY_ASYMMETRIC=" + serviceJwt); console.log("JWT_KEYS=" + JSON.stringify(jwksKeypair.keys)); console.log("JWT_JWKS=" + JSON.stringify(jwksPublic)); ' "$jwt_secret" > "$tmpdir/output" # Read generated values SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' "$tmpdir/output" | cut -d= -f2-) SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' "$tmpdir/output" | cut -d= -f2-) ANON_KEY_ASYMMETRIC=$(grep '^ANON_KEY_ASYMMETRIC=' "$tmpdir/output" | cut -d= -f2-) SERVICE_ROLE_KEY_ASYMMETRIC=$(grep '^SERVICE_ROLE_KEY_ASYMMETRIC=' "$tmpdir/output" | cut -d= -f2-) JWT_KEYS=$(grep '^JWT_KEYS=' "$tmpdir/output" | cut -d= -f2-) JWT_JWKS=$(grep '^JWT_JWKS=' "$tmpdir/output" | cut -d= -f2-) echo "" echo "SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY}" echo "SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY}" echo "" echo "JWT_KEYS=${JWT_KEYS}" echo "" echo "JWT_JWKS=${JWT_JWKS}" echo "" echo "Ensure the following configuration is uncommented in docker-compose.yml for the asymmetric key pair to work:" echo "" echo " Auth: GOTRUE_JWT_KEYS: \${JWT_KEYS:-[]}" echo " Realtime: API_JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}" echo " Storage: JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}" echo "" if [ "$1" = "--update-env" ]; then update_env=true elif test -t 0; then printf "Update .env file? (y/N) " read -r REPLY case "$REPLY" in [Yy]) update_env=true ;; *) update_env=false ;; esac else echo "Running non-interactively. Pass --update-env to write to .env." update_env=false fi if [ "$update_env" != "true" ]; then exit 0 fi echo "Updating .env..." # Append new variables if they don't exist, or update them if they do for var in SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY ANON_KEY_ASYMMETRIC SERVICE_ROLE_KEY_ASYMMETRIC JWT_KEYS JWT_JWKS; do eval "val=\$$var" if grep -q "^${var}=" .env; then sed -i.old -e "s|^${var}=.*$|${var}=${val}|" .env else echo "${var}=${val}" >> .env fi done # Uncomment new auth configuration in docker-compose.yml echo "Updating docker-compose.yml..." if [ ! -f docker-compose.yml ]; then echo "Error: docker-compose.yml not found in $(pwd)" exit 1 fi # Always fall through to the grep check sed -i.old \ -e '/^[ ]*#GOTRUE_JWT_KEYS:/ s/#//' \ -e '/^[ ]*#API_JWT_JWKS:/ s/#//' \ -e '/^[ ]*#JWT_JWKS:/ s/#//' \ docker-compose.yml || true if grep -q '^[ ]*GOTRUE_JWT_KEYS:' docker-compose.yml && \ grep -q '^[ ]*API_JWT_JWKS:' docker-compose.yml && \ grep -q '^[ ]*JWT_JWKS:' docker-compose.yml; then echo "Done." else echo "Warning: could not edit docker-compose.yml. Uncomment auth configuration manually." fi