Cloudflare Access JWT 映射(Portal 可选)
Portico 不是身份提供者,也不托管登录界面。Portal 在显式启用时,可以把 Cloudflare Access 签发的 JWT 映射到本地名册上的人类身份。这不是第二套可写身份,也不会签发 pst1_ 会话。
CLI、Gateway 与 MCP 继续只认 Portico 会话。GitHub 登录与邮箱 One-time PIN 如果要存在,只存在于 Cloudflare 边缘,不进入本仓库。
何时启用
默认关闭。未设置 PORTICO_CF_ACCESS_ENABLED,或缺少合法 team / audience 时,现有 CLI 会话路径不变,JWT 被忽略。
同时满足下列条件才启用:
| 变量 | 作用 | 示例(占位符) |
|---|---|---|
PORTICO_CF_ACCESS_ENABLED | 打开映射。仅 true / yes / on / 1 视为开启 | true |
PORTICO_CF_ACCESS_TEAM | Access team 名,只允许 [a-z0-9-],用于拼 ISS 与默认 JWKS | example |
PORTICO_CF_ACCESS_AUD | Application audience,防止跨应用重放 | example-audience |
PORTICO_CF_ACCESS_JWKS_URL | 可选覆盖。缺省为该 team 的 Cloudflare certs URL | http://127.0.0.1:9/certs(仅测试) |
example team 对应的 ISS 是 https://example.cloudflareaccess.com。JWKS 覆盖只允许:
- 测试用
http://127.0.0.1/... - 该 team 的
https://<team>.cloudflareaccess.com/cdn-cgi/access/certs
其它 URL、缺 team、缺 audience:功能保持关闭,而不是 500。
真实 team 名与 AUD 只在安装现场注入,不要写进本仓库、CI 或示例实值。
名册绑定
外部登录成功不等于拥有特权。JWT 里的 email 必须命中名册中的人类身份;Agent 不能带邮箱。
deno task cli -- identity grant \
--identities ./data/identities.json \
--sessions ./data/sessions.json \
--session "$AUDITOR_SESSION" \
--id human:reader \
--kind human \
--role reader \
--email reader@example.invalid
邮箱会大小写归一。reader@example.invalid 只是文档占位符。邮箱不是第二身份证明:没有合法 JWT 或会话时,它不会让请求变成已登录。
Portal 如何判定身份
- 已有 Portico 会话(
Authorization: Bearer或X-Portico-Session)优先。 - 无会话且功能已启用时,读取
Cf-Access-Jwt-Assertion,校验 RS256 签名、aud、iss、exp,再用已校验 email 查名册。 - 明文
Cf-Access-Authenticated-User-Email不是证明,单独携带该头仍是匿名。 - 都没有或任何一步失败:匿名。
/internal返回 HTML 404,不泄漏目录。
成功或失败都不写 identities.json / sessions.json,不签发会话。Portal 进程仍然没有 --allow-write。
这是 fail-closed:伪造签名、过期、AUD 不匹配、JWKS 不可达、名册未登记,全部降为匿名,而不是变成 500 打开内部面。
边缘 IdP(不在本仓库)
GitHub 组织登录与邮箱 One-time PIN 在 Cloudflare Access identity providers 配置。Client Secret 与 OTP 投递留在 Cloudflare,不要复制进 Portico、环境示例或审计日志。
JWT 校验口径见 Validating JSON Web Tokens。
明确不做
- 不在 Portico 实现 GitHub OAuth 客户端、邮箱 OTP 或本地 SMTP。
- 不把 Cloudflare Tunnel /
cloudflared配置、隧道 token 或反代模板收进本仓库。那是边缘基础设施。 - 不把 JWT 扩到 CLI、Gateway 或 MCP。
- 不把未审批对象变成公开可达。匿名与失败映射看到的仍是公开面。