Skip to content
Self-Hosting

Envoy API 网关

Architecture and configuration of the Envoy API gateway for self-hosted Supabase.

自托管的 Supabase 配有一个可选的基于 Envoy 的 API 网关。它接收来自客户端的请求,将它们路由到内部服务(Auth、PostgREST、Realtime、Storage、Edge Functions、postgres-meta、Studio),并通过将不透明的 sb_ 密钥转换为这些服务使用的内部凭证来强制执行 API 密钥认证。

🌐 Self-hosted Supabase ships with an optional Envoy-based API gateway. It accepts incoming client requests, routes them to internal services (Auth, PostgREST, Realtime, Storage, Edge Functions, postgres-meta, Studio), and enforces API key authentication by translating opaque sb_ keys into the internal credentials used by those services.

本指南向想要了解或自定义 Envoy 网关的运维人员介绍其架构、配置布局和安全状态。这不是 Envoy 教程——有关过滤器、路由和集群的参考,请查看 Envoy 文档

🌐 This guide explains the architecture, configuration layout, and security posture of the Envoy gateway for operators who want to understand or customize it. It is not an Envoy tutorial - for reference on filters, routes, and clusters, see the Envoy documentation.

在你开始之前 #

🌐 Before you begin

启用 Envoy 网关 #

🌐 Enabling the Envoy gateway

Envoy 网关作为 Docker Compose 的覆盖配置提供。

🌐 The Envoy gateway is provided as a Docker Compose override.

启用 Envoy 覆盖,然后启动堆栈:

🌐 Enable the Envoy override, then start the stack:

1
sh run.sh config add envoy
2
sh run.sh start

这个覆盖会禁用默认的 Kong 网关,并在同一个端口(默认 8000)启动 Envoy。它还会通过依赖重新配置 Functions 服务,让它等待 Envoy。

🌐 The override disables the default Kong gateway and starts Envoy on the same port (default 8000). It also reconfigures the Functions service to wait for Envoy via a dependency.

Envoy 被注册为 api-gw 服务,并且还将 kong 作为网络别名暴露;基础的 Kong 服务同样暴露 api-gw。任意主机名都会解析到当前活跃的网关,因此那些硬编码了 kong:8000 的内部配置(例如在 Edge Functions 或 Studio 中)依然可以正常工作,无需修改。

🌐 Envoy is registered as the api-gw service and also exposes kong as a network alias; the base Kong service likewise exposes api-gw. Either hostname resolves to whichever gateway is currently active, so internal configs that hardcode kong:8000 (for example, in Edge Functions or Studio) keep working without changes.

验证 #

🌐 Verify

确认网关正在路由请求并执行 API 密钥:

🌐 Confirm the gateway is routing requests and enforcing API keys:

1
curl -i -H "apikey: your-service-role-key" http://<your-domain>/rest/v1/

来自 PostgREST 的 200 OK 响应确认网关已启动。一个没有 apikey 头的 401 Unauthorized 确认执行已激活。

🌐 A 200 OK response from PostgREST confirms the gateway is up. A 401 Unauthorized without the apikey header confirms enforcement is active.

架构 #

🌐 Architecture

Envoy作为一个单容器(supabase-envoy)运行,在端口8000上有一个监听器。每个传入请求都会先经过一个按顺序排列的HTTP过滤器链,然后才被转发到上游集群:

🌐 Envoy runs as a single container (supabase-envoy) with one listener on port 8000. Every incoming request passes through an ordered chain of HTTP filters before being forwarded to an upstream cluster:

1
Client
2
3
4
Listener (port 8000)
5
6
7
HTTP filter chain
8
├─ CORS
9
├─ Basic Auth (dashboard only)
10
├─ Lua: copy ?apikey query to header
11
├─ Lua: 401 for missing/invalid API key
12
├─ Lua: translate opaque keys in query
13
├─ Lua: translate opaque keys in header
14
├─ Lua: mirror apikey to x-api-key (Realtime WS)
15
├─ Lua: synthesize Authorization header
16
├─ RBAC (global: service_role → /pg/, apikey → other API routes;
17
│ per-route DENY override on /mcp and Realtime
18
│ /api/tenants, /api/openapi)
19
└─ Router
20
21
22
Upstream clusters
23
├─ auth (auth:9999)
24
├─ rest (rest:3000)
25
├─ realtime (realtime-dev.supabase-realtime:4000)
26
├─ storage (storage:5000)
27
├─ functions (functions:9000)
28
├─ meta (meta:8080)
29
└─ studio (studio:3000)

