Last active
February 26, 2026 15:01
-
-
Save vpcano/28e93b8af3cb36ba3ecd9a397ccf0ab7 to your computer and use it in GitHub Desktop.
Script to create a pair of asymmetric JWT keys for self-hosted Supabase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ========================================== | |
| # SUPABASE ASYMMETRIC KEY GENERATOR | |
| # ========================================== | |
| RED="\033[0;31m" | |
| GREEN="\033[0;32m" | |
| YELLOW="\033[1;33m" | |
| BLUE="\033[0;34m" | |
| CYAN="\033[0;36m" | |
| NC="\033[0m" # No Color | |
| set -e | |
| # 1. Pre-checks | |
| if ! command -v node &> /dev/null; then | |
| echo -e "${RED}Error: Node.js is not installed. Please install it to run this script.${NC}" | |
| exit 1 | |
| fi | |
| if ! command -v openssl &> /dev/null; then | |
| echo -e "${RED}Error: OpenSSL is not installed.${NC}" | |
| exit 1 | |
| fi | |
| echo -e "\n${CYAN}Asymmetric Key Generator for Self-Hosted Supabase${NC}" | |
| echo "--------------------------------------------------------" | |
| # 2. Choose Algorithm | |
| echo "Select signing algorithm:" | |
| echo " 1) ES256 (Elliptic Curve - P-256) [Recommended]" | |
| echo " 2) RS256 (RSA - 2048 bit)" | |
| read -p "Option [1]: " ALG_OPT < /dev/tty | |
| ALG_OPT=${ALG_OPT:-1} | |
| if [ "$ALG_OPT" == "1" ]; then | |
| ALG="ES256" | |
| echo -e "Selected: ${BLUE}ES256 (ECC)${NC}" | |
| # Generate EC Private Key | |
| openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem | |
| else | |
| ALG="RS256" | |
| echo -e "Selected: ${BLUE}RS256 (RSA)${NC}" | |
| # Generate RSA Private Key | |
| openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 | |
| fi | |
| # 3. Configure KID | |
| echo -e "\nEnter a Key ID (kid) to identify this key." | |
| read -p "Key ID [Leave empty to generate a random one]: " KID_INPUT < /dev/tty | |
| if [ -z "$KID_INPUT" ]; then | |
| KID="key-$(echo "$ALG" | tr '[:upper:]' '[:lower:]')-$(openssl rand -hex 4)" | |
| echo -e "Generated KID: ${BLUE}$KID${NC}" | |
| else | |
| KID=$KID_INPUT | |
| echo -e "Using KID: ${BLUE}$KID${NC}" | |
| fi | |
| # 4. Request Old Tokens | |
| echo -e "\nEnter your current JWTs (to re-sign them with the new key):" | |
| read -p "Paste current ANON_KEY: " OLD_ANON < /dev/tty | |
| read -p "Paste current SERVICE_ROLE_KEY: " OLD_SERVICE < /dev/tty | |
| if [ -z "$OLD_ANON" ] || [ -z "$OLD_SERVICE" ]; then | |
| echo -e "${RED}Error: You must enter both tokens to regenerate them.${NC}" | |
| rm private_key.pem | |
| exit 1 | |
| fi | |
| echo -e "\nProcessing keys and signing tokens... (using Node.js)" | |
| # 5. Embedded Node.js script to process cryptography | |
| # We use this because manipulating JWK and signing JWT in pure bash is error-prone. | |
| # This script does NOT require npm install, it uses native Node >v16 libraries. | |
| cat <<EOF > processor.js | |
| const fs = require('fs'); | |
| const crypto = require('crypto'); | |
| try { | |
| const pem = fs.readFileSync('private_key.pem'); | |
| const alg = '$ALG'; | |
| const kid = '$KID'; | |
| const oldAnon = '$OLD_ANON'; | |
| const oldService = '$OLD_SERVICE'; | |
| // 1. Convert PEM to JWK | |
| const privateKey = crypto.createPrivateKey(pem); | |
| const jwkPrivate = privateKey.export({ format: 'jwk' }); | |
| // Add required metadata | |
| jwkPrivate.kid = kid; | |
| jwkPrivate.alg = alg; | |
| jwkPrivate.use = 'sig'; | |
| jwkPrivate.key_ops = ['sign']; | |
| // Create public version (for PostgREST) - Remove private parts | |
| const jwkPublic = { ...jwkPrivate }; | |
| if (alg === 'RS256') { | |
| delete jwkPublic.d; | |
| delete jwkPublic.p; | |
| delete jwkPublic.q; | |
| delete jwkPublic.dp; | |
| delete jwkPublic.dq; | |
| delete jwkPublic.qi; | |
| } else { | |
| delete jwkPublic.d; | |
| } | |
| delete jwkPublic.key_ops; | |
| // Format outputs for environment variables | |
| const signingKeys = JSON.stringify([jwkPrivate]); | |
| const jwks = JSON.stringify({ keys: [jwkPublic] }); | |
| // 2. JWT Signing Function (Resign) | |
| function resignToken(oldToken, roleName) { | |
| // Decode payload (middle part) | |
| const parts = oldToken.split('.'); | |
| if (parts.length !== 3) throw new Error('Invalid ' + roleName + ' token'); | |
| const payloadBuffer = Buffer.from(parts[1], 'base64'); | |
| const payload = JSON.parse(payloadBuffer.toString()); | |
| // Ensure role is correct (security check) | |
| payload.role = roleName; | |
| // Create new Header | |
| const header = { | |
| alg: alg, | |
| typ: 'JWT', | |
| kid: kid | |
| }; | |
| // Construct JWT | |
| const b64Header = Buffer.from(JSON.stringify(header)).toString('base64url'); | |
| const b64Payload = Buffer.from(JSON.stringify(payload)).toString('base64url'); | |
| const dataToSign = b64Header + '.' + b64Payload; | |
| // Sign | |
| const sign = crypto.createSign(alg === 'RS256' ? 'RSA-SHA256' : 'SHA256'); | |
| sign.update(dataToSign); | |
| const signature = sign.sign({ | |
| key: privateKey, | |
| dsaEncoding: 'ieee-p1363', | |
| padding: crypto.constants.RSA_PKCS1_PADDING, | |
| }, 'base64url'); | |
| return dataToSign + '.' + signature; | |
| } | |
| const newAnon = resignToken(oldAnon, 'anon'); | |
| const newService = resignToken(oldService, 'service_role'); | |
| // Print results in JSON for Bash to read | |
| console.log(JSON.stringify({ | |
| signingKeys: signingKeys, | |
| jwks: jwks, | |
| anon: newAnon, | |
| service: newService | |
| })); | |
| } catch (err) { | |
| console.error("Error in Node script:", err.message); | |
| process.exit(1); | |
| } | |
| EOF | |
| # 6. Execute Node and capture output | |
| OUTPUT=$(node processor.js) | |
| rm processor.js private_key.pem # Cleanup | |
| # Extract values using node (to avoid jq dependency) | |
| JWT_SIGNING_KEYS=$(echo "$OUTPUT" | node -e "console.log(JSON.parse(fs.readFileSync(0)).signingKeys)") | |
| JWT_JWKS=$(echo "$OUTPUT" | node -e "console.log(JSON.parse(fs.readFileSync(0)).jwks)") | |
| NEW_ANON=$(echo "$OUTPUT" | node -e "console.log(JSON.parse(fs.readFileSync(0)).anon)") | |
| NEW_SERVICE=$(echo "$OUTPUT" | node -e "console.log(JSON.parse(fs.readFileSync(0)).service)") | |
| # 7. Show Final Result | |
| echo -e "\n${GREEN}PROCESS COMPLETED!${NC}" | |
| echo -e "1. Copy the content below and append it to your ${YELLOW}.env${NC} file." | |
| echo -e "2. Update your ${YELLOW}docker-compose.yml${NC} services to map these variables as shown in the comments." | |
| echo "====================================================================" | |
| echo "" | |
| echo "# --- 1. AUTH CONFIGURATION (GoTrue) ---" | |
| echo "# Map in docker-compose 'auth' service -> GOTRUE_JWT_KEYS" | |
| echo "JWT_SIGNING_KEYS='$JWT_SIGNING_KEYS'" | |
| echo "" | |
| echo "# Map in docker-compose 'auth' service -> GOTRUE_JWT_VALID_METHODS" | |
| echo "JWT_METHODS=HS256,RS256,ES256" | |
| echo "" | |
| echo "# --- 2. SHARED PUBLIC KEYS (Verification) ---" | |
| echo "# Map this variable to the following services in docker-compose:" | |
| echo "# - 'rest' -> PGRST_JWT_SECRET" | |
| echo "# - 'rest' -> PGRST_APP_SETTINGS_JWT_SECRET" | |
| echo "# - 'realtime' -> API_JWT_JWKS" | |
| echo "# - 'storage' -> JWT_JWKS" | |
| echo "JWT_JWKS='$JWT_JWKS'" | |
| echo "" | |
| echo "# --- 3. NEW API TOKENS ---" | |
| echo "# Replace your old keys in .env (and updating client-side apps)" | |
| echo "ANON_KEY=$NEW_ANON" | |
| echo "SERVICE_ROLE_KEY=$NEW_SERVICE" | |
| echo "" | |
| echo "====================================================================" | |
| echo -e "${YELLOW}Remember to restart your containers to apply changes.${NC}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment