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 uses an Envoy-based API gateway by default. 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.

使用网关 #

🌐 Using the gateway

Envoy 是默认的 API 网关,启动堆栈时会自动运行——不需要额外配置。

🌐 Envoy is the default API gateway and runs automatically when you start the stack - no extra configuration is required.

Envoy 被注册为 api-gw 服务,同时还将 envoy 和 kong 作为网络别名,以帮助从旧配置过渡。

🌐 Envoy is registered as the api-gw service and also exposes envoy and kong as network aliases to help transition from legacy configurations.

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

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

curl -i -H "apikey: your-supabase-secret-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:

Client
│
▼
Listener (port 8000)
│
▼
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
├─ RBAC (global: service_role → /pg/, apikey → other API routes;
│ per-route DENY override on /mcp and Realtime
│ /api/tenants, /api/openapi)
└─ Router
│
▼
Upstream clusters
├─ auth (auth:9999)
├─ rest (rest:3000)
├─ realtime (realtime-dev.supabase-realtime:4000)
├─ storage (storage:5000)
├─ functions (functions:9000)
├─ meta (meta:8080)
└─ 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_KEY、SUPABASE_PUBLISHABLE_KEY、ANON_KEY_ASYMMETRIC 和 SERVICE_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/功能/sb_ 键拒绝无效和冲突的 sb_ 键;其他通过;闲置超时 160 秒,上游响应超时 410 秒
/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 头

/functions/v1/ 路由会在 volumes/api/envoy/lds.template.yaml 中将 idle_timeout 设置为 160s,并将 timeout 设置为 410s。这样可以让运行时的 150 秒请求空闲超时和 400 秒工作线程生命周期限制先到期。有关运行时设置,请参见 内存或超时错误。

🌐 The /functions/v1/ route sets idle_timeout to 160s and timeout to 410s in volumes/api/envoy/lds.template.yaml. These allow the runtime's 150-second request idle timeout and 400-second worker lifetime limit to expire first. See Memory or timeout errors for the runtime settings.

身份验证 #

🌐 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_USERNAME 和 DASHBOARD_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_KEY 和 SERVICE_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 过滤器会将不透明密钥转换为相应的预签名内部 JWT,然后请求才会到达 API 密钥执行和上游服务。在 /functions/v1/ 上会跳过这个链,它有自己的过滤器。参见 Edge Functions。

🌐 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 chain is skipped on /functions/v1/, which has its own filter. See Edge Functions.

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

🌐 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.

边缘函数 #

🌐 Edge Functions

在 /functions/v1/ 上,一个专用的 Lua 过滤器处理 API 密钥,而不会更改 apikey 或 Authorization 头。当配置了新的 API 密钥时,它的工作方式如下:

🌐 On /functions/v1/, a dedicated Lua filter handles API keys without changing the apikey or Authorization headers. When the new API keys are configured, it works as follows:

请求结果
没有 apikey 或 Authorization直接通过
apikey 是 sb_publishable_* 或 sb_secret_*通过,但 sb-api-key 设置为内部 JWT
Authorization: Bearer sb_... 并且没有 apikey同上,使用 Authorization 的密钥
非 sb_ 值(旧版 API 密钥、用户会话令牌或其他任意值)直接通过
未知 sb_ 密钥拒绝,并返回 401 Unauthorized
apikey 和 Authorization: Bearer sb_... 包含不同的密钥拒绝,并返回 401 Unauthorized

这个过滤器总是会移除客户端发送的任何 sb-api-key 头。没有配置新的 API 密钥时,所有其他请求都会照常通过。sb-api-key 的值是原始 JWT,没有 Bearer 前缀。这个路由不会读取 apikey 查询参数。

🌐 The filter always removes any sb-api-key header sent by the client. Without the new API keys configured, all other requests pass through unchanged. The sb-api-key value is the raw JWT, without a Bearer prefix. The apikey query parameter is not read on this route.

当 FUNCTIONS_VERIFY_JWT 是 true 时,函数服务会在 Authorization 中验证 JWT。如果 Authorization 缺失或包含 sb_ 键,它会改为验证 sb-api-key。在请求到达你的函数之前,它会移除 sb-api-key。函数也可以自己检查 API 密钥,例如使用 @supabase/server 的 withSupabase。参见 自托管边缘函数。

🌐 When FUNCTIONS_VERIFY_JWT is true, the functions service verifies the JWT in Authorization. If Authorization is missing or contains an sb_ key, it verifies sb-api-key instead. It removes sb-api-key before the request reaches your function. Functions can also check API keys themselves, for example with withSupabase from @supabase/server. See Self-hosted Edge Functions.

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

🌐 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 根据连接自动设置(http 或 https)
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.

/pg/ 路由(通过 meta 直接进行 Postgres 内省,由服务角色密钥控制)是个例外:它有自己更严格的每路由 CORS 覆盖,仅限 SUPABASE_PUBLIC_URL 和 localhost/127.0.0.1 来源,因为没有合理的理由让这个路由被任意第三方来源调用。

🌐 The /pg/ route (direct Postgres introspection via meta, gated by the service role key) is the exception: it carries its own stricter per-route CORS override, restricted to SUPABASE_PUBLIC_URL and localhost/127.0.0.1 origins, since there's no legitimate reason for this route to be called from an arbitrary third-party origin.

安全加固 #

🌐 Security hardening

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

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

设置目的
normalize_path: true、merge_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 只能从容器内部访问,其他容器或宿主机无法访问

自定义配置 #

🌐 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.yml 中的 api-gw 服务(以便将变量传入容器)和 docker-entrypoint.sh(用 sed 进行替换)。
  • 正在应用更改。 Envoy 会从文件系统读取渲染后的 lds.yaml。配置更改需要重启容器,以便入口点重新渲染模板:
sh run.sh restart api-gw

管理界面 #

🌐 Admin interface

Envoy 在容器内部的 127.0.0.1:9901 上暴露了一个管理接口,有像 /ready、/clusters、/stats 和 /config_dump 这样的端点。标准的 envoyproxy/envoy 镜像是最小化的(没有 curl 或 wget),所以没有添加工具的话,管理员端点无法通过 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:

docker run --rm --network container:supabase-envoy curlimages/curl http://127.0.0.1:9901/clusters
docker run --rm --network container:supabase-envoy curlimages/curl http://127.0.0.1:9901/stats
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:

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_KEY、SERVICE_ROLE_KEY、SUPABASE_PUBLISHABLE_KEY 或 SUPABASE_SECRET_KEY 之一。注意,只有当新的密钥配置完全设置好时,才接受 SUPABASE_PUBLISHABLE_KEY 和 SUPABASE_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=true 和 STORAGE_PUBLIC_URL。存储会使用网关发送的 X-Forwarded-Prefix 头来重建原始请求路径,以进行 SigV4 验证。
  • 带下划线的 400 Bad Request 头部。 headers_with_underscores_action: REJECT_REQUEST 已启用。一些客户端发送像 X_Forwarded_For 这样的带下划线的头部;这些会被拒绝。头部名称请使用连字符。

另请参阅 #

🌐 See also