路由会按照监听器配置中声明的顺序进行匹配。每条路由都会选择一个上游集群,重写请求路径前缀,并且可以覆盖过滤器的行为(例如,在 API 路由上禁用基本认证,或者在 MCP 路由上拒绝所有流量)。

🌐 Routes are matched in the order declared in the listener config. Each route selects an upstream cluster, rewrites the request path prefix, and can override filter behavior (for example, disabling basic auth on API routes or denying all traffic on MCP routes).

配置文件结构 #

🌐 Configuration file structure

所有 Envoy 配置都存放在 ./volumes/api/envoy/(相对于包含 docker-compose.yml 的目录):

🌐 All Envoy configuration lives in ./volumes/api/envoy/ (relative to the directory containing docker-compose.yml):

文件作用
envoy.yaml引导配置。将 Envoy 指向 CDS 和 LDS 文件,配置管理界面,并对下游连接设置过载管理器限制。
cds.yaml集群发现服务 - 上游服务定义(DNS、端口、健康检查、连接超时、熔断器)。
lds.template.yaml监听器发现服务模板。定义监听器、过滤器链、路由、RBAC 策略和 CORS 策略。包含密钥和凭证的占位符。
docker-entrypoint.sh在容器启动时,将 LDS 模板渲染到 lds.yaml,通过替换环境变量,计算 DASHBOARD_PASSWORD 的 SHA1+base64 哈希用于基本认证,然后启动 Envoy。

启动时配置是如何呈现的 #

🌐 How the configuration is rendered at startup

Envoy 本身不能直接在配置中读取环境变量。入口脚本会在启动 Envoy 之前,用 sed 将环境特定的值渲染到监听器配置中:

🌐 Envoy cannot natively read environment variables inside its config. The entrypoint script renders environment-specific values into the listener config with sed before launching Envoy:

  1. DASHBOARD_PASSWORD 计算 SHA1+base64 哈希,然后将其与 DASHBOARD_USERNAME 拼接成一个 DASHBOARD_BASIC_AUTH 字符串,格式为 Envoy 的 basic_auth 过滤器所期望的 username:{SHA}<base64-encoded-sha1> 格式。不支持其他哈希格式。
  2. 接着将以下变量代入 lds.template.yaml,结果写入 lds.yaml
变量用途
ANON_KEY旧版 HS256 匿名 JWT(API 密钥验证,RBAC)
SERVICE_ROLE_KEY旧版 HS256 service_role JWT(API 密钥验证,RBAC)
SUPABASE_PUBLISHABLE_KEY不透明的 sb_publishable_* 密钥(翻译源)
SUPABASE_SECRET_KEY不透明的 sb_secret_* 密钥(翻译源)
ANON_KEY_ASYMMETRIC预签名 ES256 匿名 JWT(翻译目标;仅内部使用)
SERVICE_ROLE_KEY_ASYMMETRIC预签名 ES256 service_role JWT(翻译目标;仅内部使用)
DASHBOARD_BASIC_AUTH仪表板基础认证凭证(在步骤 1 中计算)
  1. 如果 SUPABASE_SECRET_KEYSUPABASE_PUBLISHABLE_KEYANON_KEY_ASYMMETRICSERVICE_ROLE_KEY_ASYMMETRIC 都设置了,就会启用不透明密钥翻译。否则,Envoy 会以仅支持传统模式运行,只接受传统的 HS256 密钥。入口点会在启动时把当前模式打印到容器日志里。

路线 #

🌐 Routes

路由会按声明的顺序匹配。第一个匹配的前缀优先。受保护的路由需要有效的 apikey 头;开放路由无需 API 密钥验证即可通过。

🌐 Routes are matched in the order declared. The first matching prefix wins. Protected routes require a valid apikey header; open routes pass through without API key validation.

