oauth2-proxy 与 Pomerium 身份感知入口部署治理手册
一个内部报表站点接入 OIDC 后,正常域名会跳转登录,直接访问上游 Service 却仍返回 200。更糟的是,上游只要看见 X-Auth-Request-Email: admin@example.com 就认为请求来自管理员,于是能绕过代理的客户端也能伪造身份。登录链路本身没有坏,真正失守的是流量拓扑和 Header 信任边界。
oauth2-proxy 与 Pomerium 都能把身份带到旧应用前面,但二者解决问题的深度不同。oauth2-proxy 擅长完成 OAuth/OIDC 登录、恢复会话并以反向代理或 external-auth 方式保护入口;Pomerium 还拥有 Route、Authorize、Databroker 与逐请求策略。两者都不是 API 网关:它们不能替代通用 API 生命周期、配额、协议转换、开发者门户或复杂流量治理,也不该为了“更安全”无目的串联成两次登录。
先按访问决策选择代理模型
如果应用只需要“未登录跳转、登录后把稳定身份交给上游”,oauth2-proxy 的模型更小:一个 provider、OAuth client、redirect URL、cookie 或 Redis session store,以及 upstream 或 /oauth2/auth external-auth 端点。它能按邮箱、域或组做有限过滤,却不是通用关系授权引擎。业务角色仍应由应用根据经过验证的身份决定。
如果每条 Route 都要根据用户、组、设备或上下文重新评估 allow/deny,并希望代理层统一持有路由与策略,Pomerium 更合适。Pomerium Core 可以一体运行,也可拆成 Proxy、Authorize、Authenticate 与 Databroker:Proxy 接请求,Authorize 每次评估策略,Authenticate 处理 OIDC 登录,Databroker 保存 session、claim、token 和相关状态。Zero 是托管控制面,Enterprise 是自托管控制面,不能把它们的目录同步、管理或策略能力默认归给 Core。
选择前画出真实流量:公网或办公网入口、LB/Ingress、代理、上游、回调域名、IdP 和 session store。只要还有第二个 DNS、NodePort、内网 LB 或 Service 能直达上游,代理策略就只是一条可绕过的建议。身份代理的上线完成标志不是登录页出现,而是所有受保护路径都经过验证,旁路请求失败,上游不会信任客户端自带的身份字段。
固定发行物并完成官方安装
oauth2-proxy 的安装文档列出 release 二进制、容器和源码入口,Kubernetes 可使用独立的官方 manifests 与 Helm chart。Pomerium Core 的部署文档提供发行二进制、系统包与容器入口,Kubernetes 当前入口是官方 Ingress Controller及其 CRD/Kustomize 清单。
两套工具都应固定程序版本与镜像 digest;oauth2-proxy chart 与程序版本独立,Pomerium Core 与 Ingress Controller 也要分别核对。不要从 latest 或文档默认分支推断生产版本。先检查目标 release 的校验值、平台 manifest、release notes、配置变更和安全公告,再把二进制、镜像和部署清单纳入制品库。
export O2P_IMAGE=quay.io/oauth2-proxy/oauth2-proxy:<O2P_VERSION>
export POMERIUM_IMAGE=pomerium/pomerium:<POMERIUM_VERSION>
docker buildx imagetools inspect "$O2P_IMAGE"
docker run --rm "$O2P_IMAGE" --version
docker buildx imagetools inspect "$POMERIUM_IMAGE"
# oauth2-proxy 在不监听端口时验证最终稳定配置。
docker run --rm \
-v "$PWD/oauth2-proxy.cfg:/etc/oauth2-proxy.cfg:ro" \
"$O2P_IMAGE" --config=/etc/oauth2-proxy.cfg --config-test
# Pomerium 启动后从容器内读取默认 loopback 健康监听器。
docker exec pomerium pomerium health -v--config-test 能发现 oauth2-proxy 配置解析问题,但不能证明 provider discovery、client secret、回调或上游连通。Pomerium 的 startup/readiness 也不能证明真实策略允许正确用户。安装证据应再加入镜像 digest、运行版本、配置来源、Service/Ingress、RBAC、Secret 引用与一次完整登录。
Kubernetes 安装前先渲染清单,找出 CRD、ClusterRole、IngressClass、Secret 与 namespace 级对象。Pomerium Ingress Controller 通常用 spec.ingressClassName: pomerium 和 ingress.pomerium.io/* annotation 表达 Route,全局配置由名为 global 的 Pomerium CRD 表达;这些 API 会随 controller 版本变化,不能拿旧 Helm 示例替换当前 Kustomize/CRD 契约。
oauth2-proxy 的会话与请求链
oauth2-proxy 收到未认证请求后保存登录上下文,重定向到 provider;回调处校验 state/CSRF、换取 token、提取 claims,再建立 session。后续请求通过 cookie 恢复 session,必要时刷新 provider token,最后代理到 upstream,或由 /oauth2/auth 把认证结果交给 Nginx、Ingress 等外层代理。稳定配置来源优先级为 CLI 高于环境变量,环境变量高于配置文件,排障时必须读取最终生效值而非只看仓库里的 TOML。
cookie session 把会话状态加密并签名后放在客户端。它部署简单、多个副本可共享同一 cookie secret,却受浏览器 cookie 大小限制;groups、token 或 claims 过大会产生多段 cookie,并放大每次请求。Redis session 只在 cookie 中保存票据,服务端记录实际状态,适合较大 token 和集中失效,但 Redis 的可用性、TLS、权限、备份与数据归属随之进入关键链路。
provider = "oidc"
oidc_issuer_url = "https://idp.example.net"
client_id = "internal-reports"
client_secret = "<INJECT_FROM_SECRET_STORE>"
redirect_url = "https://reports.example.com/oauth2/callback"
http_address = "0.0.0.0:4180"
upstreams = ["http://reports.reports.svc:8080/"]
reverse_proxy = true
cookie_name = "__Host-reports_session"
cookie_secure = true
cookie_httponly = true
cookie_samesite = "lax"
cookie_secret = "<INJECT_RANDOM_COOKIE_SECRET>"
set_xauthrequest = true
pass_authorization_header = false
pass_access_token = falseredirect_url 必须在 IdP 精确注册;__Host- cookie 要求 Secure、Path 为 / 且不设置 Domain,可减少跨子域重放面。多个副本必须共享 cookie secret、client 配置、外部 URL 和路由语义。把 access token 或 Authorization 传给上游,会赋予应用代表用户调用其他资源的能力,默认应关闭,确有需求时再限制 audience、scope、日志和下游出网。
external-auth 模式必须正确处理多段 Set-Cookie。Nginx auth_request 不会自动复制所有 cookie 分片,ingress-nginx 的示例可能依赖 Lua,其他同名 Ingress 实现不一定等价。升级或改变 claims 后要用真实大组账号验证回调响应和后续请求,避免只有小账号能登录。
Pomerium 的 Route、Policy 与 Databroker
Pomerium 中 Route 至少包含 from,并指向 to、redirect 或静态 response。没有 Route 时进程可以健康,却不会保护业务。登录后 Authenticate 校验 IdP 响应,在 Databroker 创建 session,并写入引用 session 的 cookie;后续每次请求由 Authorize 加载状态并重新评估 Route policy,而不是只在登录时做一次判断。
Pomerium Policy Language 的动作是 allow 与 deny。请求至少命中一个 allow 且不命中任何 deny 才放行,deny 优先。allow_public_unauthenticated_access 会绕过认证授权,只能放在真正公开的独立 Route;把它和私有 Route 的普通 policy 混用,会让维护者误判实际保护状态。
authenticate_service_url: https://authenticate.example.com
cookie_secret: ${POMERIUM_COOKIE_SECRET}
shared_secret: ${POMERIUM_SHARED_SECRET}
databroker_storage_type: postgres
databroker_storage_connection_string: ${POMERIUM_DATABASE_URL}
routes:
- from: https://admin.example.com
to: http://admin.admin.svc:8080
pass_identity_headers: true
remove_request_headers:
- x-pomerium-claim-groups
policy:
- allow:
and:
- domain:
is: example.com
- groups:
has: platform-admins
- deny:
or:
- email:
is: suspended-user@example.com示例中的 policy criteria 是否属于目标 Core、Zero 或 Enterprise 版本,必须对照PPL 与策略文档检查。策略不是普通字符串配置:修改后应以匿名用户、允许用户、普通用户和显式 deny 用户分别执行请求,确认 deny 优先、缺少 allow 时拒绝、公开覆盖只影响指定 Route。
Databroker 的 memory 存储只适合单副本且重启丢状态,file 可以跨重启保留但常规模式仍只适合单副本。多副本通常使用 Postgres保证状态一致。基于 Raft 的 clustered Databroker 若在目标版本仍标 experimental,就不能作为默认生产替代品;采用它意味着要管理唯一 node ID、各自存储、成员变更和实验特性升级风险。
身份 Header 与 cookie 的防伪边界
身份 Header 最大的误区是“来自内网所以可信”。攻击者只要能直达上游,或外层代理没有删除同名字段,就能发送 X-Auth-Request-Email、X-Pomerium-Claim-Email、X-Forwarded-User 等值。外层入口必须先清除客户端身份 Header,再复制认证子请求返回的字段;网络策略、安全组和 Service 拓扑还要阻止任何不经过代理的连接。
oauth2-proxy 启用 reverse-proxy 语义后会读取 forwarded headers。只有可信 LB/Ingress 才能决定原始 scheme、host、URI 与客户端 IP,trusted-proxy-ip 应限制为实际代理 CIDR。否则攻击者可伪造 X-Forwarded-Uri 影响 skip route 判断,或伪造 host/scheme 影响 callback 与 cookie。skip-auth-route 应按真实规范化路径匹配,不能让 query、重复编码或路径前缀把私有地址伪装成健康检查。
Pomerium 的 pass_identity_headers 默认关闭。启用后,上游可收到 X-Pomerium-Jwt-Assertion 和 claim Header;应用应验证 assertion 的签名、issuer、audience 与时间语义,并从 Pomerium JWKS 更新 key。普通 claim Header只能用于便利读取,不能成为单独授权证据。remove_request_headers 应删除应用不需要的 groups 或敏感 claim,减少泄露与 Header 体积。
cookie secret、provider client secret、Pomerium shared secret 与 signing private key用途不同,不应复用。cookie 的 Domain、Path、SameSite、Secure 与生命周期必须按实际入口设计;共享顶级域 cookie 会扩大所有子域的重放和覆盖面。日志、错误页、Tracing baggage 与 APM 请求头采集应过滤 session cookie、CSRF cookie、Authorization、access token 和 assertion。
项目接入与旁路封口
旧应用接入 oauth2-proxy 时,可以让代理直接转发,也可以由现有 Nginx/Ingress 调 /oauth2/auth。前者拓扑简单,后者保留现有路由能力,但 Header 与 cookie 复制责任更多。应用必须明确消费哪个稳定主体字段,遇到字段缺失时拒绝还是降级,以及业务权限从哪里读取。把 email 直接映射管理员通常不可接受,因为 email 可能变更、复用或大小写不一致。
Pomerium 接入时,每个 Route 都应有独立 owner、上游 Service、TLS 域名和 policy 测试。应用若验证 X-Pomerium-Jwt-Assertion,需实现 JWKS 缓存与 key 轮换,并对 audience 绑定当前应用。不要让多个不同敏感度的上游共享一个宽泛 audience,也不要在认证代理前再加一个会覆盖 Authorization 或身份 Header 的未知中间层。
旁路封口需要同时执行:删除或限制上游公开 Ingress;NetworkPolicy 只允许代理 namespace/ServiceAccount 到达上游;安全组关闭旧 LB;应用拒绝缺少有效 assertion 或认证上下文的请求;运维探针走专用路径和来源。即使上游只监听内网,也要假设内网客户端可以伪造 Header。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: reports-only-from-access-proxy
namespace: reports
spec:
podSelector:
matchLabels:
app: reports
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: access-proxy
podSelector:
matchLabels:
app: oauth2-proxy
ports:
- protocol: TCP
port: 8080NetworkPolicy 是否生效取决于 CNI,且它不能覆盖集群外旧 LB、hostNetwork 或其他网络路径。上线前要从受信入口、普通 Pod、办公网与旧域名分别请求 /whoami,记录上游实际看到的来源、Host、URI、身份 Header 和验证结果。
正向与反向实验要一起运行
正向实验使用普通允许用户完成登录,验证 state/CSRF、callback、cookie 属性、session 恢复、刷新与上游身份;再轮换一个副本,确认登录态在剩余副本有效。Pomerium 还要验证 Route policy 在每次请求执行,而不是只在登录时缓存一次 allow。
反向实验从匿名、伪造 Header、恶意 forwarded header、显式 deny 和旁路直连同时施压:
export ENTRY=https://reports.example.com
export UPSTREAM=http://reports.reports.svc:8080
# 匿名私有请求只能跳转登录或返回 401/403,不能抵达 upstream 的 2xx。
curl -sS -o /dev/null -D - "$ENTRY/private"
# query 中出现公开路径不能改变真实私有 path 的授权结果。
curl -sS -o /dev/null -D - "$ENTRY/private?next=/healthz"
# 不可信客户端伪造身份与 forwarded 字段,仍不能成为管理员。
curl -sS -o /dev/null -D - \
-H 'X-Auth-Request-Email: admin@example.com' \
-H 'X-Pomerium-Claim-Email: admin@example.com' \
-H 'X-Forwarded-Uri: /healthz' \
"$ENTRY/private"
# 从非代理 Pod 直连上游,应由网络或应用验证层拒绝。
curl -sS -o /dev/null -w '%{http_code}\n' "$UPSTREAM/private"Pomerium 的额外反例包括:没有 allow 的 Route 返回 403;显式 deny 用户即使已登录也不能访问;伪造 X-Pomerium-Jwt-Assertion 不能通过签名校验;非法 PPL 在 controller event 或配置日志中明确报错且不生效。oauth2-proxy 的 external-auth 反例则要直接向外层入口发送 X-Auth-Request-*,确认外层删除客户端值,只复制认证子请求返回值。
测试断言应绑定不变量,不绑定某个 UI 文案:未认证请求不能到上游,伪造字段不能改变 principal,旁路失败,deny 永远优先,停掉会话存储后 readiness 和真实访问按设计失败。将响应中的 token、cookie 和个人信息脱敏后再保存为证据。
故障证据如何定位到组件
oauth2-proxy 的 302 循环通常从 redirect URL、cookie Domain/Path/SameSite、外部 scheme/host 和 forwarded-header 信任检查。403 要继续区分 provider 已认证但邮箱/组过滤拒绝,还是 upstream 自己授权拒绝。大账号登录失败而小账号成功时,检查 groups 导致的 cookie/Header 大小、多段 Set-Cookie 与外层代理限制。Redis 模式出现间歇登出时,再查 key TTL、网络、TLS、连接池与副本配置是否一致。
Pomerium 可按 Proxy、Authenticate、Authorize、Databroker 分层:没有登录跳转先看 Route 与 Proxy,回调失败看 Authenticate/IdP,已登录 403 看 Authorize policy,重启后会话丢失或副本不一致看 Databroker/Postgres。Core 的健康检查文档区分 startup、ready 与 health,但真实登录、授权和数据库可写仍需合成探针。
# oauth2-proxy
curl --fail https://auth.example.com/ping
curl --fail https://auth.example.com/ready
kubectl -n access-proxy logs deployment/oauth2-proxy --since=15m
# Pomerium:默认健康监听器通常只在 loopback,从容器内读取。
docker exec pomerium pomerium health -v
docker exec pomerium sh -c 'wget -qO- http://127.0.0.1:28080/readyz'
curl -fsS http://127.0.0.1:9090/metrics | grep '^pomerium_build_info'
# Kubernetes 控制器与 Route 调谐证据
kubectl describe pomerium/global
kubectl describe ingress -n "$NS" "$INGRESS"
kubectl -n pomerium logs deployment/pomerium排障日志保留 provider/Route、状态码、错误类别、耗时和脱敏 correlation ID;删除 email、groups、完整 URL query、session cookie、Authorization、assertion、数据库 DSN 与 Secret。不要为了看 claims 把 token 原文写进工单。临时 debug 日志应设自动回收时间,并验证集中日志副本也已按保留策略处理。
HA、容量和长期成本
oauth2-proxy 使用 cookie store 时可水平扩展且通常不要求 sticky session,但所有副本必须共享 cookie secret、client 配置、redirect URL 和路由。并发 refresh 仍可能发生竞争,副本无状态不等于刷新没有状态语义。Redis store 下所有副本要连接同一逻辑 session 集群;应用 Pod Ready 不代表 Redis 可用,应分别测试 Redis 故障切换、/ready 与真实 refresh。
Pomerium 多副本需要稳定的 cookie/shared/signing secret 和共享 Databroker 状态。常规生产使用 Postgres,并让连接串在多主机情况下选择可写节点。Proxy、Authorize、Authenticate 与 Databroker 可以独立扩缩,容量模型也不同:Proxy 看请求吞吐与连接,Authorize 看每请求策略成本,Authenticate 看登录峰值与 IdP 延迟,Databroker 看 session 读写、通知与数据库连接。
两者的容量测试都要加入 Header 大小、cookie 分片、大 groups、登录风暴、token refresh 峰值、IdP 限流和存储中断。资源请求不能只根据空闲内存设置;至少记录请求延迟分位、认证/授权错误分类、活跃 session、Redis/Postgres 延迟与连接、进程 CPU/内存、上游响应和端到端登录成功率。代理前 LB 的超时还要覆盖慢 IdP 回调,不能短到把成功换码误判为失败。
长期成本不只是一组 Pod:还包括 Redis/Postgres HA、跨区流量、域名与证书、IdP 应用、密钥轮换、审计日志、值班与策略测试。oauth2-proxy 的小模型通常维护成本较低;Pomerium 的逐请求授权能收敛多应用策略,但需要承担 Databroker、策略发布和组件化排障。若现有 API 网关已经可靠完成 JWT 校验与外部授权,不应为同一请求再堆身份代理,除非旧 Web 应用确实需要浏览器登录与会话桥接。
升级、清理与恢复直连
升级 oauth2-proxy 前运行目标版本 --config-test,阅读配置参考与安全修复,比较 stable config、alpha config、provider、session 与 Header 行为。先用测试 client 验证 callback、cookie、refresh、external-auth 和伪造 Header 反例,再滚动一个副本。chart 升级要检查渲染后的 Ingress、ServiceAccount、Secret、NetworkPolicy 与镜像,不能只看程序 release。
Pomerium 升级遵循官方升级指南,同时核对 Core、Ingress Controller、CRD、PPL、Databroker schema 与 Core/Zero/Enterprise 功能归属。先恢复 Postgres 备份到隔离环境,验证 Route、deny、assertion/JWKS、session 与真实 IdP 登录。CRD 升级应保存旧对象与转换结果;回退二进制不保证能回退状态格式。
退出身份代理时,先决定业务入口迁移到新代理、应用原生 OIDC,还是受控内网直连。恢复直连不是打开旧 Service 就结束:应用必须删除对代理身份 Header 的信任,或改为验证新的 token;外层 Nginx/Ingress 要同步移除 auth_request、callback 与 Header copy;DNS/LB 切换后确认旧入口无流量,再撤销 IdP client 与 secret。
# 先保留实际对象证据,再按安装入口执行反操作。
helm get manifest oauth2-proxy -n access-proxy > oauth2-proxy-before-exit.yaml
helm uninstall oauth2-proxy -n access-proxy
kubectl get all,secret,configmap,ingress -A | grep -Ei 'oauth2-proxy|pomerium'
kubectl get crd,clusterrole,clusterrolebinding,ingressclass | \
grep -Ei 'oauth2-proxy|pomerium'
dig +short reports.example.com
curl -sS -o /dev/null -D - https://reports.example.com/
# 独立确认 IdP client 已禁用、旧 secret 无法换 token、上游不再信任旧 Header。
# Redis/Postgres 先确认数据归属与保留策略,不对共享实例执行通用清库命令。oauth2-proxy 的 cookie store 没有集中 session 可删除,使旧 cookie 失效要轮换或销毁 cookie secret并等待客户端 cookie 过期;Redis store 只删除专用 key 或专用库,不能误清共享 Redis。Pomerium 还要处理 cookie/shared/signing secret、Databroker/Postgres、Hosted 或自托管 Authenticate、JWKS 信任缓存,以及 Zero/Enterprise 中独立的控制面对象。
安全退出后的检查结果应证明:旧域名已按迁移计划指向新入口或停止服务,旧 OAuth client 与 secret 失效,旧 cookie/assertion 不能继续授权,旁路和临时 Ingress 已关闭,应用不再接受旧身份 Header,外部 Redis/Postgres、备份、日志、DNS、TLS 与审计记录都有明确保留或销毁责任。只有这些状态一起闭合,代理下线才不会把一次身份迁移变成新的匿名入口。
