身份链路可观测与排障:从 DNS、令牌到 SCIM 的证据闭环
一次生产登录故障常被描述成“身份平台挂了”:浏览器在回调页循环跳转,网关只留下 401,身份平台却显示认证成功。最终证据表明,边缘代理把外部 https 改写成内部 http,应用据此生成了错误的 redirect URI;运维为止血放宽回调地址后,登录虽然恢复,却把开放重定向风险一起带进生产。故障不在密码、令牌或数据库,而在进入身份平台之前的代理边界。
另一次事故恰好相反。身份平台成功签发令牌,JWKS 也能访问,但新密钥轮换后只有部分资源实例持续返回 401;另一些实例对同一用户返回 403。前者缓存着旧 JWKS,后者拿到了新公钥,却因组声明被网关截断而拒绝授权。只有把 issuer、audience、kid、策略版本和跨层关联标识放在同一时间线上,团队才能把“登录失败”拆成两个责任层完全不同的问题。
先建立一条不能被凭证污染的关联链
身份请求至少跨过浏览器或 CLI、入口代理、身份提供方、应用回调、令牌端点和资源服务。单个系统生成的 request ID 只能证明本地处理,不能自动跨越重定向、异步审计与 SCIM 批任务。入口应生成不可预测但不含用户信息的 correlation_id,应用在跳转前把它与 OAuth state 的服务端记录关联;回调后继续传给后端和资源端。state 仍承担 CSRF 绑定,不能直接用可被调用者伪造的请求头替代。
结构化事件记录租户或 realm、client、issuer、resource、授权流程阶段、决策码、策略版本和相对耗时。用户使用内部不可逆伪名;令牌只记录带独立审计密钥的指纹,不能保存完整 access token、ID Token、refresh token、authorization code、SAML assertion、cookie 或 client secret。审计密钥与签名密钥分离,避免日志系统反向获得签发能力。
{
"correlation_id": "01J...",
"stage": "resource_validation",
"tenant": "workforce",
"client_id": "orders-web",
"issuer": "https://id.example.invalid/realms/workforce",
"resource": "orders-api",
"token_fingerprint": "hmac-sha256:8b3...",
"key_id": "signing-2026-b",
"decision": "deny",
"reason_code": "audience_mismatch",
"policy_version": "orders-authz-42",
"duration_ms": 7
}日志 schema 要拒绝高风险字段,而不是依赖开发者自觉。采集器二次删除 authorization、cookie、assertion、client_secret 和常见 token 参数;日志平台限制查询角色、导出范围和保留周期。关联 ID 可搜索,邮箱、手机号、组全量和设备属性只在经过批准的受限审计域出现。故障工单和截图同样遵守这条边界。
DNS、TLS 与反向代理是第一责任层
先从故障客户端所在网络解析域名,再检查 TCP、TLS、SNI、证书链和最终 HTTP 状态。办公网、集群 DNS、VPN 与公网可能得到不同地址;代理还可能重写 Host、scheme、path prefix 与可信转发头。应用日志里的 URL 是应用看到的结果,不足以证明浏览器实际访问的入口。
ID_HOST=id.example.invalid
dig +short "$ID_HOST"
curl --resolve "$ID_HOST:443:203.0.113.10" \
-sSvo /dev/null "https://$ID_HOST/.well-known/openid-configuration"
openssl s_client -connect "$ID_HOST:443" -servername "$ID_HOST" -showcerts </dev/null正向实验应保存解析结果、证书 subject/issuer/SAN、TLS 握手、响应状态和跳转目标。反向实验可以在隔离环境使用错误 SNI 或不受信 CA,预期证据是握手阶段明确失败,身份平台没有产生授权事件。若请求已经到达平台才报 redirect 错误,就不要继续改 DNS 或关闭 TLS 校验。
代理只信任来自已知上游地址的 Forwarded 或 X-Forwarded-*,并在边界覆盖客户端同名头。把任意来源的 X-Forwarded-Proto: https 当真会造成错误绝对 URL、Secure cookie 判断偏差和身份伪造。修复标准是外部 URL、内部转发和应用生成 URL 三者一致,而不是简单增加“可信所有代理”。
Discovery 与 SAML metadata 是动态信任发布面
OIDC 客户端读取 Discovery 后必须确认返回的 issuer 与配置值精确一致,并由其取得 authorization、token、JWKS 等端点。OpenID Connect Discovery 把 issuer 一致性放在协议校验链中;不能因为端点可达就接受大小写、尾斜杠、内部域名或租户路径不同的结果。
ISSUER=https://id.example.invalid/realms/workforce
curl -fsS "$ISSUER/.well-known/openid-configuration" > discovery.json
jq '{issuer,authorization_endpoint,token_endpoint,jwks_uri}' discovery.json
jq -e --arg expected "$ISSUER" '.issuer == $expected' discovery.json
curl -fsS "$(jq -r .jwks_uri discovery.json)" | jq '{key_count:(.keys|length),kids:[.keys[].kid]}'保存元数据内容哈希、读取时间、缓存年龄与来源,不记录私密配置。正向实验要求 issuer 精确匹配且 JWKS 至少包含当前签名公钥;反向实验把客户端配置换成错误租户路径,预期在元数据校验阶段失败。禁止通过关闭 issuer 校验或硬编码内部端点绕过,因为这会让恶意元数据或错误租户进入信任根。
SAML 也要记录 metadata entity ID、SSO endpoint、证书指纹、有效期与导入版本。证书临近轮换时允许新旧验证证书在受控窗口并存,但只有指定签名实体可更新 metadata。按照 OASIS SAML 安全与隐私考虑 检查签名覆盖、受众、目的地址、时间条件和 assertion 重放;不能把“XML 能解析”视为可信。
Authorization 请求要还原浏览器实际发送的字段
授权阶段关注 client、精确 redirect URI、response type、scope、PKCE challenge、state 与 OIDC nonce。浏览器网络记录可以证明请求参数,但导出 HAR 前必须清除 cookie、code 和 token。服务端只记录参数摘要及校验结果,不把一次性秘密写入普通访问日志。
典型循环跳转来自 redirect URI 的 scheme/path 不一致、SameSite cookie 丢失、state 找不到、回调节点读不到 session 或代理重复编码。按照顺序比较浏览器请求、客户端注册、服务端 session 和回调日志,不要先把 redirect 配成通配符。正向证据是 state 一次性消费、nonce 与 ID Token 绑定、PKCE S256 校验成功;重复回调应得到明确拒绝。
authorization_request_created
-> state_hash stored with session and expiry
-> browser redirected with PKCE challenge and nonce hash
-> callback received once
-> state atomically consumed
-> code exchanged with verifier
-> nonce validated before application session issued当平台返回 invalid_redirect_uri,先精确比较 scheme、host、port、path、大小写和末尾斜杠。当回调报 state mismatch,检查 cookie 是否发送、会话是否跨节点可见以及回调是否被代理改写。只有证据指向对应字段时才修改注册,不用“重新建客户端”抹掉原始现场。
Token 交换、JWKS 与时间必须分开判断
Token 端点的 invalid_grant 可能来自 code 已消费、过期、redirect 不同、PKCE verifier 错误或时钟偏差;invalid_client 才优先检查客户端认证。记录 grant 类型、客户端认证方法、错误码、端点、相对时序和关联 ID,不记录 code、verifier、secret 或返回 token。
资源端依据 OpenID Connect Core 与自己的授权合同校验 issuer、audience、签名、算法、时间和令牌用途。遇到未知 kid 时允许一次受控 JWKS 刷新并设置最小刷新间隔,防止攻击者用随机 kid 制造下载风暴;刷新后仍未知就拒绝。绝不能临时关闭验签、接受任意算法或跳过 audience。
async function validateWithBoundedRefresh(tokenHeader, cache) {
let key = cache.find(tokenHeader.kid);
if (!key && cache.mayRefresh()) {
await cache.refreshOnce();
key = cache.find(tokenHeader.kid);
}
if (!key) throw new Error("invalid_token: unknown_kid");
return verifyToken({
key,
issuer: process.env.EXPECTED_ISSUER,
audience: process.env.EXPECTED_AUDIENCE,
algorithms: ["RS256"]
});
}反向实验依次送入错误 audience、过期 token 和未知 kid 的测试令牌,预期分别得到稳定 reason code,且 JWKS 下载次数受限。测试令牌只能由隔离签发器创建,不能把生产 token 粘贴到调试网站。所有节点同步 NTP 状态并记录接收时间;适度 clock skew 用于网络和时钟误差,不应用来掩盖分钟级漂移。
Session 与 Claim 映射解释“时好时坏”
用户只在部分节点登录失败,优先核对会话模型:cookie 是否自包含、服务端状态位于数据库还是缓存、负载均衡是否错误依赖粘性、加密或签名密钥是否跨节点一致。记录 session 版本、存储命中、创建节点与认证时间,但不记录 cookie 值。扩大 Pod 数量前先证明 session 在节点切换后仍有效。
声明映射要记录来源属性、转换规则版本、输出名称、值数量和截断状态。组过多可能超过 token、header 或 cookie 尺寸;错误处理若静默截断,会使同一用户在不同入口得到不同权限。架构上优先传递稳定角色或授权引用,不把完整组织树塞进每次请求。
可复制的反向实验是给测试主体增加超过正常阈值的组,再从浏览器、代理、应用和资源端比较声明数量与响应头大小。预期结果应是明确拒绝或受控降级并产生 claim_limit_exceeded,而不是随机 431、cookie 丢失或权限悄悄减少。实验结束后删除测试组和 session,确认旧声明不能继续使用。
用 401 与 403 把认证和授权责任分开
401 表示请求缺少可接受的认证凭证,资源端通常返回合适的 WWW-Authenticate;403 表示主体已被识别但无权执行动作。实际系统常把上游超时、策略引擎异常、租户不匹配和组映射错误都压成 401,导致客户端无限重登。资源端应保留对外最小错误,对内记录稳定 reason code 与策略版本。
no credential / malformed / bad signature / wrong issuer / expired -> 401
valid credential + wrong audience or token type -> 401
valid principal + missing permission / policy deny -> 403
authorization dependency unavailable -> 503 or fail-closed policy result正向实验使用最小权限测试主体访问允许资源,保存资源、动作、策略 ID 与 allow 结果;反向实验删除所需角色后复用旧会话,预期在撤销传播预算内转为 403。若仍允许,排查 token 寿命、授权缓存和事件传播;若变成 401,则身份与授权层被混淆。客户端不得把 403 自动转换为重新登录。
应用接入时由统一认证中间件产出已验证 principal,再由业务授权层根据 resource/action/tenant 决策。入口代理传递的身份头必须先删除外部同名头并采用签名或受信网络绑定;应用不能因为请求来自代理就放弃资源级授权。
SCIM 排障要追踪资源身份而不是只看 HTTP 200
SCIM 同步跨越源目录、调度器、连接凭证、目标 API 和目标数据模型。依据 RFC 7643 与 RFC 7644,事件至少关联 job ID、源对象 ID、externalId、目标资源 ID、版本、操作、重试次数和最终 active 状态。HTTP 200 只说明一次调用成功,不能证明组成员、停用或删除已达到预期终态。
BASE=https://scim.example.invalid/v2
TOKEN="$SCIM_TEST_TOKEN"
curl -fsS -H "Authorization: Bearer $TOKEN" \
"$BASE/Users?filter=externalId%20eq%20%22lab-user-43%22" | jq .
curl -sS -o scim-denied.json -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN" \
"$BASE/Groups?filter=displayName%20eq%20%22restricted%22"正向实验创建测试用户、设置组、停用并查询终态,同时验证重复请求不会生成第二个对象。反向实验使用权限不足 token、非法 filter、重复唯一属性和服务端 429,预期分别进入拒绝、校验、冲突与带退避重试路径。429 不能无限重试;达到预算后进入死信和人工核销,避免离职账号因队列积压继续活跃。
SCIM token 按目标租户和最小操作授权,存入密钥系统并轮换;日志只记凭证版本。RFC 9865 增加了游标分页,但客户端要通过目标实现能力判断是否可用,不能假定所有服务都支持。全量同步的页数、吞吐、429、重试积压和停用延迟都进入容量模型。
指标、审计与告警必须支持判断而非堆数量
四类黄金信号分别回答不同问题:成功率按阶段和 reason code 分解;延迟区分授权、token、外部连接器、资源校验与 SCIM;流量覆盖登录、刷新、JWKS、管理 API 和供给批次;饱和度观察数据库连接、会话缓存、线程池、队列、外部限流和日志管道。不要把用户 ID、client ID 全量或组名直接放进指标标签,避免高基数与个人信息扩散。
告警应绑定可行动的责任层,例如“未知 kid 比例上升且 JWKS 刷新失败”“SCIM 停用积压超过离职时限”“token 端点 p99 上升且数据库连接耗尽”。单纯“登录失败数大于零”会在密码输错、攻击流量和平台故障之间反复误报。审计流要监测延迟、丢失与导出失败,因为没有日志不等于没有事件。
排障结束形成一条可复核时间线:客户端现象、入口证据、协议阶段、签发与验证结果、策略决策、供给终态、修复动作和反向复验。临时 HAR、pcap、调试日志与测试账号按工单核销,临时放宽的 redirect、clock skew、日志级别和代理信任必须恢复。真正的闭环不是页面重新打开,而是故障责任层已证明、危险绕过已撤销、同类失败能被指标和合成探针提前识别。
