Dex 联邦身份代理部署、接入与安全退出手册
集群里的多个应用原本分别对接 LDAP、GitHub 和企业 OIDC。某次目录属性调整后,一个应用还能登录,另一个却因为 groups 缺失拒绝访问,Kubernetes CLI 又因 iss 不匹配整体失效。三个系统都声称“OIDC 登录成功”,但它们消费的 issuer、client、scope 和声明契约并不相同,故障证据散落在浏览器回调、Dex 日志、上游身份源与资源端验证器里。
Dex 适合把多种上游认证方式收敛成一个稳定的 OIDC issuer:上游身份源负责账号、密码、多因素认证和人员生命周期,Dex 负责 connector 适配、授权请求、令牌与声明,下游应用仍负责把身份映射成自己的权限。它不是完整 IAM,也不替应用保存菜单权限、审批关系或离职流程;这种克制正是部署时必须守住的工程边界。
先把 issuer 变成长期契约
Dex 对外最重要的对象不是 Pod,而是 issuer。它同时进入发现文档的 issuer 字段、令牌的 iss 声明、授权与令牌端点地址,以及客户端回调链。浏览器、命令行客户端和资源端验证器都必须能以同一个外部 URL 访问它。把 Pod IP、临时端口或集群内 Service 地址写进配置,即使首页返回 200,也会在回调、JWKS 拉取或令牌校验阶段暴露成 issuer mismatch。
生产域名应先完成 DNS、TLS、反向代理路径和证书续期设计。若外部地址是 https://login.example.com/dex,代理必须完整保留 /dex 前缀,并正确传递 scheme 与 host;不能让 Dex 看到 HTTP 内部地址后生成错误回调。下游注册的 redirect URI 必须精确匹配,协议、主机、路径和尾斜杠的差异都应视为不同地址。
首次接入先用发现文档建立可重复的契约检查,而不是观察登录页:
export DEX_ISSUER=https://login.example.com/dex
discovery=$(curl --fail --silent --show-error \
"$DEX_ISSUER/.well-known/openid-configuration")
echo "$discovery" | jq '{issuer,authorization_endpoint,token_endpoint,jwks_uri}'
test "$(echo "$discovery" | jq -r .issuer)" = "$DEX_ISSUER"
jwks_uri=$(echo "$discovery" | jq -r .jwks_uri)
curl --fail --silent --show-error "$jwks_uri" | jq -e '.keys | length > 0'这组检查分别证明外部路由、issuer 自描述和签名公钥可达。它没有证明上游登录成功,却能把大量“登录后才失败”的配置错误提前截住。TLS 证书链、DNS 和反向代理前缀变化后应立即重跑,并从集群外和资源端所在网络各执行一次。
从官方发行物完成安装与首启
Dex 的入门文档提供容器运行入口,配置参考描述主配置结构,Kubernetes 环境可使用官方 Helm 仓库。目标 release、Helm chart 与镜像 digest 是三条独立版本轴:chart 的升级不自动等于 Dex 程序升级,镜像 tag 也不能替代 digest 和平台清单核验。
隔离环境可以先用容器和 SQLite 验证 issuer、client 与 connector,但 SQLite 只适合单实例快速启动。正式部署应把配置放在只读挂载,把 connector secret、client secret 和数据库凭证交给受控 Secret 注入,并让日志输出到标准错误供平台采集。下面以固定占位版本展示安装动作,实际值应来自审批过的 release:
export DEX_VERSION=<DEX_VERSION>
export DEX_IMAGE=ghcr.io/dexidp/dex:${DEX_VERSION}
export DEX_CHART=<DEX_CHART_VERSION>
docker buildx imagetools inspect "$DEX_IMAGE"
docker run --rm "$DEX_IMAGE" version
helm repo add dex https://charts.dexidp.io
helm repo update dex
helm show values dex/dex --version "$DEX_CHART" > dex-values.reference.yaml
helm upgrade --install dex dex/dex \
--namespace dex --create-namespace \
--version "$DEX_CHART" \
--values ./deploy/dex-values.yaml
kubectl -n dex rollout status deployment/dexhelm show values 的输出用于比对,不应直接当成长期配置提交。安装后读取 Deployment 中的实际镜像、Service、Ingress、Secret 引用和 ServiceAccount 权限,并保存 helm get manifest 作为变更证据。若选择 Kubernetes storage,还会创建或使用 Dex 自定义资源与 RBAC;Helm release 删除后这些集群级对象是否保留,必须以实际渲染清单判断。
源码构建适合需要审计补丁或参与开发的团队,不是默认生产入口。无论使用二进制、容器还是 Helm,都应从官方发布页读取目标版本说明,并核对镜像架构、签名或校验值、依赖数据库和升级要求。
配置对象如何共同完成一次登录
Dex 只读取主配置文件。issuer 定义对外身份,storage 保存运行状态,connectors 描述向哪个上游认证,staticClients 描述哪些下游可以发起授权,expiry 与 oauth2 改变令牌行为,可选 grpc 则开放动态管理接口。一次登录不是 connector 单独完成的:授权请求先绑定 client 与 redirect URI,再选择 connector,上游回调返回身份,Dex 映射 claims、保存授权状态并签发下游可验证的 token。
下面的配置展示 OIDC 上游与 Web 客户端的最小关系。Secret 采用环境变量展开只是表达注入位置,实际部署应确认目标版本的环境变量展开行为,并防止渲染结果进入日志、CI artifact 或 helm get values 的公开输出:
issuer: https://login.example.com/dex
storage:
type: postgres
config:
host: dex-db.identity.svc
port: 5432
database: dex
user: dex_runtime
password: $DEX_DB_PASSWORD
ssl:
mode: verify-full
caFile: /etc/dex/db-ca.pem
web:
http: 0.0.0.0:5556
staticClients:
- id: inventory-web
name: Inventory Web
secretEnv: INVENTORY_CLIENT_SECRET
redirectURIs:
- https://inventory.example.com/oauth/callback
connectors:
- type: oidc
id: corp-oidc
name: Corporate Login
config:
issuer: https://idp.example.net
clientID: $UPSTREAM_CLIENT_ID
clientSecret: $UPSTREAM_CLIENT_SECRET
redirectURI: https://login.example.com/dex/callback
scopes: [openid, profile, email, groups]配置变化的影响要逐项判断。改 issuer 会使所有依赖旧 iss 的验证器拒绝新 token;删 connector 会让相应上游无法开始新登录,但既有会话和 refresh token 是否继续有效取决于 token、存储与客户端行为;改 client secret 只影响持有该 client 的换码请求;改 claim 映射则可能让下游权限突然收缩或膨胀。不要用“重启成功”概括这些不同的状态变化。
enablePasswordDB 与 staticPasswords 会让 Dex 自己承担本地密码记录、bcrypt 哈希和账号停用责任。它可以帮助受控测试或特殊兼容,但一旦用于人员登录,备份、密码策略、重置和离职核销都会落到 Dex 运维者身上。若组织已有权威目录,应优先让 connector 把这些责任留在上游。
Connector 与声明不是同一种能力
Connector 表达“如何验证上游身份”,并不保证所有上游都能提供相同 claims、刷新能力或注销语义。OIDC、LDAP、GitHub 等 connector 的 group、email、preferred_username 与 refresh 支持要按connector 文档逐个核对。通用 OAuth2 connector 与专用 OIDC connector 即使都能完成首次登录,也可能在刷新和 group 返回上完全不同。
声明契约应在下游接入前确定:哪个字段作为稳定主体标识,email 是否可变,groups 是名称还是 ID,大小写如何处理,缺失 group 时是拒绝还是降级。资源端最好使用 issuer、subject、audience 的组合识别身份,不把可修改 email 当永久主键。组列表会进入 ID token 或 UserInfo 响应时,还要测量真实目录规模;大组集合会推高 token、cookie 和 HTTP header 大小,最终表现为代理 431、回调失败或日志截断。
Dex 的 SAML connector 页面带有明确维护与安全警告,新接入不应因为“状态列表可见”就将其当作稳妥默认项。AuthProxy connector 更敏感:前置代理必须在每条可达路径删除客户端提供的 X-Remote-User、X-Remote-Group 等身份头,然后写入经过验证的值。只在正常入口覆盖 Header 不够,任何能绕过前置代理直达 Dex 的地址都会变成身份伪造通道。
用正反实验锁定客户端契约
正向实验应使用专门测试 client,完整执行 authorization code 流程,检查授权码只能使用一次,ID token 的 iss、aud、sub、过期时间和签名都正确,并在申请 offline_access 时验证 refresh token 轮换。对 CLI 或 Kubernetes 客户端,还要在真实资源端完成一次请求,因为“拿到 token”并不等于资源端接受 token。
反向实验先攻击 redirect URI 与 issuer,这两处最容易被首页健康掩盖:
export CLIENT_ID=inventory-web
# 未注册回调必须被拒绝,不得向 evil.invalid 返回 code。
curl -sS -o /dev/null -D - \
"$DEX_ISSUER/auth?client_id=$CLIENT_ID&redirect_uri=https%3A%2F%2Fevil.invalid%2Fcb&response_type=code&scope=openid"
# 错误路径不能返回一个看似正常、实则 issuer 不同的发现文档。
curl -sS -o /dev/null -w '%{http_code}\n' \
"https://login.example.com/wrong/.well-known/openid-configuration"
# 资源端应拒绝错误 audience、过期 token 与未知签名 key。
curl -sS -o /dev/null -w '%{http_code}\n' \
-H 'Authorization: Bearer <WRONG_AUDIENCE_TEST_TOKEN>' \
https://inventory.example.com/api/me实验记录只保存状态码、错误类别、issuer、audience 与脱敏后的 subject 哈希,不保存授权码、access token、refresh token、session cookie 或完整 groups。预期不应笼统写“失败”:恶意回调不能收到 code,错误 audience 应在资源端返回 401 或明确拒绝,未知 key 应触发 JWKS 刷新后仍拒绝,而不是回退为匿名管理员。
刷新实验要并发发起两次 refresh,观察默认轮换下旧 token 的行为。reuseInterval 可以缓和网络重试与并发客户端的竞争,disableRotation 会扩大 refresh token 被重放的窗口;它们是安全与兼容性的取舍,不是解决客户端并发缺陷的免费开关。
项目与 Kubernetes 的接入落点
Web 项目通常由后端保存 client secret,并在回调处换取 token;浏览器公共客户端不能保守 client secret,应使用适合公共客户端的授权模式与 PKCE。应用接入至少要固定 issuer、client ID、精确 redirect URI、scope、audience、JWKS 缓存策略和登出后的跳转,并明确谁把身份映射成业务角色。Dex 只证明“是谁”,库存审批、项目成员或管理员权限仍由应用授权层判定。
Kubernetes API Server 接入 OIDC 后,用户名与组声明会直接影响 RBAC。必须先用低权限测试组验证映射,再创建绑定;不要把宽泛目录组直接绑定 cluster-admin。API Server 需要稳定访问 Dex 的 issuer/JWKS,集群管理员还要评估 issuer 故障对新建连接、已有 token 和 break-glass 账号的影响。保留不依赖 Dex 的受审计应急身份,权限应足够恢复 OIDC 配置但不能成为日常旁路。
接入证据可以从三端拼合:Dex 日志给出 connector、client 与错误阶段,上游 IdP 审计说明认证是否完成,资源端日志说明 iss、aud、claim 与 RBAC 为何拒绝。三端共享一个脱敏 correlation 标识时,才能区分“上游没认证”“Dex 没签发”和“资源端不授权”。
从故障证据反推失败节点
遇到重定向循环,先比较浏览器地址、发现文档与配置中的 issuer,再看反向代理是否丢失路径前缀或 scheme;不要先清 cookie。遇到 invalid_client,核对发起换码的 client ID、secret 注入版本与回调,不要去改 connector。遇到 invalid_grant,再检查授权码是否被重复使用、回调是否一致、时钟与 refresh 轮换竞争。
声明缺失要先确认上游是否返回,再确认 connector 是否映射,最后看下游请求的 scope 与资源端读取字段。LDAP 搜索超时、OIDC discovery 不可达、数据库连接耗尽和签名 key 加载失败会产生不同证据,应该分别建立告警。下面的取证命令保持只读,并避免输出 Secret 内容:
kubectl -n dex get deploy,pod,svc,ingress -o wide
kubectl -n dex describe deployment dex
kubectl -n dex logs deployment/dex --since=15m | \
grep -Ei 'connector|oauth2|storage|token|error'
kubectl -n dex get events --sort-by=.metadata.creationTimestamp
curl -fsS "$DEX_ISSUER/.well-known/openid-configuration" | \
jq '{issuer,authorization_endpoint,token_endpoint,jwks_uri}'
openssl s_client -connect login.example.com:443 \
-servername login.example.com </dev/null 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates日志中的 email、subject、groups、callback query 和错误正文可能包含个人或凭证信息。采集规则应保留 client、connector、错误类型、耗时和请求关联字段,删除 token、code、cookie 与数据库 DSN。调高 debug 级别必须有到期时间,排障结束后恢复并清理临时日志副本。
权限、存储与旁路封口
Dex 数据库保存 refresh token、授权请求、防重放状态、签名 key 和可选密码记录,是安全边界而非普通缓存。运行账号需要正常读写和目标版本迁移所需权限,但不应拥有创建数据库、管理其他 schema 或读取其他业务表的能力。SQL 首次连接可能自动迁移且迁移没有自动回滚,升级窗口可以临时授予受控 DDL 权限,完成后收回并审计实际 schema 变化。
Kubernetes storage 会把状态放进自定义资源,ServiceAccount 的 CRD 权限、etcd 加密、备份与集群管理员可见性都随之进入风险模型。SQLite 文件既不能提供多副本共享,也不能承担可靠备份恢复。Postgres、MySQL 或 etcd 可提供共享状态,但数据库自身的复制、故障转移、连接预算与备份恢复仍需单独验证。
旁路封口包括四层:网络层只允许入口代理访问 Dex Web 端口;反向代理删除来自客户端的身份 Header;资源端只信任精确 issuer 和 audience;下游应用不能保留一个绕过 OIDC 的公共直连域名。gRPC API 默认关闭,启用动态 client/connector 管理时必须使用双向 TLS、网络白名单和最小管理权限;明文 gRPC 即使能在测试环境工作,也不能暴露到共享网络。
HA、容量与成本怎样估算
Dex 本身可以水平扩展,但所有副本必须共享同一 storage、issuer、connector 配置、client secret 与 TLS 行为。负载均衡器不应依赖某个 Pod 的本地文件状态。数据库连接池总量按“每副本连接上限乘副本数”计算,再给迁移、备份和人工排障保留连接预算;盲目增加副本可能先把数据库打满。
容量测试要覆盖登录峰值、授权码交换、JWKS 拉取、refresh 峰值和大 groups token,而不只是首页吞吐。上游目录限流会把大量请求积压在 connector,数据库慢写会延长授权状态持有时间,签名 key 轮换会提高 JWKS 缓存更新压力。可观测指标至少包括各 connector 成功率与延迟、token 端点错误分类、storage 延迟/连接、进程资源、HTTP 状态码和真实端到端登录探针。
成本主要来自共享数据库或 etcd、跨故障域流量、日志审计、证书与域名、上游 IdP API,以及值班复杂度。对只有一个 OIDC 上游和少数原生 OIDC 应用的团队,直接接上游可能更便宜;当多个下游需要稳定 issuer、上游 connector 经常变化,或 Kubernetes 等资源端需要统一声明时,Dex 才能抵消适配成本。这个判断不应把它扩张成账号目录或权限中心。
HA 演练应逐个终止副本,并持续执行发现、登录、换码和 refresh;随后阻断一个上游 connector、使数据库只读或耗尽连接,确认失败是可见的拒绝而不是静默放行。就绪探针只证明进程当前状态,不能替代真实授权链探针。
升级、迁移与安全退出
升级前同时读取 Dex release notes、connector 页面和storage 文档,在副本数据库恢复备份后运行目标版本,比较发现文档、claims、refresh、回调和 schema。先让测试 client 通过,再灰度真实 client。由于 SQL migration 不自动回滚,回退计划必须包含数据库恢复或前向修复,不能只把 Deployment 镜像改回旧版本。
迁移 issuer 时,旧 token 的 iss 不会自动变成新地址。稳妥做法是建立新 issuer、复制并复核 client/connector 契约,让资源端在受控窗口并行信任新旧 issuer,再迁移客户端并观察旧 issuer 使用量。签名 key、subject 与 claims 的变化都可能让应用把同一人员识别成新账号,迁移前必须用脱敏映射表验证。
退出按依赖逆序进行:先把下游 client 与 Kubernetes API 切到新 issuer,确认旧 issuer 没有授权和 refresh 流量;再撤销上游 connector client、下游 client secret 与 gRPC 证书;随后从 DNS/LB 摘除入口,停止 Dex;最后依据保留策略处理数据库、备份、Kubernetes CRD、审计日志和镜像。Helm 卸载只删除 release 管理的对象,不会自动删除外部数据库、IdP OAuth app、DNS/TLS 或应用中的 issuer 信任。
helm get manifest dex -n dex > dex-rendered-before-exit.yaml
helm uninstall dex -n dex
kubectl get all,secret,configmap,ingress -A | grep -i dex
kubectl get crd,clusterrole,clusterrolebinding -o name | grep -i dex
dig +short login.example.com
curl -sS -o /dev/null -D - https://login.example.com/dex/
# 在 IdP 管理端禁用旧 connector client,并验证旧 secret 无法换取 token。
# 数据库先确认归属、保留期与备份,再执行审批过的数据销毁动作。退出后的正确结果取决于迁移设计:域名可能指向新 issuer,也可能停止解析,不能机械要求 NXDOMAIN。真正的不变量是旧 connector 和 client secret 已失效、旧 issuer 不再被资源端信任、旁路地址不可用、存储与审计残留有明确归属,并且应急身份仍能在新链路故障时受控恢复访问。
