本文是“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。对比上一个版本改动,当前标签包含 3 个提交,改动了 7 个文件,增加 458 行、删除 1 行。完整差异可以在 r2…r3 中查看。

这次 r3 实现的中间其实返工过一次。

第一版已经把 Provider、Adapter、HTTPS preamble 和 Traefik 叠加编排放进去了,但重新对照 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(星空之门):不用改业务代码,给内部服务加一道登录门》,当时 Stargate 的定位就是一块放在反向代理与业务服务之间的基础设施胶水:Traefik 先询问 Stargate 当前请求能不能通过,业务应用不必重复实现会话检查。

接入 Phorge 时,需求比保护一个普通内部面板多了一步:网关不仅要回答“这个请求能不能进”,还要告诉 Phorge“进来的是谁”。

前者是入口访问控制,后者是身份映射。只有把两者分开,才能保留 Phorge 自己的项目权限、审计、差异评审和账号关联能力。

三个多月后,Stargate 已经不是当时的最小版本

截至 2026 年 9 月 6 日,Stargate 最新正式版本是 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 当前 READMEv1.0.0 迁移说明 为准。

这意味着,在使用的时候,我们把五月文章中的示例原样复制过来已经不够了。尤其是端口、可信代理和身份来源,必须按照 v1.0.0 的契约重新配置。

一次请求是怎么完成两层认证的

把 Stargate 和 Phorge 放到一起后,一次未登录请求会经历两层状态:先通过 Stargate 的入口会话,再建立或复用 Phorge 自己的本地账号会话。

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-ScopesX-Auth-RoleX-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 头:

$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:

$adapter->setAccountID($auth_user);
$adapter->setAccountEmail($auth_email ?: null);
$adapter->setAccountRealName($auth_name ?: null);
$adapter->setAccountName($username);

用户名的推导逻辑优先使用邮箱 @ 前的部分,移除 Phorge 不接受的字符;结果仍然不合法时,再退回 X-Auth-User

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.comalice@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:

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

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

docker compose \
  -f docker-compose.yml \
  -f docker-compose.traefik.yml \
  up -d

这样做的好处是:默认用户仍然可以运行最小的 Phorge + MySQL;需要统一入口时,再叠加 Traefik、路由标签和 preamble。即便多种功能代码使用同一个镜像,但部署复杂度则由使用场景决定。

r3 的叠加文件刻意保留了一个通用认证后端占位符:

- "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 时,上面的配置可以这样写:

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 当前在只有共享密码的会话中,会返回类似下面的状态:

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-HostX-Forwarded-Proto、原始 URI 和客户端 IP 都不会被当成可信信息。

这不是额外麻烦,而是身份网关应该有的默认行为。否则任何能够直接请求 Stargate 的客户端,都可以伪造原始域名、协议和来源地址。

Docker 网络如果使用自动分配的网段,容器地址可能随部署环境变化。比较稳妥的方式是为代理网络指定一个明确且不冲突的 CIDR:

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 配置包含下面三行:

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 文档 说明,RequestHeader unset 会在内容处理器运行前删除相应请求头。Apache 无法判断这个头来自浏览器,还是刚由 Traefik 注入,于是攻击者的假头和认证服务的真头会一起消失。

当前 r3 把清洗前移到了 Traefik,并明确声明中间件顺序:

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 文档 说明,authResponseHeaders 会把认证服务响应中的指定头复制到后端请求,并替换已有冲突值;Headers Middleware 支持用空值删除请求头。显式增加 strip 的价值在于:即使认证服务没有返回某个可选头,也不会意外保留客户端自带的同名值。

第二次返工:用 !reset 真正收回后端端口

基础 docker-compose.yml 已经发布了 Phorge 的端口:

ports:
  - "${PHORGE_HTTP_PORT:-8088}:80"

第一版叠加配置试图再声明一个绑定回环地址的端口来覆盖它:

ports:
  - "127.0.0.1:${PHORGE_HTTP_PORT:-8088}:80"

但 Compose 合并文件时,ports 不是普通标量。按照 Docker Compose 的合并规则,端口使用 {ip, target, published, protocol} 作为唯一键。基础配置的 IP 为空,叠加配置的 IP 是 127.0.0.1,它们不是同一个键,后一条不会覆盖前一条。最终模型可能同时保留两个映射,既没有完成隔离,还可能产生端口绑定冲突。

当前 r3 要求 Docker Compose 2.24.4 或更高版本,并使用 !reset 明确清空基础文件的整个端口序列:

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。

同时要确保三组域名彼此一致:

PHORGE_BASE_URI=https://code.example.com/
TRAEFIK_DOMAIN=code.example.com
AUTH_HOST=auth.example.com

Phorge 用第一项生成绝对地址并校验 Host;Traefik 用第二项匹配路由;Stargate 用第三项生成认证跳转。任何一处仍然停留在 phorge.local 或旧端口,都可能形成重定向循环。

启用 Provider 前,先保留一条管理员恢复路径

类已经进入镜像,并不代表认证方式已经启用。需要先使用现有管理员账号进入:

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 模型

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 的存活与就绪

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. 未登录请求必须被拦截

curl -ksS -o /dev/null -D - https://code.example.com/

浏览器请求通常会被引导到 Stargate 登录页;明确请求 JSON 的 API 客户端则应该得到 401,不能直接进入 Phorge。

4. 伪造身份头不能越过认证

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 调试日志。至少应该满足:

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 直连

从另一台机器访问下面的地址应该失败:

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