add new opaque api keys and new auth to self-hosted

This commit is contained in:
Andrey A. authored and GitHub committed 2026-03-16 12:00:10 +01:00
1 parent ec26943390
commit 6190518640
7 files changed
+526 -48

No files matched your search

+29 -2
View File
@@ -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
View File
@@ -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}
+188
View File
@@ -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
+26 -14
View File
@@ -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..."
+90
View File
@@ -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
+49
View File
@@ -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
+97 -7
View File
@@ -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.