# ADR-0004:使用无 Secret 的分层 Web 入口 - **状态:** accepted - **日期:** 2026-07-12 ## 背景 系统需要同时满足三个约束:浏览器只能访问一个本机 Web 端口;数据库感知 API 不能获得公网出口;React/Nginx 不能因反向代理而加入数据库所在网络。仅把 API 同时接入普通 `edge` 和内部 `data` 网络会获得公网出口;仅把 API 接入 `internal` 网络时,Docker Desktop 又不会可靠发布宿主机端口。 ## 决策 采用四层单机拓扑: ```text browser -> web (Nginx + React; edge + ingress) -> gateway (fixed-origin HTTP proxy; ingress + data) -> api (FastAPI; data only) -> PostgreSQL/pgvector (data only) ``` - `edge` 是普通 bridge,只连接无 Secret 的 `web`,用于宿主机回环端口发布。 - `ingress` 是 `internal` 网络,只连接 `web` 和 `gateway`。 - `data` 是 `internal` 网络,只连接 `gateway`、`api`、迁移和数据库等内部组件。 - 命名的 `egress` 只供显式云模型工具使用;`web`、`gateway`、`api` 均不连接该网络。普通 `edge` bridge 仍会给 Web 一个技术上的默认公网出口,不能把网络名称当作防火墙。 - 只有 `web` 发布 `127.0.0.1:8000`;gateway、API 和数据库不发布端口。 - `web` 与 `gateway` 不挂载数据库或百炼 Secret;gateway 固定上游为 `http://api:8000`,执行请求体上限、头白名单、SSRF 防护和脱敏 502 映射。 - API 只挂载 app 数据库 Secret,以非 root、只读根文件系统运行,并通过真实 socket 探测证明无公网出口。 ## 取舍 多一层 gateway 会增加一个容器和一次本机网络转发,但当前查询耗时主要来自数据库和模型,额外开销相对可忽略。保留应用层 gateway 可以让固定上游、请求大小和错误脱敏形成可单测契约;Nginx 专注静态资源、SPA fallback、安全响应头和 SSE 代理。 在当前 Docker Desktop 上,`edge` 必须保持普通 bridge 才能可靠发布宿主机回环端口;实测 Web 可以从该网络建立公网 TCP 连接。当前接受这一残余风险,因为 Web 无 Secret、无数据网络、无挂载,且 Nginx 只代理固定 gateway。需要入口容器零出网的部署必须额外使用主机防火墙或受控出口策略,重新验收后再收紧本 ADR。 ## 被否决方案 1. **API 直接加入 edge 并发布端口:** 实测仍可访问公网,不满足数据库感知进程无 egress 的要求。 2. **web 直接加入 data 并代理 API:** web 会获得到数据库网络的横向连通性,扩大入口层受攻陷后的范围。 3. **只发布 internal 网络中的 API 端口:** 在当前 Docker Desktop 上没有形成可访问的宿主机端口。 4. **只使用 Nginx、删除 gateway:** 可以减少一跳,但会丢失已经测试的固定上游、请求头白名单和脱敏错误契约;本阶段优先保持安全边界可验证。 ## 验证要求 - Compose 合同测试锁定网络成员、唯一发布端口和 Secret 挂载。 - API 与 gateway 容器公网 TCP 探测必须失败;Web 的 edge 出口能力必须被显式记录,web/gateway 镜像历史和环境不得出现 Key、数据库密码或内部 DSN。 - 浏览器从 Web 入口访问 SPA、`/api`、健康检查与 SSE 时保持同源。 - 每次改变该拓扑都必须更新本 ADR、部署文档并重新执行容器级验收。