Skip to content

Instantly share code, notes, and snippets.

@vpcano
Last active February 26, 2026 15:01
Show Gist options
  • Select an option

  • Save vpcano/28e93b8af3cb36ba3ecd9a397ccf0ab7 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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