本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2026年09月06日 统计字数: 15563字 阅读时间: 32分钟阅读 本文链接: https://soulteary.com/2026/09/06/phorge-modernization-part-3-stargate-forward-auth-trust-boundary.html ----- # Phorge 现代化改造实战(三):接入 Stargate,把 Forward Auth 的信任边界做完整 本文是“Phorge 现代化改造实战”系列第三篇。前两篇解决容器如何运行、如何稳定运行;这一篇把视角移到入口,处理反向代理、认证网关和 Phorge 本地账号之间的信任传递。 ## 系列导航 1. 从没有官方镜像到 Docker Compose 跑起来:建立最小可运行基线; 2. 改进容器化的七个细节:补齐权限、持久化、依赖和探活; 3. **接入 Stargate:把 Forward Auth 的信任边界做完整**; 4. 拆分 Gorge:无侵入不等于不碰文件。 ## 写在前面 前两篇文章分别解决了两个问题:第一篇让 Phorge 从源码变成一个可以用 Docker Compose 启动的应用;第二篇处理配置持久化、目录权限、进程降权、镜像瘦身和 Healthcheck,让它从“能跑”向“可以长期跑”靠近了一步。 按照原来的计划,这一篇应该开始拆 Web 和 `phd`。但在继续拆进程之前,我先补了另一个实际部署绕不开的能力:让 Phorge 能够运行在 Traefik Forward Auth 后面,并识别网关传递过来的用户身份。 相关代码已经放在重新整理后的 [`2026.09.06-r3`](https://github.com/soulteary/phorge/releases/tag/2026.09.06-r3)。对比上一个版本改动,当前标签包含 3 个提交,改动了 7 个文件,增加 458 行、删除 1 行。完整差异可以在 [r2...r3](https://github.com/soulteary/phorge/compare/2026.09.06-r2...2026.09.06-r3) 中查看。 这次 r3 实现的中间其实返工过一次。 第一版已经把 Provider、Adapter、HTTPS preamble 和 Traefik 叠加编排放进去了,但重新对照 [Stargate](https://github.com/soulteary/stargate) 当前实现、Traefik 的头处理顺序和 Docker Compose 的合并规则后,我发现了两个会直接破坏安全边界的问题:Apache 会把 Traefik 刚注入的可信身份头一起删掉;叠加编排新增的回环端口也不会覆盖基础文件中的公网绑定。 当前 r3 已经修正了这两个问题:身份头在 Traefik 中先清洗、再由 ForwardAuth 写回;Phorge 的宿主机端口则通过 `!reset []` 从合并后的模型里明确移除。Apache 与 Dockerfile 中那组不再需要的 `mod_headers` 改动也一并撤销了。 我个人认为,这段返工的过程很值得记录下来。使用 Forward Auth 不是“加一个认证 URL”这么简单,而是一条有严格先后顺序的信任链。 ## 为什么 Phorge 有账号系统,还要再接一层认证网关 Phorge 本身并不缺登录功能。它有账号、密码、外部认证 Provider、用户权限和审计记录。如果只运行一个 Phorge,直接使用这些能力完全没有问题。 但实际的内网和自托管环境里,Phorge 往往不是唯一的服务。旁边可能还有 Grafana、代码浏览器、文档站、机器人数据后台,以及一批只在团队内部使用的工具。 如果每个服务都各自处理入口认证,会出现一组熟悉的问题: - 登录入口和口令策略各不相同; - 第三方应用未必方便修改认证代码; - 多个子域名之间反复登录; - 想临时下线一个人的访问权,需要逐个系统处理; - 部分服务只需要“先挡在门外”,并不值得接入一套完整 IAM。 我在五月写过一篇 [《Stargate(星空之门):不用改业务代码,给内部服务加一道登录门》](https://soulteary.com/2026/05/30/stargate-lightweight-forward-auth-gateway-for-internal-services.html),当时 Stargate 的定位就是一块放在反向代理与业务服务之间的基础设施胶水:Traefik 先询问 Stargate 当前请求能不能通过,业务应用不必重复实现会话检查。 接入 Phorge 时,需求比保护一个普通内部面板多了一步:**网关不仅要回答“这个请求能不能进”,还要告诉 Phorge“进来的是谁”。** 前者是入口访问控制,后者是身份映射。只有把两者分开,才能保留 Phorge 自己的项目权限、审计、差异评审和账号关联能力。 ## 三个多月后,Stargate 已经不是当时的最小版本 截至 2026 年 9 月 6 日,Stargate 最新正式版本是 [`v1.0.0`](https://github.com/soulteary/stargate/releases/tag/v1.0.0)。五月文章里的整体思路没有变化,但很多内容发生了变化。 | 项目 | 五月文章中的方案 | 当前 `v1.0.0` | | --------------- | --------------------------- | ----------------------------------- | | 镜像 | `soulteary/stargate:latest` | `ghcr.io/soulteary/stargate:v1.0.0` | | 容器端口 | `80` | `8080` | | Forward Auth 地址 | `http://stargate/_auth` | `http://stargate:8080/_auth` | | 基础身份 | 共享口令、`X-Forwarded-User` | 可输出用户 ID、邮箱、姓名、角色、Scope 和 AMR | | 代理信任 | 主要依赖网络拓扑 | `TRUSTED_PROXIES` 显式约束可信代理 | | 跨域会话 | 直接共享会话思路 | 短时、签名、单次使用的交换票据 | | 多实例 | 以单实例为主 | Redis 保存共享 Session 和票据重放状态 | | 健康检查 | 单一健康状态 | `/healthz` 存活、`/readyz` 依赖就绪 | | 身份增强 | 后续规划 | Warden、Herald、TOTP、Step-up 已经落地 | `v1.0.0` 还加强了严格配置校验、结构化审计、Prometheus 指标、容器加固、SBOM、制品证明、Cosign 签名和多架构扫描。正式版之后,主分支又继续补了请求超时传播、Warden 刷新、验证码审计归属和 CI 发布约束,但 Forward Auth 的核心协议地址仍然是 `:8080/_auth`。 口令算法的使用也收紧了,当前版本只把 `plaintext` 保留给本地测试,生产使用 BCrypt,并明确拒绝 MD5 和无盐 SHA-512。升级时可以以 [Stargate 当前 README](https://github.com/soulteary/stargate/blob/main/README.md) 和 [`v1.0.0` 迁移说明](https://github.com/soulteary/stargate/blob/main/docs/zhCN/MIGRATION_V1.md) 为准。 这意味着,在使用的时候,我们把五月文章中的示例原样复制过来已经不够了。尤其是端口、可信代理和身份来源,必须按照 `v1.0.0` 的契约重新配置。 ## 一次请求是怎么完成两层认证的 把 Stargate 和 Phorge 放到一起后,一次未登录请求会经历两层状态:先通过 Stargate 的入口会话,再建立或复用 Phorge 自己的本地账号会话。 ```mermaid sequenceDiagram participant B as 浏览器 participant T as Traefik participant S as Stargate participant P as Phorge B->>T: 请求 code.example.com T->>S: GET /_auth S-->>T: 2xx + X-Auth-* T->>P: 转发请求并注入身份头 P-->>B: 映射本地账号与会话 ``` 如果 Stargate 判断用户尚未认证,它会让请求转到自己的登录页;认证完成后,Traefik 再次调用 `/_auth`,并把 Stargate 响应中的身份头复制到发往 Phorge 的请求里。 r3 中约定了三个 Phorge 真正使用的头: | 请求头 | 在 Phorge 中的用途 | | -------------- | -------------- | | `X-Auth-User` | 外部账号的稳定唯一标识,必填 | | `X-Auth-Email` | 邮箱,并参与初始用户名推导 | | `X-Auth-Name` | 用户显示名称 | Stargate 目前还可以输出 `X-Auth-Scopes`、`X-Auth-Role` 和 `X-Auth-AMR`,但 r3 的 Provider 尚未消费它们。也就是说,Stargate 当前只负责确认身份,用户进入 Phorge 后拥有什么项目权限,仍然由 Phorge 自己管理。 我认为这个边界是合理的。认证网关不应该因为返回了一个 `admin` 字符串,就自动获得 Phorge 的管理员权限。身份同步与权限同步是两件不同的事情。 ## r3 为 Phorge 增加了什么 r3 最终新增 4 个文件,修改 3 个文件: | 类型 | 文件 | 作用 | | --- | ------------------------------------ | ---------------------------------- | | 新增 | `PhabricatorTraefikAuthProvider.php` | 从请求头读取身份,参与登录、注册和账号关联 | | 新增 | `PhutilTraefikAuthAdapter.php` | 把外部身份转换成 Phorge 的 External Account | | 新增 | `support/preamble.proxy-https.php` | 识别代理终止 TLS 后的原始 HTTPS 请求 | | 新增 | `docker-compose.traefik.yml` | 可选的 Traefik 叠加编排 | | 修改 | `src/__phutil_library_map__.php` | 注册 Provider、Adapter 及继承关系 | | 修改 | `.env.example` | 增加 Traefik 相关环境变量 | | 修改 | `DOCKER.md` | 增加部署与安全说明 | 这里少掉的两个文件恰好是这次返工的结果:最终版本不再让 Apache 清洗 `X-Auth-*`,因此 `docker/phorge-apache.conf` 无需改动,Dockerfile 也不必为此启用 `mod_headers`。头的清洗和可信值的注入都留在 Traefik 内完成,执行顺序更清楚。 下面分开看其中几个值得记录的实现细节。 ## Provider 与 Adapter:把外部身份变成 Phorge 账号 Provider 在处理登录请求时读取三个 HTTP 头: ```php $auth_user = AphrontRequest::getHTTPHeader('X-Auth-User'); $auth_email = AphrontRequest::getHTTPHeader('X-Auth-Email'); $auth_name = AphrontRequest::getHTTPHeader('X-Auth-Name'); if (!phutil_nonempty_string($auth_user)) { // 没有稳定身份,不允许继续登录。 } ``` `X-Auth-User` 是整个映射关系的锚点。它应该是不会随着姓名、邮箱或手机号变化的稳定 ID,而不是显示名称。 拿到身份后,Provider 将信息写入 Adapter: ```php $adapter->setAccountID($auth_user); $adapter->setAccountEmail($auth_email ?: null); $adapter->setAccountRealName($auth_name ?: null); $adapter->setAccountName($username); ``` 用户名的推导逻辑优先使用邮箱 `@` 前的部分,移除 Phorge 不接受的字符;结果仍然不合法时,再退回 `X-Auth-User`: ```php private function deriveUsernameFromEmail($auth_email, $auth_user) { if (!phutil_nonempty_string($auth_email)) { return $auth_user; } $at = strpos($auth_email, '@'); $local = ($at !== false) ? substr($auth_email, 0, $at) : $auth_email; $local = preg_replace('/[^a-zA-Z0-9._-]/', '', $local); $local = rtrim($local, '.'); if (phutil_nonempty_string($local) && PhabricatorUser::validateUsername($local)) { return $local; } return $auth_user; } ``` 当然,这个逻辑还有一个小问题,未来遇到 `alice@example.com` 和 `alice@another.example` 的时候,会推导出相同的用户名。以及 External Account 仍然由 `X-Auth-User` 区分,但当第二个用户在注册本地账号时可能遇到用户名冲突。 所以,后续可能还是需要对应用作一些改动,更好的支持多域、多租户环境的用户策略。 ### 别忘了 Phorge 的类映射 迁移功能代码时,我最初以为 Phorge 会在运行时自动发现新增类,不需要额外登记。核对主仓后才发现,这两个类同样出现在 `src/__phutil_library_map__.php` 中。 r3 因此补了两组信息:一组是类名到文件路径的映射,另一组是类的继承关系。 这是 Phabricator/Phorge 项目里很容易遗漏的步骤:复制 PHP 类文件并不代表类已经可以可靠加载。以后再迁移 Provider、Controller 或其他 libphutil 类,类映射都应该成为检查清单的一部分。 ## TLS 在 Traefik 结束,Phorge 为什么还要知道原始协议 Traefik 对外提供 HTTPS,但它转发到 Apache 时通常使用容器网络里的明文 HTTP。于是浏览器认为当前页面是 HTTPS,Phorge 从 `$_SERVER` 里看到的却是 HTTP。 这会影响绝对链接、Cookie、安全检查和重定向,还可能出现“服务端认为是 HTTP、客户端认为是 HTTPS”的告警。 r3 增加了一个 preamble: ```php if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') { $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = 443; } ``` 叠加编排会把它只读挂载为 Phorge 实际加载的 `support/preamble.php`: ```yaml services: phorge: volumes: - ./support/preamble.proxy-https.php:/opt/phorge/phorge/support/preamble.php:ro ``` 这个实现本身很小,但它同样建立在可信代理前提上。任何能够直连后端的人都可以伪造 `X-Forwarded-Proto`。它通常不会直接带来账号冒充,但会影响 Phorge 对请求安全上下文的判断,所以后端隔离仍然不能省略。 ## 叠加编排:默认部署不应该被高级能力拖复杂 r3 没有修改基础 `docker-compose.yml`,而是增加 `docker-compose.traefik.yml`: ```bash docker compose \ -f docker-compose.yml \ -f docker-compose.traefik.yml \ up -d ``` 这样做的好处是:默认用户仍然可以运行最小的 Phorge + MySQL;需要统一入口时,再叠加 Traefik、路由标签和 preamble。即便多种功能代码使用同一个镜像,但部署复杂度则由使用场景决定。 r3 的叠加文件刻意保留了一个通用认证后端占位符: ```yaml - "traefik.http.middlewares.phorge-forwardauth.forwardauth.address=${TRAEFIK_FORWARD_AUTH_ADDRESS:-http://auth-backend:9091/api/verify}" - "traefik.http.middlewares.phorge-forwardauth.forwardauth.authResponseHeaders=${TRAEFIK_AUTH_RESPONSE_HEADERS:-X-Auth-User,X-Auth-Email,X-Auth-Name}" - "traefik.http.middlewares.phorge-forwardauth.forwardauth.trustForwardHeader=true" ``` 这是为了让同一套 Provider 也能接 authelia、oauth2-proxy 等实现,并不只是 Stargate。 当我们选择接入 Stargate `v1.0.0` 时,上面的配置可以这样写: ```env TRAEFIK_FORWARD_AUTH_ADDRESS=http://stargate:8080/_auth TRAEFIK_AUTH_RESPONSE_HEADERS=X-Auth-User,X-Auth-Email,X-Auth-Name ``` ## 第一个兼容边界:Stargate 的共享密码模式没有用户身份 五月文章为了降低第一次使用的门槛,主要演示的是共享密码模式。这种模式仍然有价值:输入正确口令后,Stargate 可以确认当前会话已经认证,Traefik 也会放行请求。 但“会话已认证”不等于“知道这是哪个人”。 Stargate 当前在只有共享密码的会话中,会返回类似下面的状态: ```text X-Forwarded-User: authenticated X-Auth-User: ``` 而 Phorge r3 的 Provider 明确要求 `X-Auth-User` 非空。于是两边会出现一个看似矛盾、实际合理的结果: - Stargate 认为请求已经通过入口认证; - Phorge 认为没有得到可用于建账号的外部身份。 不能简单把 `X-Forwarded-User: authenticated` 当作备用用户 ID。那会让所有使用共享密码的人都映射到同一个 Phorge 外部账号,既无法审计,也存在严重的账号混用风险。 所以,Stargate 与 Phorge 的完整联动需要使用能够产生个人身份的认证路径,例如 Warden 中的用户记录,再由 Herald 提供验证码或 TOTP。成功登录后,Stargate 会在 Session 中保存 `user_id`、邮箱和姓名,并由 `/_auth` 返回对应的 `X-Auth-*` 头。 这里可以把能力分成两档: | Stargate 模式 | 能否保护 Phorge 入口 | 能否映射为独立 Phorge 用户 | | ----------------------- | -------------- | -------------------- | | 共享密码 | 可以 | 不可以,缺少 `X-Auth-User` | | Warden 用户 + Herald/TOTP | 可以 | 可以,能够提供稳定用户 ID | | 可信上游身份头 | 可以 | 可以,但必须配置共享代理密钥与用户查询 | 这也是为什么不能只写“接上 Stargate”五个字。 我们使用的到底是哪一种认证模式,决定了后端拿到的是一个布尔结果,还是一个真正的用户身份。 ## 第二个兼容边界:Stargate `v1.0.0` 不再默认相信转发头 Stargate 现在要求用 `TRUSTED_PROXIES` 声明直接连接它的可信代理。未命中这个范围时,`X-Forwarded-Host`、`X-Forwarded-Proto`、原始 URI 和客户端 IP 都不会被当成可信信息。 这不是额外麻烦,而是身份网关应该有的默认行为。否则任何能够直接请求 Stargate 的客户端,都可以伪造原始域名、协议和来源地址。 Docker 网络如果使用自动分配的网段,容器地址可能随部署环境变化。比较稳妥的方式是为代理网络指定一个明确且不冲突的 CIDR: ```yaml services: traefik: networks: - edge stargate: image: ghcr.io/soulteary/stargate:v1.0.0 environment: AUTH_HOST: auth.example.com CALLBACK_ALLOWED_HOSTS: code.example.com COOKIE_DOMAIN: .example.com PASSWORDS: ${STARGATE_PASSWORDS:?required} TRUSTED_PROXIES: 172.31.30.0/24 SESSION_EXCHANGE_SECRET: ${SESSION_EXCHANGE_SECRET:?required} WARDEN_ENABLED: "true" WARDEN_URL: http://warden:8080 WARDEN_API_KEY: ${WARDEN_API_KEY:?required} HERALD_ENABLED: "true" HERALD_URL: http://herald:8080 HERALD_HMAC_SECRET: ${HERALD_HMAC_SECRET:?required} PORT: 8080 networks: - edge phorge: networks: - default - edge networks: edge: name: phorge-edge ipam: config: - subnet: 172.31.30.0/24 ``` 其中 `phorge` 同时连接两个网络:在 `default` 网络访问 MySQL,在 `edge` 网络接受 Traefik 转发。Stargate 的 Warden、Herald、Redis 等配置没有在这里展开,应该根据实际认证模式放在独立的内部网络中。 如果你的环境已有公共 Traefik 网络,不要为了套示例强行重建。先用 `docker network inspect` 读取实际 CIDR,再把同一个结果用于 `TRUSTED_PROXIES` 和服务网络配置。 r3 目前把 Forward Auth 的 `trustForwardHeader` 设为 `true`。这个选项表示 Traefik 会信任请求中已有的 `X-Forwarded-*`,适合前面还有一层受信任负载均衡器的环境;如果 Traefik 本身就是最外层入口,更稳妥的值是 `false`。因此上线时还要确认一件事:Traefik 前面是否真的存在会覆盖、清洗这些头的可信代理。这里不是 Phorge Provider 的功能开关,而是实际网络拓扑的一部分。 ## 第一次返工:身份头要先删,再由 ForwardAuth 写回 第一版的 Apache 配置包含下面三行: ```apache RequestHeader unset X-Auth-User RequestHeader unset X-Auth-Email RequestHeader unset X-Auth-Name ``` 当时的想法是:Provider 无条件信任 `X-Auth-*`,所以后端应该清掉客户端伪造的同名头。方向没有错,位置错了。 请求到达 Apache 之前,Traefik 已经完成 Forward Auth,并把 Stargate 响应中的 `X-Auth-*` 写入后端请求。[Apache `mod_headers` 文档](https://httpd.apache.org/docs/current/mod/mod_headers.html) 说明,`RequestHeader unset` 会在内容处理器运行前删除相应请求头。Apache 无法判断这个头来自浏览器,还是刚由 Traefik 注入,于是攻击者的假头和认证服务的真头会一起消失。 当前 r3 把清洗前移到了 Traefik,并明确声明中间件顺序: ```yaml services: phorge: labels: - "traefik.http.routers.phorge.middlewares=phorge-strip-auth-headers@docker,phorge-forwardauth@docker" - "traefik.http.middlewares.phorge-strip-auth-headers.headers.customrequestheaders.X-Auth-User=" - "traefik.http.middlewares.phorge-strip-auth-headers.headers.customrequestheaders.X-Auth-Email=" - "traefik.http.middlewares.phorge-strip-auth-headers.headers.customrequestheaders.X-Auth-Name=" - "traefik.http.middlewares.phorge-forwardauth.forwardauth.authResponseHeaders=X-Auth-User,X-Auth-Email,X-Auth-Name" ``` 执行过程现在变成: 1. `phorge-strip-auth-headers` 删除客户端带来的 `X-Auth-*`; 2. `phorge-forwardauth` 调用认证服务; 3. 认证成功后,`authResponseHeaders` 把认证服务返回的可信身份写入后端请求; 4. Apache 原样把请求交给 PHP,Provider 得到最终身份。 中间件顺序不能反过来。先 Forward Auth、后 strip,会把刚写入的可信身份再次删掉。当前 Dockerfile 也只启用原本需要的 `rewrite`,不再为错误位置的清洗额外开启 `mod_headers`。 [Traefik ForwardAuth 文档](https://doc.traefik.io/traefik/v3.1/middlewares/http/forwardauth/) 说明,`authResponseHeaders` 会把认证服务响应中的指定头复制到后端请求,并替换已有冲突值;[Headers Middleware](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/headers/) 支持用空值删除请求头。显式增加 strip 的价值在于:即使认证服务没有返回某个可选头,也不会意外保留客户端自带的同名值。 ## 第二次返工:用 `!reset` 真正收回后端端口 基础 `docker-compose.yml` 已经发布了 Phorge 的端口: ```yaml ports: - "${PHORGE_HTTP_PORT:-8088}:80" ``` 第一版叠加配置试图再声明一个绑定回环地址的端口来覆盖它: ```yaml ports: - "127.0.0.1:${PHORGE_HTTP_PORT:-8088}:80" ``` 但 Compose 合并文件时,`ports` 不是普通标量。按照 [Docker Compose 的合并规则](https://docs.docker.com/reference/compose-file/merge/),端口使用 `{ip, target, published, protocol}` 作为唯一键。基础配置的 IP 为空,叠加配置的 IP 是 `127.0.0.1`,它们不是同一个键,后一条不会覆盖前一条。最终模型可能同时保留两个映射,既没有完成隔离,还可能产生端口绑定冲突。 当前 r3 要求 Docker Compose 2.24.4 或更高版本,并使用 `!reset` 明确清空基础文件的整个端口序列: ```yaml services: phorge: ports: !reset [] ``` Traefik 与 Phorge 在 Docker 网络内直接通信,不需要 Phorge 再发布宿主机端口。需要临时直连排障时,应该另建一个 debug override,显式增加仅绑定回环地址的映射,并在排障后移除。 这个修正也带来一个更准确的验收方法:不能只看 `docker compose config` 是否返回 0,还要检查最终模型中 `phorge.ports` 是否已经为空。语法正确只说明 YAML 能解析,不能说明合并后的语义就是预期结果。 ## TLS 证书仍然交给部署环境 r3 为 Traefik 定义了 `websecure` 入口,也给 Phorge Router 设置了 `tls=true`,但没有替使用者选择 ACME Certificate Resolver,也没有假设证书文件的来源。 这是有意留下的部署边界:测试时 Traefik 可以使用默认生成的证书,浏览器会提示不受信任;生产环境仍然需要按自己的 DNS、证书和入口架构接入 Let's Encrypt、DNS Challenge 或已有证书 Provider。 同时要确保三组域名彼此一致: ```env PHORGE_BASE_URI=https://code.example.com/ TRAEFIK_DOMAIN=code.example.com AUTH_HOST=auth.example.com ``` Phorge 用第一项生成绝对地址并校验 Host;Traefik 用第二项匹配路由;Stargate 用第三项生成认证跳转。任何一处仍然停留在 `phorge.local` 或旧端口,都可能形成重定向循环。 ## 启用 Provider 前,先保留一条管理员恢复路径 类已经进入镜像,并不代表认证方式已经启用。需要先使用现有管理员账号进入: ```text Auth → Auth Providers → Add Authentication Provider → Traefik Auth ``` 然后根据场景开启: - `Allow Login`:允许外部身份登录; - `Allow Registration`:允许首次出现的身份创建 Phorge 账号; - `Allow Linking`:允许现有用户把外部身份关联到自己的账号。 比较安全的上线顺序是: 1. 先用基础 Compose 在回环端口完成初始管理员注册; 2. 配置并验证 Stargate 能返回稳定的 `X-Auth-User`; 3. 添加 Traefik Auth Provider,但暂时保留原登录方式; 4. 用非管理员测试账号完成注册、退出和再次登录; 5. 确认恢复路径有效后,再关闭不需要的认证入口和后端端口。 不要在第一次切换时同时关闭原 Provider、移除直连入口、替换域名和更新 Cookie 配置。认证系统最容易出现的不是单个代码错误,而是多个正确配置组合后把管理员一起关在门外(哈哈)。 还要注意,r3 的登录方式并不是“访问 Phorge 就自动建立 Phorge 会话”。Traefik 会先保证入口已经认证,用户仍需要在 Phorge 登录页点击一次 `Traefik Auth`,由 Provider 完成本地 External Account 的查找或创建。后续有了 Phorge 自己的会话,才会按正常方式访问。 ## 把验证从“页面能打开”升级成一组攻击测试 这类功能只验证正常路径是不够的。至少应该同时测试正常登录、未登录访问、伪造头、后端直连和账号撤销。 ### 1. 检查最终 Compose 模型 ```bash docker compose \ -f docker-compose.yml \ -f docker-compose.traefik.yml \ config > /tmp/phorge-compose.yml ``` 然后确认: - Phorge 没有发布到 `0.0.0.0:8088`; - Traefik、Stargate 和 Phorge 位于同一个 edge 网络; - Forward Auth 地址是 `http://stargate:8080/_auth`; - Phorge Router 上的中间件顺序是先 strip、后 Forward Auth; - `PHORGE_BASE_URI`、Router Host 和证书域名一致。 ### 2. 检查 Stargate 的存活与就绪 ```bash docker compose exec stargate \ wget -q -O - http://127.0.0.1:8080/healthz docker compose exec stargate \ wget -q -O - http://127.0.0.1:8080/readyz ``` `healthz` 只说明进程活着;使用 Warden、Herald 或 Redis 时,要以 `readyz` 判断依赖是否已经可以承接认证流量。 ### 3. 未登录请求必须被拦截 ```bash curl -ksS -o /dev/null -D - https://code.example.com/ ``` 浏览器请求通常会被引导到 Stargate 登录页;明确请求 JSON 的 API 客户端则应该得到 `401`,不能直接进入 Phorge。 ### 4. 伪造身份头不能越过认证 ```bash curl -ksS -o /dev/null -D - \ -H 'X-Auth-User: admin' \ -H 'X-Auth-Email: admin@example.com' \ https://code.example.com/ ``` 在没有 Stargate Session 的情况下,结果仍然应该是跳转或 `401`。假设类似这个请求之后,用户状态直接变成管理员,那么这整套方案必须立即下线。 ### 5. 认证完成后必须得到真实用户 ID 使用 Warden 中的测试用户完成登录,再观察 `/_auth` 的响应头或 Traefik 调试日志。至少应该满足: ```text X-Auth-User: user-123 X-Auth-Email: user@example.com X-Auth-Name: Test User ``` `X-Auth-User` 为空时,即使 Stargate 返回 2xx,也不能进入 Phorge Provider 的注册流程。 ### 6. 后端不能被绕过 Traefik 直连 从另一台机器访问下面的地址应该失败: ```bash curl http://phorge-host.example.com:8088/ ``` 更严格的部署中,宿主机上也不应该存在这个监听端口。网络隔离不是 Apache 头处理的替代品,而是头认证成立的前提。 ### 7. 验证本地账号生命周期 最后再检查这些业务行为: - 第一次点击 Traefik Auth 能否创建普通账号; - 第二次登录能否命中同一个 External Account; - 邮箱本地部分冲突时如何处理; - Stargate Session 失效后是否重新进入认证; - Warden 禁用用户后,已有会话是否按预期撤销; - Phorge 管理员是否仍保留可用的恢复方式。 只有这些检查都通过,“Forward Auth 已接入”才不只是一条能通过 `docker compose config` 的 YAML。 ## 当前 r3 完成了什么,还保留了哪些边界 重新整理后的 r3 已经把这条认证链需要的通用能力闭合了: 1. Provider、Adapter 与类映射负责把稳定的外部 ID 关联到 Phorge 本地账号; 2. preamble 让 TLS 在代理层终止时,Phorge 仍能识别原始 HTTPS; 3. 可选叠加编排不影响原来的最小启动方式; 4. 客户端身份头在 ForwardAuth 之前清洗,认证服务返回的可信值可以完整到达 PHP; 5. `ports: !reset []` 从合并模型中移除后端映射,对外只留下 Traefik; 6. 文档写明了 Provider 启用方式、中间件顺序与后端不可直连的安全前提。 它没有试图把每个认证系统都塞进 Phorge 仓库。`TRAEFIK_FORWARD_AUTH_ADDRESS` 仍然是通用占位符;使用 Stargate 时要改为 `http://stargate:8080/_auth`,并在 Stargate 一侧配置能够产生个人身份的 Warden 用户、`TRUSTED_PROXIES`,以及实际需要的 Herald、TOTP、Redis 等组件。 还有几项更适合留给后续迭代:把 `trustForwardHeader` 做成可配置项;增加真正走完 Traefik → Stargate → Phorge 的自动化集成测试;为多域用户名冲突设计策略;根据需要决定是否把 Stargate 的角色和 Scope 映射进 Phorge。它们不阻断当前的基础接入,但会决定这套方案能否在更复杂的生产拓扑里长期维护。 这次返工最值得保留的经验,是测试边界要覆盖组件之间的“接缝”。 PHP 文件能加载、Compose 能解析、Traefik 能调用认证地址,都不能单独证明可信身份最终到达了 Provider,也不能证明后端端口真的从宿主机消失。跨组件功能必须沿着整条请求链验收。 ## 最后 五月写 Stargate 时,我把它形容成给内部服务安装的一道门。现在把这道门接到 Phorge 前面,才更明显地看到:Forward Auth 的核心从来不只是一个 `/_auth` 地址,而是一份横跨浏览器、Traefik、认证服务和业务应用的信任契约。 Traefik 必须知道哪些响应头可以写入后端请求;Stargate 必须知道哪个代理值得信任;Phorge 必须把稳定外部 ID 映射到本地账号;网络层还必须保证任何人都不能绕过这条链路。 重新整理后的 r3 不只让 Phorge 能理解来自网关的身份,也把最关键的两处信任边界落实到了默认配置中:假身份在认证之前被清掉,后端端口在叠加部署里不再对宿主机发布。 下一篇继续讨论另一个维度的边界:我们把 Phorge 的模块进行拆分,如何同时让一个持续跟进活跃上游的 fork 尽量少背冲突债务。 --EOF