mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
add new opaque api keys and new auth to self-hosted
This commit is contained in:
1 parent
ec26943390
commit
6190518640
7 files changed
+526
-48
No files matched your search
+29
-2
@@ -15,11 +15,31 @@
|
||||
# Postgres
|
||||
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
|
||||
|
||||
# Symmetric encryption key and JWT API keys
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||
|
||||
# Asymmetric key pair (ES256) and opaque API keys
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys
|
||||
#
|
||||
# To generate:
|
||||
# sh ./utils/add-new-auth-keys.sh
|
||||
#
|
||||
# Opaque API key for client-side use (anon role).
|
||||
SUPABASE_PUBLISHABLE_KEY=
|
||||
# Opaque API key for server-side use (service_role). Never expose in client code.
|
||||
SUPABASE_SECRET_KEY=
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric).
|
||||
# Used by Auth.
|
||||
JWT_KEYS=
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# Used by PostgREST, Realtime, Storage to verify tokens.
|
||||
JWT_JWKS=
|
||||
|
||||
# Access to Dashboard
|
||||
DASHBOARD_USERNAME=supabase
|
||||
DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated
|
||||
@@ -260,12 +280,19 @@ GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
|
||||
|
||||
|
||||
############
|
||||
# API Proxy - Configuration for the Kong API gateway
|
||||
# API gateway
|
||||
############
|
||||
|
||||
# Kong configuration variables
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# Used internally by the API gateway - DO NOT use in any client or server code.
|
||||
# Pre-signed ES256 JWT "API key" for anon role.
|
||||
ANON_KEY_ASYMMETRIC=
|
||||
# Pre-signed ES256 JWT "API key" for service_role.
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=
|
||||
|
||||
|
||||
############
|
||||
# imgproxy
|
||||
|
||||
+47
-25
@@ -27,6 +27,8 @@ services:
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
kong:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# Binds nestjs listener to both IPv4 and IPv6 network interfaces
|
||||
HOSTNAME: "::"
|
||||
@@ -70,35 +72,45 @@ services:
|
||||
|
||||
kong:
|
||||
container_name: supabase-kong
|
||||
image: kong:2.8.1
|
||||
image: kong/kong:3.9.1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- api-gw
|
||||
healthcheck:
|
||||
test: ["CMD", "kong", "health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- ${KONG_HTTP_PORT}:8000/tcp
|
||||
- ${KONG_HTTPS_PORT}:8443/tcp
|
||||
volumes:
|
||||
# https://github.com/supabase/supabase/issues/12661
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||
- ./volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
|
||||
#- ./volumes/api/server.crt:/home/kong/server.crt:ro
|
||||
#- ./volumes/api/server.key:/home/kong/server.key:ro
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
|
||||
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
|
||||
# https://github.com/supabase/cli/issues/14
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
#KONG_SSL_CERT: /home/kong/server.crt
|
||||
#KONG_SSL_CERT_KEY: /home/kong/server.key
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-}
|
||||
ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-}
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-}
|
||||
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
||||
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
# https://unix.stackexchange.com/a/294837
|
||||
entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start'
|
||||
entrypoint: /home/kong/kong-entrypoint.sh
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
@@ -121,8 +133,6 @@ services:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
@@ -139,8 +149,13 @@ services:
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric)
|
||||
#GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
|
||||
|
||||
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
|
||||
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
|
||||
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||
@@ -232,15 +247,14 @@ services:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
|
||||
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
# PostgREST accepts a plain-text symmetric secret, a single JWK, or a JWKS
|
||||
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
|
||||
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
||||
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
|
||||
@@ -258,8 +272,6 @@ services:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -279,7 +291,13 @@ services:
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||
DB_ENC_KEY: supabaserealtime
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric)
|
||||
#API_JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
DNS_NODES: "''"
|
||||
@@ -319,7 +337,13 @@ services:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
POSTGREST_URL: http://rest:3000
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric)
|
||||
#JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
REQUEST_ALLOW_X_FORWARDED_PATH: "true"
|
||||
FILE_SIZE_LIMIT: 52428800
|
||||
@@ -375,8 +399,6 @@ services:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: ${POSTGRES_HOST}
|
||||
@@ -394,16 +416,21 @@ services:
|
||||
- ./volumes/functions:/home/deno/functions:Z
|
||||
- deno-cache:/root/.cache/deno
|
||||
depends_on:
|
||||
analytics:
|
||||
kong:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
# New opaque API keys
|
||||
SUPABASE_PUBLISHABLE_KEYS: "{\"default\":\"${SUPABASE_PUBLISHABLE_KEY:-}\"}"
|
||||
SUPABASE_SECRET_KEYS: "{\"default\":\"${SUPABASE_SECRET_KEY:-}\"}"
|
||||
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
# TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786
|
||||
# TODO: Allow configuring VERIFY_JWT per function.
|
||||
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
|
||||
command:
|
||||
[
|
||||
@@ -495,9 +522,6 @@ services:
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
depends_on:
|
||||
vector:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_HOST: /var/run/postgresql
|
||||
PGPORT: ${POSTGRES_PORT}
|
||||
@@ -574,8 +598,6 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: 4000
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/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)
|
||||
# - openssl
|
||||
# - node >= 16
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "Error: openssl is required but not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "Error: node (>= 16) is required but not found."
|
||||
exit 1
|
||||
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-)
|
||||
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
|
||||
|
||||
# Generate EC P-256 private key
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out "$tmpdir/ec_private.pem" 2>/dev/null
|
||||
|
||||
# Node.js does the crypto-heavy work:
|
||||
# - PEM -> JWK conversion
|
||||
# - JWKS construction (with symmetric key included)
|
||||
# - ES256 JWT signing
|
||||
# - Opaque API key generation with checksum
|
||||
node -e '
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
|
||||
const pem = fs.readFileSync(process.argv[1]);
|
||||
const jwtSecret = process.argv[2];
|
||||
|
||||
// EC key -> JWK
|
||||
const privateKey = crypto.createPrivateKey(pem);
|
||||
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));
|
||||
' "$tmpdir/ec_private.pem" "$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 "ANON_KEY_ASYMMETRIC=${ANON_KEY_ASYMMETRIC}"
|
||||
echo "SERVICE_ROLE_KEY_ASYMMETRIC=${SERVICE_ROLE_KEY_ASYMMETRIC}"
|
||||
echo ""
|
||||
echo "JWT_KEYS=${JWT_KEYS}"
|
||||
echo ""
|
||||
echo "JWT_JWKS=${JWT_JWKS}"
|
||||
echo ""
|
||||
echo "To enable asymmetric key support, uncomment these lines in docker-compose.yml:"
|
||||
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
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Generate secrets and legacy symmetric JWT API keys for self-hosted Supabase.
|
||||
#
|
||||
# Generates: JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, and other secrets
|
||||
# needed for a fresh installation.
|
||||
#
|
||||
# Usage:
|
||||
# sh generate-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh generate-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh generate-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Portions of this code are derived from Inder Singh's setup.sh shell script.
|
||||
# Copyright 2025 Inder Singh. Licensed under Apache License 2.0.
|
||||
# Original source: https://github.com/singh-inder/supabase-automated-self-host/blob/main/setup.sh
|
||||
@@ -35,7 +45,7 @@ fi
|
||||
|
||||
jwt_secret="$(gen_base64 30)"
|
||||
|
||||
# Used in get_token()
|
||||
# Used in gen_token()
|
||||
header='{"alg":"HS256","typ":"JWT"}'
|
||||
iat=$(date +%s)
|
||||
exp=$((iat + 5 * 3600 * 24 * 365)) # 5 years
|
||||
@@ -87,21 +97,23 @@ echo "POSTGRES_PASSWORD=${postgres_password}"
|
||||
echo "DASHBOARD_PASSWORD=${dashboard_password}"
|
||||
echo ""
|
||||
|
||||
if ! test -t 0; then
|
||||
echo "Running non-interactively. Skipping .env update."
|
||||
exit 0
|
||||
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
|
||||
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Not updating .env"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Rotate opaque API keys for a self-hosted Supabase installation.
|
||||
#
|
||||
# Regenerates SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY
|
||||
# without touching the asymmetric key pair (JWKS) or JWT tokens.
|
||||
#
|
||||
# Usage:
|
||||
# sh rotate-new-api-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh rotate-new-api-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh rotate-new-api-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - .env file (run generate-keys.sh and add-new-auth-keys.sh first)
|
||||
# - node >= 16
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "Error: node (>= 16) is required but not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
node -e '
|
||||
const crypto = require("crypto");
|
||||
|
||||
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_");
|
||||
|
||||
console.log("SUPABASE_PUBLISHABLE_KEY=" + publishableKey);
|
||||
console.log("SUPABASE_SECRET_KEY=" + secretKey);
|
||||
' > "$tmpdir/output"
|
||||
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
|
||||
echo ""
|
||||
echo "SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY}"
|
||||
echo "SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY}"
|
||||
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..."
|
||||
|
||||
for var in SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY; 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
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Custom entrypoint for Kong that builds Lua expressions for request-transformer
|
||||
# and performs environment variable substitution in the declarative config.
|
||||
|
||||
# Build Lua expressions for translating opaque API keys to asymmetric JWTs.
|
||||
# When opaque keys are not configured (empty env vars), expressions fall through
|
||||
# to legacy-only behavior - just passing apikey as-is.
|
||||
#
|
||||
# Full expression logic (when opaque keys are configured):
|
||||
# 1. If Authorization header exists and is NOT an sb_ key -> pass through (user session JWT)
|
||||
# 2. If apikey matches secret key -> set service_role asymmetric JWT internal "API key"
|
||||
# 3. If apikey matches publishable key -> set anon asymmetric JWT internal "API key"
|
||||
# 4. Fallback: pass apikey as-is (legacy HS256 JWT)
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# Opaque keys configured -> full translation expressions
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)"
|
||||
|
||||
# Realtime WebSocket: reads from query_params.apikey (supabase-js sends apikey
|
||||
# via query string), outputs to x-api-key header which Realtime checks first.
|
||||
export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)"
|
||||
else
|
||||
# Legacy API keys, not sb_ API keys -> pass apikey through unchanged
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)"
|
||||
export LUA_RT_WS_EXPR="\$(query_params.apikey)"
|
||||
fi
|
||||
|
||||
# Substitute environment variables in the Kong declarative config.
|
||||
# Uses awk instead of eval/echo to preserve YAML quoting (eval strips double
|
||||
# quotes, breaking "Header: value" patterns that YAML parses as mappings).
|
||||
awk '{
|
||||
result = ""
|
||||
rest = $0
|
||||
while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) {
|
||||
varname = substr(rest, RSTART + 1, RLENGTH - 1)
|
||||
if (varname in ENVIRON) {
|
||||
result = result substr(rest, 1, RSTART - 1) ENVIRON[varname]
|
||||
} else {
|
||||
result = result substr(rest, 1, RSTART + RLENGTH - 1)
|
||||
}
|
||||
rest = substr(rest, RSTART + RLENGTH)
|
||||
}
|
||||
print result rest
|
||||
}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
# Remove empty key-auth credentials (unconfigured opaque keys)
|
||||
sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
exec /entrypoint.sh kong docker-start
|
||||
@@ -9,9 +9,11 @@ consumers:
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_ANON_KEY
|
||||
- key: $SUPABASE_PUBLISHABLE_KEY
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_SERVICE_KEY
|
||||
- key: $SUPABASE_SECRET_KEY
|
||||
|
||||
###
|
||||
### Access Control List
|
||||
@@ -36,6 +38,7 @@ basicauth_credentials:
|
||||
services:
|
||||
## Open Auth routes
|
||||
- name: auth-v1-open
|
||||
_comment: 'Auth: /auth/v1/verify* -> http://auth:9999/verify*'
|
||||
url: http://auth:9999/verify
|
||||
routes:
|
||||
- name: auth-v1-open
|
||||
@@ -45,6 +48,7 @@ services:
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-callback
|
||||
_comment: 'Auth: /auth/v1/callback* -> http://auth:9999/callback*'
|
||||
url: http://auth:9999/callback
|
||||
routes:
|
||||
- name: auth-v1-open-callback
|
||||
@@ -54,6 +58,7 @@ services:
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-authorize
|
||||
_comment: 'Auth: /auth/v1/authorize* -> http://auth:9999/authorize*'
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
@@ -62,10 +67,20 @@ services:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-jwks
|
||||
_comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://auth:9999/.well-known/jwks.json'
|
||||
url: http://auth:9999/.well-known/jwks.json
|
||||
routes:
|
||||
- name: auth-v1-open-jwks
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/.well-known/jwks.json
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Secure Auth routes
|
||||
- name: auth-v1
|
||||
_comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
|
||||
_comment: 'Auth: /auth/v1/* -> http://auth:9999/*'
|
||||
url: http://auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
@@ -77,6 +92,14 @@ services:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
@@ -84,7 +107,7 @@ services:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure REST routes
|
||||
## Secure PostgREST routes
|
||||
- name: rest-v1
|
||||
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||
url: http://rest:3000/
|
||||
@@ -97,7 +120,15 @@ services:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
@@ -118,12 +149,16 @@ services:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- Content-Profile:graphql_public
|
||||
- "Content-Profile: graphql_public"
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
@@ -146,14 +181,23 @@ services:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "x-api-key:$LUA_RT_WS_EXPR"
|
||||
replace:
|
||||
querystring:
|
||||
- "apikey:$LUA_RT_WS_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
protocol: http
|
||||
routes:
|
||||
@@ -166,13 +210,30 @@ services:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
## Storage routes: the storage server manages its own auth
|
||||
|
||||
## Storage API endpoint (with Authorization header transformation).
|
||||
## No key-auth — S3 protocol requests don't carry an apikey header.
|
||||
##
|
||||
## The request-transformer translates opaque API keys to asymmetric JWTs
|
||||
## and passes through existing Authorization headers (user JWTs, AWS SigV4).
|
||||
## When no Authorization or apikey header is present (S3 presigned URLs),
|
||||
## the Lua expression evaluates to nil which Kong renders as empty string.
|
||||
## The post-function strips this empty header so Storage's S3 signature
|
||||
## verification falls through to query-parameter parsing.
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||
url: http://storage:5000/
|
||||
@@ -183,11 +244,28 @@ services:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: post-function
|
||||
config:
|
||||
access:
|
||||
- |
|
||||
local auth = kong.request.get_header("authorization")
|
||||
if auth == nil or auth == "" or auth:find("^%s*$") then
|
||||
kong.service.request.clear_header("authorization")
|
||||
end
|
||||
|
||||
## Edge Functions routes
|
||||
- name: functions-v1
|
||||
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||
url: http://functions:9000/
|
||||
read_timeout: 150000
|
||||
routes:
|
||||
- name: functions-v1-all
|
||||
strip_path: true
|
||||
@@ -196,6 +274,18 @@ services:
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
- name: well-known-oauth
|
||||
_comment: 'Auth: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server'
|
||||
url: http://auth:9999/.well-known/oauth-authorization-server
|
||||
routes:
|
||||
- name: well-known-oauth
|
||||
strip_path: true
|
||||
paths:
|
||||
- /.well-known/oauth-authorization-server
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Analytics routes
|
||||
## Not used - Studio and Vector talk directly to analytics via Docker networking.
|
||||
## If external access is needed, add routes with key-auth matching Logflare's x-api-key auth.
|
||||
|
||||
Reference in new issue
Block a user