路径前缀上游路径重写访问控制备注
/auth/v1/verify认证/verify开放邮箱验证
/auth/v1/callback认证/callback开放OAuth 回调
/auth/v1/authorize认证/authorize开放OAuth 授权
/auth/v1/.well-known/jwks.json认证/.well-known/jwks.json开放用于第三方验证的 JWKS
/.well-known/oauth-authorization-server认证-开放OAuth 2.0 授权服务器元数据 (RFC 8414)
/auth/v1/sso/saml/acs认证/sso/saml/acs开放SAML 断言消费者
/auth/v1/sso/saml/metadata认证/sso/saml/metadata开放SAML 元数据
/functions/v1/functions/绕过Edge Functions 运行时执行自己的 JWT 验证;150 秒超时
/storage/v1/存储/绕过存储执行它自己的授权
/auth/v1/认证/API 密钥受保护的认证端点
/rest/v1/剩余/API 密钥PostgREST 开放 API 根(需要密钥)
/rest/v1/*剩余/API 密钥PostgREST 数据端点
/graphql/v1剩余/rpc/graphqlAPI 密钥pg_graphql(添加 Content-Profile: graphql_public
/realtime/v1/api/tenants实时-拒绝实时管理 API(默认阻止)
/realtime/v1/api/openapi实时-拒绝实时 OpenAPI 规范(默认被阻止)
/realtime/v1/api实时/apiAPI 密钥实时 REST API(广播、通道、ping)
/realtime/v1/实时/socket/API 密钥实时 WebSocket
/pg/元数据/API 密钥postgres-meta / Studio 数据库操作(需要密钥)
/api/mcp工作室-拒绝MCP 端点(默认通过 RBAC 拒绝阻止)
/mcp工作室/api/mcp拒绝MCP 端点(默认通过 RBAC 拒绝阻止)
/(通用)工作室-基本认证仪表板;会去掉入站的 Authorization

身份验证 #

🌐 Authentication

网关处理三个与认证相关的步骤:在通用路由上进行仪表板基本认证、在受保护路由上执行 API 密钥验证,以及在执行前进行不透明到内部密钥的转换步骤。

🌐 The gateway handles three authentication-related steps: dashboard basic auth on the catch-all route, API key enforcement on protected routes, and an opaque-to-internal key translation step that runs before enforcement.

仪表板基本认证 #

🌐 Dashboard basic auth

通配的 / 路由需要使用来自 DASHBOARD_USERNAMEDASHBOARD_PASSWORD 的 HTTP 基本认证。Authorization 头会在请求转发到 Studio 之前被移除(不管是什么方案),所以基本认证的凭据永远不会到达上游服务。

🌐 The catch-all / route requires HTTP basic auth with credentials from DASHBOARD_USERNAME and DASHBOARD_PASSWORD. The Authorization header is stripped (regardless of scheme) before the request is forwarded to Studio, so the basic auth credentials never reach the upstream service.

在受保护的路由上强制使用 API 密钥 #

🌐 API key enforcement on protected routes

受保护的路由(/auth/v1//rest/v1//graphql/v1/realtime/v1/api/realtime/v1//pg/)需要 apikey 头包含一个有效的配置密钥。可接受的密钥是新的不透明 sb_publishable_*sb_secret_* 密钥(转换为内部 JWT)或旧版的 ANON_KEYSERVICE_ROLE_KEY HS256 JWT API 密钥。

🌐 The protected routes (/auth/v1/, /rest/v1/, /graphql/v1, /realtime/v1/api, /realtime/v1/, /pg/) require the apikey header to contain a valid configured key. Acceptable keys are the new opaque sb_publishable_* and sb_secret_* keys (translated to internal JWTs) or the legacy ANON_KEY and SERVICE_ROLE_KEY HS256 JWT API keys.

一个 Lua 过滤器会用 HTTP 401 Unauthorized 拒绝缺失或无效的键。然后,RBAC 过滤器会应用更精细的规则:

🌐 A Lua filter rejects missing or invalid keys with HTTP 401 Unauthorized. An RBAC filter then applies finer-grained rules:

  • /pg/ - 只允许使用 service_role 键(sb_secret_* 或旧版 SERVICE_ROLE_KEY
  • /rest/v1/(确切路径,OpenAPI 架构根)- 只允许使用 service_role
  • /rest/v1/<table> 和其他数据 API 路径对所有有效密钥仍然可访问

不透明密钥翻译 #

🌐 Opaque key translation

当配置了新的 API 密钥(sb_publishable_*sb_secret_*)时,一系列 Lua 过滤器会在请求到达 API 密钥强制执行和上游服务之前,将不透明的密钥转换为对应的预签名内部 JWT。/functions/v1/ 上整个链会被跳过:Edge Runtime 会原样接收原始的 apikeyAuthorization 头。

🌐 When the new API keys (sb_publishable_*, sb_secret_*) are configured, a chain of Lua filters translates opaque keys into the corresponding pre-signed internal JWTs before the request reaches API key enforcement and upstream services. The entire chain is skipped on /functions/v1/: the Edge Runtime receives the original apikey and Authorization headers unchanged.

这个链条按这个顺序运行:

🌐 The chain operates in this order:

  1. 查询参数复制。 如果请求包含 ?apikey=... 但没有 apikey 头部,该值会被复制到 apikey 头部。这可以统一仅使用查询参数的客户端(例如来自浏览器的 Realtime WebSocket 连接,因为自定义头部不可用)。
  2. 查询参数翻译。 如果 apikey 查询参数包含一个不透明的密钥,它会被替换——无论是在 URL 中还是在 apikey 头中——为对应的预签名内部 JWT。
  3. 头部翻译。 如果 apikey 头部包含一个不透明的密钥,它会被对应的预签名内部 JWT 替换。
  4. x-api-key 镜像。 在实时 WebSocket 路由上,apikey 值会被复制到 x-api-key 头中(Realtime 首先读取它用于 WebSocket 认证)。
  5. Authorization 合成。 如果客户端在 Authorization 头中没有发送真实的 JWT(或者只发送了 Bearer sb_* 值,这不是有效的 JWT),网关会从(可能已翻译的)apikey 头合成 Authorization: Bearer <apikey>。在实时 WebSocket 路由上会跳过这一步,该路由使用的是 x-api-key

有关不透明密钥与非对称密钥的背景信息,请参见 新 API 密钥与非对称认证

🌐 For background on opaque vs asymmetric keys, see New API Keys and Asymmetric Authentication.

转发的头信息和跨域资源共享 #

🌐 Forwarded headers and CORS

X-Forwarded 头 #

🌐 X-Forwarded headers

Envoy 会把转发的头信息附加到每个上游请求上,这样下游服务就可以重建最初面向客户端的 URL。

🌐 Envoy attaches forwarded headers to every upstream request so downstream services can reconstruct the original client-facing URL.

头部
X-Forwarded-Host客户端的 Host 头部(如果尚未存在则添加)
X-Forwarded-Port监听器端口(8000
X-Forwarded-Proto由 Envoy 根据连接自动设置(httphttps
X-Forwarded-Prefix按路由设置为匹配的路径前缀(例如,Storage 的 /storage/v1
X-Forwarded-For自动设置,因为监听器上启用了 use_remote_address: true

跨域资源共享 #

🌐 CORS

网关在虚拟主机层面应用了宽松的 CORS 策略:

🌐 The gateway applies a permissive CORS policy at the virtual-host level:

  • 允许所有来源
  • 方法:GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, CONNECT, TRACE
  • 允许所有请求和响应头
  • 预检最大存活时间:3600 秒

这与当前的 Supabase 平台行为以及之前基于 Kong 的网关相匹配。Supabase API 的认证边界是 apikey 头,而不是请求来源。

🌐 This matches both the current Supabase platform behavior and the previous Kong-based gateway. The auth boundary for Supabase APIs is the apikey header rather than the request origin.

安全加固 #

🌐 Security hardening

网关已按这些面向生产的设置进行了配置:

🌐 The gateway is configured with these production-oriented settings:

设置目的
normalize_path: truemerge_slashes: true防止绕过 RBAC 前缀规则导致的路径混淆
path_with_escaped_slashes_action: REJECT_REQUEST拒绝路径中包含 URL 编码斜杠的请求
use_remote_address: true将 Envoy 视为边缘代理:使用对等连接 IP 作为受信任的客户端地址(而不是信任客户端提供的 X-Forwarded-For),并删除不受信任的 x-envoy-* 请求头
headers_with_underscores_action: REJECT_REQUEST阻止利用下划线与连字符规范化进行的标题走私攻击
per_connection_buffer_limit_bytes: 32768将每个连接的缓冲区内存限制为 32 KiB
max_active_downstream_connections: 30000下游连接总数的过载管理限制
管理界面绑定到 127.0.0.1:9901管理 API 只能从容器内部访问,不能从其他容器或主机访问
镶贴到 envoyproxy/envoy:v1.37.2(或更新版本)的镜像包含 Envoy 1.37.x 已发布的安全补丁

自定义配置 #

🌐 Customizing the configuration

所有路由、过滤器和集群的更改都在 ./volumes/api/envoy/ 下的 YAML 文件中进行。

🌐 All routing, filter, and cluster changes are made in the YAML files under ./volumes/api/envoy/.

  • 添加或修改路由。 编辑 lds.template.yaml。路由是有顺序的——将新路由放在通配 / 路由之前以确保匹配。保留每个路由的 basic_auth: disabled 用于 API 路由,如果某条路由需要绕过全局策略,请设置适当的 RBAC 覆盖。
  • 添加一个新的上游服务。cds.yaml 中添加一个集群定义,包含服务的 DNS 名称和端口,然后在路由的 cluster: 字段中引用它。
  • 添加一个新的环境变量。 模板中的占位符使用 ${VAR_NAME} 形式。如果你添加了一个占位符,需要同时更新 docker-compose.envoy.yml(将变量传入容器)和 docker-entrypoint.sh(用 sed 替换它)。
  • 正在应用更改。 Envoy 会从文件系统读取渲染后的 lds.yaml。配置更改需要重启容器,以便入口点重新渲染模板:
1
sh run.sh restart api-gw

管理界面 #

🌐 Admin interface

Envoy 在容器内部的 127.0.0.1:9901 上暴露了一个管理接口,有像 /ready/clusters/stats/config_dump 这样的端点。标准的 envoyproxy/envoy 镜像是最小化的(没有 curlwget),所以没有添加工具的话,管理员端点无法通过 docker exec 访问。

🌐 Envoy exposes an admin interface on 127.0.0.1:9901 inside the container, with endpoints like /ready, /clusters, /stats, and /config_dump. The standard envoyproxy/envoy image is minimal (no curl or wget), so the admin endpoints are not reachable via docker exec without adding tooling.

在调试时,你可以通过运行一个短期存在的 curl 容器来查询管理界面,该容器会加入与 supabase-envoy 相同的网络命名空间:

🌐 You can query the admin interface during debugging by running a short-lived curl container that joins the same network namespace as supabase-envoy:

1
docker run --rm --network container:supabase-envoy curlimages/curl http://127.0.0.1:9901/clusters
1
docker run --rm --network container:supabase-envoy curlimages/curl http://127.0.0.1:9901/stats
1
docker run --rm --network container:supabase-envoy curlimages/curl http://127.0.0.1:9901/config_dump

故障排除 #

🌐 Troubleshooting

日志 #

🌐 Logs

Envoy 会将访问日志和过滤器输出写入 stdout。可以这样查看它们:

🌐 Envoy writes access logs and filter output to stdout. View them with:

1
docker compose logs api-gw

访问日志格式是一个标准的组合日志,包含请求方法、原始路径、响应代码和发送的字节数。

🌐 The access log format is a standard combined log with the request method, original path, response code, and bytes sent.

常见问题 #

🌐 Common issues

  • 401 Unauthorized 在受保护的路由上。 apikey 头缺失或与任何配置的密钥不匹配。请确认头的值完全匹配你在 .env 文件中的 ANON_KEYSERVICE_ROLE_KEYSUPABASE_PUBLISHABLE_KEYSUPABASE_SECRET_KEY 之一。注意,只有当新的密钥配置完全设置好时,才接受 SUPABASE_PUBLISHABLE_KEYSUPABASE_SECRET_KEY —— 详细信息见 新 API 密钥和非对称认证
  • 403 Forbidden/pg/ /pg/ 路由需要 service_role 密钥(SUPABASE_SECRET_KEY 或旧版 SERVICE_ROLE_KEY)。匿名和可发布密钥会被拒绝。
  • 403 Forbidden/api/mcp/mcp 上。 这些路径默认是阻塞的。请参阅 启用 MCP 服务器访问
  • 403 Forbidden/realtime/v1/api/tenants/realtime/v1/api/openapi 上。 这些实时管理端点在网关处被设计性阻止,外部客户端即使有有效密钥也无法访问。
  • SignatureDoesNotMatch 在 S3 上发起对存储的请求。 确认 docker-compose.yml 中的存储服务配置包含了 REQUEST_ALLOW_X_FORWARDED_PATH=trueSTORAGE_PUBLIC_URL。存储会使用网关发送的 X-Forwarded-Prefix 头来重建原始请求路径,以进行 SigV4 验证。
  • 带下划线的 400 Bad Request 头部。 headers_with_underscores_action: REJECT_REQUEST 已启用。一些客户端发送像 X_Forwarded_For 这样的带下划线的头部;这些会被拒绝。头部名称请使用连字符。

另请参阅 #

🌐 See also