From 3525fe61592cefc9dbeb886e1372a808613ea615 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:37:12 +0100 Subject: [PATCH] chore: initial import of envoy test config --- docker/docker-compose.envoy.yml | 42 ++ docker/volumes/api/envoy/cds.yaml | 160 ++++++++ docker/volumes/api/envoy/docker-entrypoint.sh | 21 + docker/volumes/api/envoy/envoy.yaml | 24 ++ docker/volumes/api/envoy/lds.yaml.template | 374 ++++++++++++++++++ docker/volumes/logs/vector.yml | 2 +- 6 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 docker/docker-compose.envoy.yml create mode 100644 docker/volumes/api/envoy/cds.yaml create mode 100755 docker/volumes/api/envoy/docker-entrypoint.sh create mode 100644 docker/volumes/api/envoy/envoy.yaml create mode 100644 docker/volumes/api/envoy/lds.yaml.template diff --git a/docker/docker-compose.envoy.yml b/docker/docker-compose.envoy.yml new file mode 100644 index 00000000000..be8430486c0 --- /dev/null +++ b/docker/docker-compose.envoy.yml @@ -0,0 +1,42 @@ +# Envoy override for Kong +# Usage: docker compose -f docker-compose.yml -f docker-compose.envoy.yml up + +services: + # Disable the original Kong service + kong: + profiles: + - disabled + + # Envoy API gateway + api: + container_name: supabase-envoy + image: envoyproxy/envoy:v1.28.0 + restart: unless-stopped + ports: + - ${KONG_HTTP_PORT}:8000/tcp + - ${KONG_HTTPS_PORT:-8443}:8443/tcp + - 9901:9901/tcp + volumes: + - ./volumes/api/envoy/envoy.yaml:/etc/envoy/envoy.yaml:ro + - ./volumes/api/envoy/cds.yaml:/etc/envoy/cds.yaml:ro + - ./volumes/api/envoy/lds.yaml.template:/etc/envoy/lds.yaml.template:ro + - ./volumes/api/envoy/docker-entrypoint.sh:/docker-entrypoint.sh:ro + depends_on: + analytics: + condition: service_healthy + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + entrypoint: ["/bin/sh", "/docker-entrypoint.sh"] + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:9901/ready"] + interval: 10s + timeout: 5s + retries: 3 + networks: + default: + aliases: + - kong + - envoy diff --git a/docker/volumes/api/envoy/cds.yaml b/docker/volumes/api/envoy/cds.yaml new file mode 100644 index 00000000000..da9d3333d78 --- /dev/null +++ b/docker/volumes/api/envoy/cds.yaml @@ -0,0 +1,160 @@ +resources: + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: auth + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: auth + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: auth + port_value: 9999 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: rest + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: rest + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: rest + port_value: 3000 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: realtime + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: realtime + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: realtime-dev.supabase-realtime + port_value: 4000 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: storage + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: storage + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: storage + port_value: 5000 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: functions + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: functions + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: functions + port_value: 9000 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: analytics + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: analytics + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: analytics + port_value: 4000 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: meta + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: meta + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: meta + port_value: 8080 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 + + - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster + name: studio + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: studio + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: studio + port_value: 3000 + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 10000 + max_pending_requests: 10000 + max_requests: 10000 diff --git a/docker/volumes/api/envoy/docker-entrypoint.sh b/docker/volumes/api/envoy/docker-entrypoint.sh new file mode 100755 index 00000000000..1169a5ec42d --- /dev/null +++ b/docker/volumes/api/envoy/docker-entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +# Generate base64 encoded basic auth credentials for dashboard +# Format: username:password -> base64 +DASHBOARD_BASIC_AUTH=$(printf '%s' "${DASHBOARD_USERNAME}:${DASHBOARD_PASSWORD}" | base64 | tr -d '\n') + +echo "Generating Envoy configuration..." + +# Process the lds.yaml template with environment variables using sed +# Using | as delimiter since JWT tokens contain / +sed -e "s|\${ANON_KEY}|${ANON_KEY}|g" \ + -e "s|\${SERVICE_ROLE_KEY}|${SERVICE_ROLE_KEY}|g" \ + -e "s|\${DASHBOARD_BASIC_AUTH}|${DASHBOARD_BASIC_AUTH}|g" \ + /etc/envoy/lds.yaml.template > /etc/envoy/lds.yaml + +echo "Envoy configuration generated successfully" +echo "Starting Envoy..." + +# Start Envoy +exec envoy -c /etc/envoy/envoy.yaml "$@" diff --git a/docker/volumes/api/envoy/envoy.yaml b/docker/volumes/api/envoy/envoy.yaml new file mode 100644 index 00000000000..57fa3ce4f0b --- /dev/null +++ b/docker/volumes/api/envoy/envoy.yaml @@ -0,0 +1,24 @@ +dynamic_resources: + cds_config: + path_config_source: + path: /etc/envoy/cds.yaml + resource_api_version: V3 + lds_config: + 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 + typed_config: + '@type': >- + type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig + max_active_downstream_connections: 30000 +admin: + address: + socket_address: + address: 0.0.0.0 + port_value: 9901 diff --git a/docker/volumes/api/envoy/lds.yaml.template b/docker/volumes/api/envoy/lds.yaml.template new file mode 100644 index 00000000000..b9e3e9655bc --- /dev/null +++ b/docker/volumes/api/envoy/lds.yaml.template @@ -0,0 +1,374 @@ +resources: + - '@type': type.googleapis.com/envoy.config.listener.v3.Listener + name: http_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + upgrade_configs: + - upgrade_type: websocket + access_log: + - name: envoy.access_loggers.stdout + typed_config: + '@type': type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + 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" + http_filters: + # CORS filter + - name: envoy.filters.http.cors + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors + + # RBAC filter for API key validation + - name: envoy.filters.http.rbac + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: DENY + policies: + # Deny requests without valid API key to protected routes + api_key_required: + permissions: + - and_rules: + rules: + # Match protected paths that need API key + - or_rules: + rules: + - url_path: + path: + prefix: /auth/v1/ + - url_path: + path: + prefix: /rest/v1/ + - url_path: + path: + prefix: /graphql/v1 + - url_path: + path: + prefix: /realtime/v1/ + - url_path: + path: + prefix: /pg/ + # Exclude open auth routes + - not_rule: + or_rules: + rules: + - url_path: + path: + exact: /auth/v1/verify + - url_path: + path: + prefix: /auth/v1/verify? + - url_path: + path: + exact: /auth/v1/callback + - url_path: + path: + prefix: /auth/v1/callback? + - url_path: + path: + exact: /auth/v1/authorize + - url_path: + path: + prefix: /auth/v1/authorize? + principals: + # Deny if no valid API key + - not_id: + or_ids: + ids: + - header: + name: apikey + string_match: + exact: "${ANON_KEY}" + - header: + name: apikey + string_match: + exact: "${SERVICE_ROLE_KEY}" + - header: + name: Authorization + string_match: + prefix: "Bearer ${ANON_KEY}" + - header: + name: Authorization + string_match: + prefix: "Bearer ${SERVICE_ROLE_KEY}" + # /pg/ requires service_role only + pg_service_role_only: + permissions: + - url_path: + path: + prefix: /pg/ + principals: + - not_id: + or_ids: + ids: + - header: + name: apikey + string_match: + exact: "${SERVICE_ROLE_KEY}" + - header: + name: Authorization + string_match: + prefix: "Bearer ${SERVICE_ROLE_KEY}" + + # Lua filter for removing apikey from forwarded requests where hide_credentials is needed + - name: envoy.filters.http.lua + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + default_source_code: + inline_string: | + function envoy_on_request(request_handle) + end + source_codes: + hide_apikey: + inline_string: | + function envoy_on_request(request_handle) + request_handle:headers():remove("apikey") + -- Also strip apikey from query string + local path = request_handle:headers():get(":path") + if path then + path = path:gsub("&apikey=[^&]*", ""):gsub("%?apikey=[^&]*&", "?"):gsub("%?apikey=[^&]*$", "") + request_handle:headers():replace(":path", path) + end + end + add_graphql_header: + inline_string: | + function envoy_on_request(request_handle) + request_handle:headers():add("Content-Profile", "graphql_public") + request_handle:headers():remove("apikey") + local path = request_handle:headers():get(":path") + if path then + path = path:gsub("&apikey=[^&]*", ""):gsub("%?apikey=[^&]*&", "?"):gsub("%?apikey=[^&]*$", "") + request_handle:headers():replace(":path", path) + end + end + + # Router filter + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + route_config: + name: local_route + virtual_hosts: + - name: supabase_services + domains: + - "*" + typed_per_filter_config: + envoy.filters.http.cors: + '@type': type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy + allow_origin_string_match: + - safe_regex: + regex: ".*" + allow_methods: GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS,TRACE,CONNECT + allow_headers: "*" + expose_headers: "*" + max_age: "3600" + allow_credentials: true + routes: + # Block /api/mcp + - match: + prefix: /api/mcp + direct_response: + status: 403 + body: + inline_string: "Access is forbidden." + + # Block /mcp + - match: + prefix: /mcp + direct_response: + status: 403 + body: + inline_string: "Access is forbidden." + + # Open Auth routes (no API key required) + - match: + path: /auth/v1/verify + route: + cluster: auth + prefix_rewrite: /verify + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + - match: + prefix: /auth/v1/verify + route: + cluster: auth + regex_rewrite: + pattern: + regex: ^/auth/v1/verify + substitution: /verify + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + - match: + path: /auth/v1/callback + route: + cluster: auth + prefix_rewrite: /callback + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + - match: + prefix: /auth/v1/callback + route: + cluster: auth + regex_rewrite: + pattern: + regex: ^/auth/v1/callback + substitution: /callback + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + - match: + path: /auth/v1/authorize + route: + cluster: auth + prefix_rewrite: /authorize + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + - match: + prefix: /auth/v1/authorize + route: + cluster: auth + regex_rewrite: + pattern: + regex: ^/auth/v1/authorize + substitution: /authorize + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + # Secure Auth routes (API key required, credentials NOT hidden) + - match: + prefix: /auth/v1/ + route: + cluster: auth + prefix_rewrite: / + timeout: 30s + + # REST routes (API key required, credentials hidden) + - match: + prefix: /rest/v1/ + route: + cluster: rest + prefix_rewrite: / + timeout: 120s + typed_per_filter_config: + envoy.filters.http.lua: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute + name: hide_apikey + + # GraphQL routes (API key required, add Content-Profile header) + - match: + prefix: /graphql/v1 + route: + cluster: rest + prefix_rewrite: /rpc/graphql + timeout: 120s + typed_per_filter_config: + envoy.filters.http.lua: + '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute + name: add_graphql_header + + # Realtime WebSocket routes + - match: + prefix: /realtime/v1/ + route: + cluster: realtime + prefix_rewrite: /socket/ + timeout: 0s + upgrade_configs: + - upgrade_type: websocket + + # Realtime REST API + - match: + prefix: /realtime/v1/api + route: + cluster: realtime + prefix_rewrite: /api + timeout: 30s + + # Storage routes (no API key required - storage handles its own auth) + - match: + prefix: /storage/v1/ + route: + cluster: storage + prefix_rewrite: / + timeout: 300s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + # Edge Functions routes (no API key required - functions handle their own auth) + - match: + prefix: /functions/v1/ + route: + cluster: functions + prefix_rewrite: / + timeout: 300s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + # Analytics routes (no API key required) + - match: + prefix: /analytics/v1/ + route: + cluster: analytics + prefix_rewrite: / + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + + # pg-meta routes (service_role only) + - match: + prefix: /pg/ + route: + cluster: meta + prefix_rewrite: / + timeout: 30s + + # Studio/Dashboard (catch-all with basic auth via RBAC) + - match: + prefix: / + route: + cluster: studio + timeout: 30s + typed_per_filter_config: + envoy.filters.http.rbac: + '@type': type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBACPerRoute + rbac: + rules: + action: DENY + policies: + require_basic_auth: + permissions: + - any: true + principals: + - not_id: + header: + name: authorization + string_match: + exact: "Basic ${DASHBOARD_BASIC_AUTH}" diff --git a/docker/volumes/logs/vector.yml b/docker/volumes/logs/vector.yml index e79d7e82685..3a4cbf93797 100644 --- a/docker/volumes/logs/vector.yml +++ b/docker/volumes/logs/vector.yml @@ -30,7 +30,7 @@ transforms: inputs: - project_logs route: - kong: '.appname == "supabase-kong"' + kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"' auth: '.appname == "supabase-auth"' rest: '.appname == "supabase-rest"' realtime: '.appname == "realtime-dev.supabase-realtime"'