oauth2-proxy 部署、会话与上游信任手册
oauth2-proxy 最容易制造的错觉,是域名已经跳转登录,上游就已经安全。实际上,它只保护经过自身或 /oauth2/auth 的请求;旧 Ingress、内部 Service、NodePort 或另一条负载均衡路径仍可能绕开认证。如果应用又把 X-Auth-Request-Email 当成管理员凭据,能直连上游的客户端只需伪造一个 Header,完整的 OIDC 登录链就失去了意义。
这款工具适合给缺少现代登录能力的 Web 应用增加 OAuth/OIDC 认证与会话桥接。它不是业务授权引擎,也不负责 API 生命周期、配额、协议转换或开发者门户。架构师需要先限定它的职责:确认用户是谁,建立可恢复的会话,把经过约束的身份交给应用;业务角色、对象权限和高风险操作审计仍由资源端完成。
用最小职责判断是否该部署
应用只需要“未登录时跳转、登录后获得稳定主体”时,oauth2-proxy 的模型很合适:一个 provider、OAuth client、精确注册的 redirect URL、cookie 或 Redis session store,以及一个 upstream 或 /oauth2/auth 认证端点。它可以按邮箱、域和部分 claim 做入口过滤,但这些条件不应膨胀成业务权限系统。email 会改名、组会延迟同步,资源 owner 仍应在应用内部维护真正的角色与授权关系。
若需求是每条路由按用户、设备、组和上下文逐请求计算复杂 policy,或希望中央控制面统一管理大量身份感知路由,应评估 Pomerium 或专门的策略执行点。反过来,如果现有网关已经可靠验证 JWT,应用也能处理浏览器登录,就不应再叠加 oauth2-proxy 只为增加一层 302。额外代理会带来 cookie、回调、Header、超时和排障责任。
部署前画出公网入口、LB/Ingress、代理、上游、回调域名、IdP 和 session store。接受条件必须写成可验证的不变量:所有私有路径经过认证,旁路连接被拒绝,客户端自带身份字段不能改变 principal,代理故障不会自动放开上游。仅看到登录页或一次成功回调,都不足以证明这条链成立。
固定发行物并验证最终配置
安装文档列出发行二进制、容器与源码入口,Kubernetes 另有官方 manifests 和 Helm chart。chart 版本与程序版本不是同一个概念,生产清单应同时固定 chart、镜像标签和 digest。不要从 latest 或文档默认分支推断生产行为;变更前应保存 release notes、镜像摘要、渲染清单、配置差异和安全公告。
export O2P_IMAGE=quay.io/oauth2-proxy/oauth2-proxy:<O2P_VERSION>
docker buildx imagetools inspect "$O2P_IMAGE"
docker run --rm "$O2P_IMAGE" --version
docker run --rm \
-v "$PWD/oauth2-proxy.cfg:/etc/oauth2-proxy.cfg:ro" \
"$O2P_IMAGE" --config=/etc/oauth2-proxy.cfg --config-test
helm template oauth2-proxy oauth2-proxy/oauth2-proxy \
--namespace access-proxy -f values.yaml > rendered.yaml--config-test 只能证明稳定配置可以解析,不能证明 discovery、client secret、redirect URI、证书或 upstream 可达。CLI、环境变量和配置文件存在优先级,验收要从运行进程读取最终参数,而不是只审查仓库里的 TOML。Kubernetes 还要核对 ServiceAccount、Secret 引用、Service、Ingress、NetworkPolicy、探针和镜像 digest,避免 chart 默认值悄悄扩大权限或监听范围。
权限应按部署模式收敛。单纯反向代理通常不需要读取集群对象;由控制器或外层 Ingress 调用认证端点时,也只授予实际所需的 namespace 资源。client secret、cookie secret 和 Redis 凭据来自 Secret 管理系统,不能写进 values、命令历史、CI 日志或生成后的公开制品。
理解回调、cookie 与 Redis 会话
未认证请求到达后,oauth2-proxy 保存登录上下文并重定向到 provider;回调处校验 state/CSRF、交换 code、提取 claim,再建立 session。后续请求从 cookie 恢复会话,必要时刷新 provider token,最终转发 upstream,或通过 /oauth2/auth 把认证判断交给 Nginx、Ingress 等外层代理。故障定位必须沿这条链逐段取证,不能把所有 403 都归因于 IdP。
cookie store 把会话状态加密并签名后交给浏览器。多个副本共享同一 cookie secret 即可恢复状态,部署简单,却受 cookie 大小限制;groups、token 或 claim 变大后会产生多段 cookie,并放大每次请求。Redis store 只把票据放在浏览器,实际 session 留在服务端,适合较大令牌和集中失效,但 Redis 的 TLS、ACL、容量、备份、连接池、可用性和数据归属由此进入关键链路。
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 模式
直接反向代理时,oauth2-proxy 自己连接 upstream,流量关系清楚,适合少量旧应用。external-auth 模式由现有 Nginx 或 Ingress 请求 /oauth2/auth,认证成功后再转发业务请求,能保留既有路由能力,却把 URI、Host、Header 与 Set-Cookie 复制责任交给外层代理。两种模式不要在同一路径上含混叠加,否则回调与重定向很难判断由谁生成。
external-auth 必须验证大账号产生的多段 Set-Cookie。Nginx auth_request 不会自动复制所有 cookie 分片,ingress-nginx 示例可能依赖 Lua,其他同名 Ingress 实现也不一定等价。升级 provider 或增加 groups claim 后,应让真实大组账号完成首次登录、刷新和副本切换,避免小账号成功掩盖 Header 超限。
启用 reverse-proxy 语义后,oauth2-proxy 会读取 forwarded headers。只有可信 LB/Ingress 才能决定原始 scheme、host、URI 与客户端 IP,trusted-proxy-ip 应限制到实际代理 CIDR。否则攻击者可能伪造 X-Forwarded-Uri 影响 skip route,或伪造 host/scheme 干扰 callback 和 cookie。公开健康路径应按规范化 path 精确匹配,不能让 query、重复编码或前缀误中私有地址。
把 Header 信任和旁路一起封住
身份 Header 不是因为来自内网就可信。入口必须先删除客户端提供的 X-Auth-Request-*、X-Forwarded-User 等字段,再复制认证子请求返回的值;应用遇到主体字段缺失时应拒绝,而不是降级成匿名管理员。email 适合展示,不宜直接映射不可变管理员身份;更稳妥的是使用 provider 的稳定 subject,并维护显式业务映射。
旁路封口需要同时处理 DNS、Ingress、Service、NetworkPolicy 和应用验证。删除旧公开 Ingress,限制安全组和 NodePort,只允许代理工作负载连接 upstream;如果应用能验证受众受限的 assertion 或 token,再增加资源端第二道校验。NetworkPolicy 是否生效取决于 CNI,它也覆盖不了集群外旧 LB、hostNetwork 与遗留隧道,所以必须从不同网络位置实际请求。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: reports-only-from-oauth2-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: 8080 }项目接入记录至少包含入口域名、callback、upstream、身份字段、业务 owner、代理 owner、IdP client owner 和应急旁路审批人。运维探针走专用来源与路径,不要通过 skip-auth-route 放开宽泛前缀。日志、错误页、Tracing baggage 与 APM 请求头采集必须过滤 session cookie、CSRF cookie、Authorization、access token 和 client Secret。
用正反实验验证真实边界
正向实验应覆盖允许用户首次登录、state/CSRF、callback、cookie 属性、session 恢复、token refresh、上游主体和一次副本滚动。Redis 模式还要切走当前连接或重启一个代理副本,确认会话仍可恢复。验证结果记录响应状态、重定向位置、脱敏 correlation ID 和上游实际看到的身份,不保存完整 cookie 或 Token。
反向实验从匿名访问、伪造 Header、恶意 forwarded header 和旁路直连同时施压:
export ENTRY=https://reports.example.com
export UPSTREAM=http://reports.reports.svc:8080
curl -sS -o /dev/null -D - "$ENTRY/private"
curl -sS -o /dev/null -D - "$ENTRY/private?next=/healthz"
curl -sS -o /dev/null -D - \
-H 'X-Auth-Request-Email: admin@example.com' \
-H 'X-Forwarded-Uri: /healthz' \
"$ENTRY/private"
curl -sS -o /dev/null -w '%{http_code}\n' "$UPSTREAM/private"预期结果不是固定 UI 文案,而是不变量:匿名请求不能抵达 upstream 的 2xx;query 不能改变真实 path 的授权结果;客户端伪造字段不能成为管理员;非代理来源直连失败。external-auth 还要直接向外层入口发送同名身份字段,确认入口先删除客户端值,只接受认证子请求的返回值。
从状态码定位首个失败层
302 循环优先检查 redirect URL、cookie Domain/Path/SameSite、外部 scheme/host 和 forwarded-header 信任。403 要区分 provider 已完成认证但邮箱或组过滤拒绝,还是 upstream 自己授权拒绝。大账号失败、小账号成功时,检查 groups 造成的 cookie/Header 大小、多段 Set-Cookie 和外层代理限制。Redis 模式间歇登出则查 TTL、网络、TLS、连接池和副本配置一致性。
/ping 和 /ready 能提供进程证据,却不能替代完整登录与上游访问。监控应分开记录回调错误、provider 延迟、刷新失败、认证拒绝、upstream 状态、Redis 延迟和连接、进程 CPU/内存以及端到端成功率。SLO 应围绕受保护请求是否正确接受或拒绝,而不是只看 Pod Ready。临时 debug 日志必须有自动回收时间,并确认集中日志副本也按保留策略处理。
容量测试加入登录洪峰、refresh 集中到期、IdP 限流、大 groups、cookie 分片、Redis 故障和上游慢响应。cookie store 通常不要求 sticky session,但并发 refresh 仍可能竞争;无状态副本不等于会话更新没有状态语义。长期成本还包括域名证书、Redis 高可用、审计存储、密钥轮换、值班和每个应用的回归实验,而不只是代理 Pod。
升级、回滚与安全退出
升级前对目标版本运行 --config-test,阅读配置参考与安全修复,比较 provider、session、Header 和 stable/alpha config 行为。先用测试 client 验证 callback、cookie、refresh、external-auth 以及伪造 Header 反例,再滚动单个副本。chart 变更必须审查渲染后的 RBAC、Ingress、Service、Secret、NetworkPolicy 和镜像,不能只看应用 release。
回滚包应包括旧镜像 digest、旧清单、兼容的 cookie secret、client 配置和路由。若新版本已经改变 cookie 或 session 格式,回退程序不保证恢复旧会话,应提前决定允许用户重新登录还是维持双版本窗口。Redis schema、key 前缀和 TTL 的改变也要在隔离副本验证,不能对共享数据库执行通用删除命令。
退出时先确定业务迁往新代理、应用原生 OIDC,还是受控内网入口。移除 auth_request、callback、Header copy 和代理路由后,应用不得继续相信旧身份 Header;DNS/LB 切换稳定后,撤销 IdP client 与 secret。cookie store 没有集中 session 可删,需销毁或轮换 cookie secret并等待客户端 cookie 过期;Redis 只清理专用 key 或专用库。
最终证据应证明旧域名按计划停止或迁移、旧 client 无法再换取令牌、旧 cookie 不能授权、临时 Ingress 与旁路均关闭、应用不再接受旧 Header,外部 Redis、日志、备份、DNS 与证书也有明确保留或销毁责任。到这一步,oauth2-proxy 才算从架构、权限、运维和成本账上真正退出。
