mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(self-hosted): block access to tenants and openapi realtime api (#46856)
This commit is contained in:
1 parent
9bf5bbcc75
commit
098157eb90
7 files changed
+154
-44
No files matched your search
@@ -214,15 +214,17 @@ When new API keys have not been added yet, the `kong-entrypoint.sh` script remov
|
||||
|
||||
To assist with the authorization flows a specialized configuration in `kong.yml` substitutes internal, gateway-level-only pre-signed JWTs for `sb_publishable` and `sb_secret` API keys. These pre-signed JWTs are also auto-configured in `.env` but **should not** be used in any application code.
|
||||
|
||||
| Route | Service | API key required | Header substitution |
|
||||
| -------------------- | -------------------- | ---------------- | ------------------- |
|
||||
| `/auth/v1/*` | Auth | Yes | `Authorization` |
|
||||
| `/rest/v1/*` | PostgREST | Yes | `Authorization` |
|
||||
| `/graphql/v1` | PostgREST | Yes | `Authorization` |
|
||||
| `/realtime/v1/api/*` | Realtime (REST) | Yes | `Authorization` |
|
||||
| `/realtime/v1/*` | Realtime (WebSocket) | Yes | `x-api-key` |
|
||||
| `/storage/v1/*` | Storage | No | `Authorization` |
|
||||
| `/functions/v1/*` | Edge Functions | No | - |
|
||||
| Route | Service | API key required | Header substitution |
|
||||
| -------------------------- | -------------------- | ---------------- | ------------------- |
|
||||
| `/auth/v1/*` | Auth | Yes | `Authorization` |
|
||||
| `/rest/v1/*` | PostgREST | Yes | `Authorization` |
|
||||
| `/graphql/v1` | PostgREST | Yes | `Authorization` |
|
||||
| `/realtime/v1/api/tenants` | Realtime (REST) | Denied (blocked) | - |
|
||||
| `/realtime/v1/api/openapi` | Realtime (REST) | Denied (blocked) | - |
|
||||
| `/realtime/v1/api/*` | Realtime (REST) | Yes | `Authorization` |
|
||||
| `/realtime/v1/*` | Realtime (WebSocket) | Yes | `x-api-key` |
|
||||
| `/storage/v1/*` | Storage | No | `Authorization` |
|
||||
| `/functions/v1/*` | Edge Functions | No | - |
|
||||
|
||||
### Request flows
|
||||
|
||||
|
||||
@@ -60,13 +60,14 @@ HTTP filter chain
|
||||
├─ CORS
|
||||
├─ Basic Auth (dashboard only)
|
||||
├─ Lua: copy ?apikey query to header
|
||||
├─ Lua: 401 for missing/invalid API key
|
||||
├─ Lua: translate opaque keys in query
|
||||
├─ Lua: translate opaque keys in header
|
||||
├─ Lua: mirror apikey to x-api-key (Realtime WS)
|
||||
├─ Lua: synthesize Authorization header
|
||||
├─ Lua: 401 for missing/invalid API key
|
||||
├─ RBAC (global: service_role → /pg/, apikey → other API routes;
|
||||
│ per-route DENY override on /mcp)
|
||||
│ per-route DENY override on /mcp and Realtime
|
||||
│ /api/tenants, /api/openapi)
|
||||
└─ Router
|
||||
│
|
||||
▼
|
||||
@@ -140,7 +141,9 @@ Routes are matched in the order declared. The first matching prefix wins. Protec
|
||||
| `/auth/v1/` | auth | `/` | API key | Protected Auth endpoints |
|
||||
| `/rest/v1/` | rest | `/` | API key | PostgREST |
|
||||
| `/graphql/v1` | rest | `/rpc/graphql` | API key | pg_graphql (adds `Content-Profile: graphql_public`) |
|
||||
| `/realtime/v1/api` | realtime | `/api` | API key | Realtime REST API |
|
||||
| `/realtime/v1/api/tenants` | realtime | - | Denied | Realtime management API (blocked by default) |
|
||||
| `/realtime/v1/api/openapi` | realtime | - | Denied | Realtime OpenAPI spec (blocked by default) |
|
||||
| `/realtime/v1/api` | realtime | `/api` | API key | Realtime REST API (broadcast, channels, ping) |
|
||||
| `/realtime/v1/` | realtime | `/socket/` | API key | Realtime WebSocket |
|
||||
| `/pg/` | meta | `/` | Service role only | postgres-meta - used by Studio for database access |
|
||||
| `/api/mcp` | studio | - | Denied | MCP endpoint (blocked by default via RBAC DENY) |
|
||||
@@ -308,6 +311,7 @@ The access log format is a standard combined log with the request method, origin
|
||||
- **`401 Unauthorized` on a protected route.** The `apikey` header is missing or does not match any configured key. Verify that the header value exactly matches one of `ANON_KEY`, `SERVICE_ROLE_KEY`, `SUPABASE_PUBLISHABLE_KEY`, or `SUPABASE_SECRET_KEY` in your `.env` file. Note that `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` are only accepted when the new key configuration is fully set up - see [New API Keys and Asymmetric Authentication](/docs/guides/self-hosting/self-hosted-auth-keys).
|
||||
- **`403 Forbidden` on `/pg/`.** The `/pg/` route requires a service_role key (`SUPABASE_SECRET_KEY` or legacy `SERVICE_ROLE_KEY`). Anon and publishable keys are rejected.
|
||||
- **`403 Forbidden` on `/api/mcp` or `/mcp`.** These routes are blocked by default. See [Enabling MCP Server Access](/docs/guides/self-hosting/enable-mcp).
|
||||
- **`403 Forbidden` on `/realtime/v1/api/tenants` or `/realtime/v1/api/openapi`.** These Realtime management endpoints are blocked at the gateway by design and are not reachable by external clients, even with a valid key.
|
||||
- **`SignatureDoesNotMatch` on S3 requests to Storage.** Verify that the Storage service configuration in `docker-compose.yml` contains `REQUEST_ALLOW_X_FORWARDED_PATH=true` and `STORAGE_PUBLIC_URL`. Storage uses the `X-Forwarded-Prefix` header the gateway sends to reconstruct the original request path for SigV4 verification.
|
||||
- **`400 Bad Request` with underscore headers.** `headers_with_underscores_action: REJECT_REQUEST` is enabled. Some clients send headers like `X_Forwarded_For` with underscores; these are rejected. Use hyphens in header names.
|
||||
|
||||
|
||||
@@ -144,17 +144,23 @@ check "No key -> 401" "401" \
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime REST (/realtime/v1/api/) ---"
|
||||
# Realtime REST API - expect 200 or other non-401 response with valid key
|
||||
check "Legacy ANON_KEY -> not 401" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $ANON_KEY")" != "401" ] && echo true || echo false)"
|
||||
# Realtime REST API - use /api/ping to verify key auth (expect 200 with a valid key)
|
||||
check "Legacy ANON_KEY -> 200" "200" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping" -H "apikey: $ANON_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY -> not 401" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")" != "401" ] && echo true || echo false)"
|
||||
check "New PUBLISHABLE_KEY -> 200" "200" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/tenants")"
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping")"
|
||||
|
||||
# Management endpoints must be blocked at the gateway (even with a valid key)
|
||||
check "/api/tenants blocked -> 403" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $ANON_KEY")"
|
||||
check "/api/openapi blocked -> 403" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/openapi" -H "apikey: $ANON_KEY")"
|
||||
|
||||
echo ""
|
||||
echo "--- supabase-js style requests (apikey + Authorization) ---"
|
||||
|
||||
@@ -444,9 +444,17 @@ check "/mcp blocked" "403" \
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime ---"
|
||||
check "Realtime health" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/realtime/v1/api/tenants" \
|
||||
-H "apikey: $ANON_KEY")" != "401" ] && echo true || echo false)"
|
||||
check "Realtime health (ping)" "200" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
|
||||
# Management endpoints must be blocked at the gateway (even with a valid key)
|
||||
check "Realtime /api/tenants blocked" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/tenants" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
check "Realtime /api/openapi blocked" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/openapi" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
|
||||
@@ -7,11 +7,11 @@ dynamic_resources:
|
||||
path_config_source:
|
||||
path: /etc/envoy/lds.yaml
|
||||
resource_api_version: V3
|
||||
|
||||
|
||||
node:
|
||||
cluster: supabase_cluster
|
||||
id: supabase_node
|
||||
|
||||
|
||||
overload_manager:
|
||||
resource_monitors:
|
||||
- name: envoy.resource_monitors.global_downstream_max_connections
|
||||
@@ -19,7 +19,7 @@ overload_manager:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig
|
||||
max_active_downstream_connections: 30000
|
||||
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
|
||||
@@ -2,12 +2,12 @@ resources:
|
||||
- '@type': type.googleapis.com/envoy.config.listener.v3.Listener
|
||||
name: supabase
|
||||
per_connection_buffer_limit_bytes: 32768 # 32 KiB
|
||||
|
||||
|
||||
address:
|
||||
socket_address:
|
||||
address: 0.0.0.0
|
||||
port_value: 8000
|
||||
|
||||
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
@@ -31,7 +31,7 @@ resources:
|
||||
log_format:
|
||||
text_format_source:
|
||||
inline_string: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT% - - [%START_TIME(%d/%b/%Y:%H:%M:%S %z)%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %BYTES_SENT% \"%REQ(REFERER)%\" \"%REQ(USER-AGENT)%\"\n"
|
||||
|
||||
|
||||
route_config:
|
||||
name: supabase_route
|
||||
virtual_hosts:
|
||||
@@ -84,7 +84,7 @@ resources:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/callback
|
||||
route:
|
||||
@@ -113,7 +113,7 @@ resources:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
|
||||
- match:
|
||||
prefix: /auth/v1/authorize
|
||||
route:
|
||||
@@ -255,7 +255,7 @@ resources:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
|
||||
- name: functions-v1-all
|
||||
match:
|
||||
prefix: /functions/v1/
|
||||
@@ -285,7 +285,7 @@ resources:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
|
||||
- match:
|
||||
prefix: /storage/v1/
|
||||
route:
|
||||
@@ -314,7 +314,7 @@ resources:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
|
||||
- name: auth-v1-protected
|
||||
match:
|
||||
prefix: /auth/v1/
|
||||
@@ -332,7 +332,7 @@ resources:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
|
||||
- name: rest-v1-protected
|
||||
match:
|
||||
prefix: /rest/v1/
|
||||
@@ -350,7 +350,7 @@ resources:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
|
||||
- name: graphql-v1-protected
|
||||
match:
|
||||
prefix: /graphql/v1
|
||||
@@ -372,7 +372,65 @@ resources:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
|
||||
- name: realtime-v1-api-openapi-blocked
|
||||
match:
|
||||
prefix: /realtime/v1/api/openapi
|
||||
route:
|
||||
cluster: realtime
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /realtime/v1/api/openapi
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: DENY
|
||||
policies:
|
||||
deny_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- name: realtime-v1-api-tenants-blocked
|
||||
match:
|
||||
prefix: /realtime/v1/api/tenants
|
||||
route:
|
||||
cluster: realtime
|
||||
timeout: 30s
|
||||
request_headers_to_add:
|
||||
- header:
|
||||
key: X-Forwarded-Prefix
|
||||
value: /realtime/v1/api/tenants
|
||||
append_action: ADD_IF_ABSENT
|
||||
typed_per_filter_config:
|
||||
envoy.filters.http.basic_auth:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
envoy.filters.http.rbac:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute
|
||||
rbac:
|
||||
rules:
|
||||
action: DENY
|
||||
policies:
|
||||
deny_all:
|
||||
permissions:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
- name: realtime-v1-api-protected
|
||||
match:
|
||||
prefix: /realtime/v1/api
|
||||
@@ -391,7 +449,7 @@ resources:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
|
||||
- name: realtime-v1-ws-protected
|
||||
match:
|
||||
prefix: /realtime/v1/
|
||||
@@ -410,7 +468,7 @@ resources:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
|
||||
- name: pg-protected
|
||||
match:
|
||||
prefix: /pg/
|
||||
@@ -428,7 +486,7 @@ resources:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.config.route.v3.FilterConfig
|
||||
disabled: true
|
||||
|
||||
|
||||
- match:
|
||||
prefix: /api/mcp
|
||||
route:
|
||||
@@ -456,7 +514,7 @@ resources:
|
||||
- any: true
|
||||
principals:
|
||||
- any: true
|
||||
|
||||
|
||||
- match:
|
||||
prefix: /mcp
|
||||
route:
|
||||
@@ -504,7 +562,7 @@ resources:
|
||||
# - direct_remote_ip:
|
||||
# address_prefix: ::1
|
||||
# prefix_len: 128
|
||||
|
||||
|
||||
- match:
|
||||
prefix: /
|
||||
route:
|
||||
@@ -536,14 +594,14 @@ resources:
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
|
||||
|
||||
|
||||
- name: envoy.filters.http.basic_auth
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth
|
||||
users:
|
||||
inline_string: '${DASHBOARD_BASIC_AUTH}'
|
||||
|
||||
|
||||
# Copies ?apikey=... from the URL into the apikey header when clients omit the header.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
@@ -590,7 +648,7 @@ resources:
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns 401 for missing/invalid API keys on protected API routes.
|
||||
- name: envoy.filters.http.lua
|
||||
typed_config:
|
||||
|
||||
@@ -87,7 +87,7 @@ services:
|
||||
- /sso/saml/acs
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
|
||||
- name: auth-v1-open-sso-metadata
|
||||
url: "http://auth:9999/sso/saml/metadata"
|
||||
routes:
|
||||
@@ -216,6 +216,38 @@ services:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
# Block access to /realtime/v1/api/openapi
|
||||
- name: realtime-v1-rest-openapi
|
||||
_comment: 'Realtime: /realtime/v1/api/openapi/* -> http://realtime:4000/api/openapi/* (blocked)'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api/openapi
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest-openapi
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api/openapi
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
# Block access to /realtime/v1/api/tenants
|
||||
- name: realtime-v1-rest-tenants
|
||||
_comment: 'Realtime: /realtime/v1/api/tenants/* -> http://realtime:4000/api/tenants/* (blocked)'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api/tenants
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest-tenants
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api/tenants
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
|
||||
Reference in new issue
Block a user