上一篇文章中,我们完成了 oidc.cipherhub.cloud 与 AWS 的联合身份配置,演示了如何通过自建 OIDC Provider 换取 AWS 临时凭证。

但那篇文章侧重于 AWS 端的操作步骤,对 OIDC 协议本身的工作机制着墨不多。

本文将以 oidc.cipherhub.cloud 的实现为蓝本,深入探讨 OIDC 的协议定位、密钥管理、Token 签发与验证、安全性保障等核心问题。

一、OIDC 与 OAuth 2.0、SSO、SAML 的关系

在聊 OIDC 的具体机制之前,先厘清几个容易混淆的概念。

OAuth 2.0:授权框架,不是认证协议

OAuth 2.0 解决的是授权问题即:“允不允许某个应用访问你的资源”

而“你是谁”属于是认证问题

OAuth 2.0 签发的是 access_token(访问令牌),它代表的是一组权限,但不包含用户身份信息。

举个例子:你用微信登录某个第三方 App,OAuth 2.0 让那个 App 能读取你的微信头像和昵称,但 OAuth 2.0 本身并没有标准化"告诉第三方 App 你到底是谁"这件事。

OIDC:在 OAuth 2.0 之上加了一层"身份"

OpenID Connect(OIDC)正是为了解决这个缺失而诞生的。

它在 OAuth 2.0 的基础上增加了一个 id_token——一个包含用户身份信息的 JWT。

1
2
3
4
5
6
7
┌─────────────────────────────────────┐
│            OpenID Connect           │  ← 身份层:id_token("你是谁")
├─────────────────────────────────────┤
│             OAuth 2.0               │  ← 授权层:access_token("能做什么")
├─────────────────────────────────────┤
│              HTTP/TLS               │  ← 传输层
└─────────────────────────────────────┘

简单说:OAuth 2.0 给你钥匙,OIDC 还额外给你一张身份证。

oidc.cipherhub.cloud 的场景中,我们只用到了 OIDC 的 id_token 部分——用它向 AWS 证明"我是谁",然后 AWS 根据信任策略决定"你能做什么"。

SAML:上一代的身份联合协议

SAML 2.0(Security Assertion Markup Language)和 OIDC 解决的是同一个问题——跨系统的身份联合与单点登录,但它们属于不同时代:

对比维度SAML 2.0OIDC
诞生时间2005 年2014 年
数据格式XMLJSON / JWT
传输方式浏览器 POST/Redirect + XML 签名HTTPS + JSON + JWT 签名
令牌体积大(XML 冗余)小(JWT 紧凑)
移动端/API 友好度差(依赖浏览器)好(纯 HTTP API)
生态企业 SSO(AD FS、Shibboleth)互联网/云原生(Google、GitHub、AWS)

AWS IAM 同时支持 SAML 和 OIDC 作为联合身份来源。

选择 OIDC 而非 SAML 的原因很直接:我们的场景是机器对机器(M2M),不涉及浏览器交互,OIDC 的 JSON/JWT 方案远比 SAML 的 XML 轻量。

SSO:是目标,不是协议

SSO(Single Sign-On,单点登录)是一种体验目标

用户登录一次就能访问多个系统,而不是一个具体的协议。

OIDC 和 SAML 都可以实现 SSO,它们是达成 SSO 目标的不同技术手段。

1
2
3
4
5
6
7
8
                ┌─────────────────┐
                │   SSO(目标)    │
                └────────┬────────┘
            ┌────────────┼────────────┐
            ▼            ▼            ▼
         ┌──────┐   ┌────────┐   ┌───────────┐
         │ OIDC │   │ SAML   │   │ CAS / 其他 │
         └──────┘   └────────┘   └───────────┘

四者关系:OAuth 2.0 管授权,OIDC 在它上面加了身份认证,SAML 是 OIDC 的上一代替代方案,SSO 是它们都能实现的一种用户体验目标。

二、标准 OIDC 流程 vs oidc.cipherhub.cloud 的简化模式

标准 OIDC 流程:以用户为中心

标准 OIDC 定义了多种交互流程(Grant Type),最常用的是 Authorization Code Flow

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
用户浏览器            应用后端            OIDC Provider(如 Google)
    │                    │                        │
    │  访问应用           │                        │
    │ ──────────────>    │                        │
    │                    │                        │
    │  302 重定向到登录页  │                        │
    │ <──────────────    │                        │
    │                    │                        │
    │  跳转到 Provider 登录页                      │
    │ ─────────────────────────────────────────>  │
    │                    │                        │
    │  用户输入账号密码、点击授权                    │
    │ <─────────────────────────────────────────  │
    │                    │                        │
    │  302 携带 code 回调应用                       │
    │ ──────────────>    │                        │
    │                    │  用 code 换 id_token    │
    │                    │ ──────────────────────> │
    │                    │  { id_token, ... }      │
    │                    │ <────────────────────── │
    │  登录成功           │                        │
    │ <──────────────    │                        │

这个流程的核心特征:

  • 有用户参与:用户在浏览器中输入凭证、确认授权
  • 有重定向:通过 redirect_uri 在各方之间传递授权码
  • 两步走:先拿 code,再用 codeid_token(防止 Token 暴露在浏览器 URL 中)

其他标准流程还包括 Implicit Flow(已不推荐)、Client Credentials Flow(机器对机器)等。

oidc.cipherhub.cloud 的简化模式:面向机器

oidc.cipherhub.cloud 面对的场景完全不同:没有"用户",只有"机器"

一台服务器需要获取 AWS 临时凭证去调用 KMS 或 Secrets Manager,整个过程没有浏览器、没有登录页面、没有人类参与。

因此我们采用了一种直接签发模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
客户端(机器)                       oidc.cipherhub.cloud
    │                                       │
    │  1. 用自己的私钥对请求参数签名            │
    │  2. GET /generate-token               │
    │     ?audience=...&timestamp=...       │
    │     &nonce=...&signature=...          │
    │ ──────────────────────────────────>   │
    │                                       │  验证签名 + 时间戳
    │     { "id_token": "eyJ..." }          │  签发 id_token
    │ <──────────────────────────────────   │

没有重定向、没有授权码、没有用户交互,一次 HTTP 请求完成

QQ_1772335386273

安全能力对比

简化流程并不意味着降低安全性,只是安全机制的侧重不同:

安全能力标准 Authorization Code Flowoidc.cipherhub.cloud 简化模式
用户认证用户名密码 / MFA / 社交登录不适用(无用户)
客户端认证client_id + client_secret证书签名(RSA-SHA256)
防重放state 参数 + 授权码一次性使用时间戳(±3s)+ 随机 Nonce
Token 传输保护授权码→后端换 Token,Token 不经浏览器全程服务端通信,无浏览器参与
权限范围scope 参数(openid, profile, email…)audience 参数(限定目标系统)
吊销能力Token 吊销端点 / 短有效期从 audiences.yaml 删除即刻失效

标准流程的安全设计主要在防御浏览器环境的威胁(CSRF、Token 泄露到 URL、开放重定向等),而我们的简化模式不存在这些攻击面。

作为替代,引入了证书签名鉴权——其安全强度实际上高于 client_id + client_secret 方案,因为私钥永远不需要在网络上传输。

什么时候不能用简化模式?

如果你的场景涉及:

  • 真实用户在浏览器中登录
  • 需要用户主动授权同意
  • 需要对接第三方社交登录

那就必须使用标准 OIDC 流程。简化模式专门服务于机器对机器的信任传递,不能替代标准的用户认证流程。

三、OIDC 服务器的密钥对数量有限制吗?

OIDC 协议本身不限制密钥对的数量。

根据 RFC 7517 (JSON Web Key) 规范,OIDC Provider 通过 /keys(即 JWKS endpoint)向外暴露公钥集合,这个集合是一个 JSON 数组,可以包含任意数量的密钥:

1
2
3
4
5
6
7
{
  "keys": [
    { "kid": "key-v1", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..." },
    { "kid": "key-v2", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..." },
    { "kid": "key-v3", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..." }
  ]
}

验证方(如 AWS STS)拿到 JWT 后,通过 JWT Header 中的 kid(Key ID)字段匹配到 JWKS 中对应的公钥进行验签。

这种设计天然支持密钥轮转:你可以同时发布新旧两把密钥,新签发的 Token 用新密钥签名,而已签发的旧 Token 仍可被旧密钥验证,直到旧 Token 全部过期后再下线旧密钥。

oidc.cipherhub.cloud 的实现中,通过 keys.yaml 配置多密钥:

1
2
3
4
5
6
keys:
  - kid: cipherhub-key-v1
    path: private_key.pem
    default: true
  - kid: cipherhub-key-v2
    path: private_key_v2.pem

其中 default: true 指定签发新 Token 时使用哪把密钥,而 /keys 接口会返回所有密钥的公钥参数。

四、OIDC 服务器只能使用 RSA 密钥吗?

不是。OIDC 协议支持多种密钥类型。

OpenID Connect Core 和底层的 RFC 7518 (JWA) 定义了以下签名算法族:

算法族典型算法密钥类型 (kty)说明
RSASSA-PKCS1-v1_5RS256, RS384, RS512RSA最广泛支持
RSASSA-PSSPS256, PS384, PS512RSARSA 的更安全填充方案
ECDSAES256, ES384, ES512EC基于 NIST 曲线的椭圆曲线签名
EdDSAEdDSA (Ed25519)OKPRFC 8037 扩展,部分平台支持

所以从协议层面看,ECC 密钥(如 P-256 对应 ES256)完全可以用于 OIDC。实际上,许多现代 OIDC Provider(如 Auth0、Google)都支持 ECDSA 签名。

SM2 密钥呢?

SM2 是中国国家密码管理局发布的椭圆曲线公钥密码算法。从纯技术角度看,JWT 的签名机制是可扩展的(可以自定义 alg 值),因此理论上可以用 SM2 密钥签发 JWT。但存在两个现实障碍:

  1. 标准兼容性:SM2 不在 RFC 7518 定义的算法列表中,主流 JWT 库(如 python-jose、nimbus-jose-jwt)没有内置 SM2 支持,需要自行实现签名与验签逻辑。
  2. 验证方支持:AWS、Google Cloud 等国际云平台只接受标准 JWA 算法。如果你的验证方不认识 SM2 签名的 Token,整个流程就无法工作。

结论:如果验证方在你自己的控制范围内,且双方都支持 SM2,则技术上可行;但在与 AWS 等国际云平台对接的场景下,应使用 RSA 或 ECDSA。

为什么 oidc.cipherhub.cloud 只用了 RSA?

因为我们的主要对接方是 AWS STS。

AWS AssumeRoleWithWebIdentity 文档明确列出支持的算法包括 RS256(以及 RS384、RS512),这是安全的选择,当然也可以拓展到 ECC 密钥。

Clipboard_Screenshot_1772335542

五、不同类型或不同长度的密钥可以混用吗?

可以,但取决于验证方的支持程度。

JWKS 中的每个密钥条目都有独立的 alg(算法)和 kty(密钥类型)声明。

验证方通过 JWT Header 中的 kid 找到对应的密钥后,再根据该密钥的 alg 进行验签。

从协议上看,以下混用是完全合法的:

1
2
3
4
5
6
7
{
  "keys": [
    { "kid": "rsa-2048-v1", "kty": "RSA", "alg": "RS256", ... },
    { "kid": "rsa-4096-v2", "kty": "RSA", "alg": "RS256", ... },
    { "kid": "ec-p256-v1",  "kty": "EC",  "alg": "ES256", "crv": "P-256", ... }
  ]
}

RSA 不同密钥长度混用

RSA-2048 和 RSA-4096 混用完全没问题,它们使用相同的算法标识 RS256(都是 RSASSA-PKCS1-v1_5 + SHA-256),只是模长不同。

验证方无需关心密钥长度——公钥的 n 参数本身就携带了长度信息。

oidc.cipherhub.cloud 支持 2048、3072、4096 三种 RSA 模长。

不同密钥可以各自选择不同的长度,在 /keys 接口中统一返回。

RSA 与 ECC 混用

这在协议上可行,但需要注意:

  • 你的 /.well-known/openid-configuration 中的 id_token_signing_alg_values_supported 需要同时声明 RS256ES256
  • 验证方必须同时支持这些算法。以 AWS 为例,它支持 RS256/RS384/RS512,但如果它不支持 ES256,那么用 EC 密钥签名的 Token 就无法在 AWS 侧验证。

六、OIDC 服务器是如何生成 Token 的?

Token 的本质:JWT

OIDC 中的 id_token 是一个 JWT (JSON Web Token),它由三段 Base64url 编码的字符串用 . 连接而成:

1
eyJhbGciOiJSUzI1NiIsImtpZCI6ImNpcGhlcmh1Yi1rZXktdjEifQ.eyJpc3MiOiJodHRwczovL29pZGMuY2lwaGVyaHViLmNsb3VkIiwiYXVkIjoiY2lwaGVyaHViLWNsaWVudCIsInN1YiI6ImNpcGhlcmh1Yi13YXlmYXJlciIsImlhdCI6MTc0MDgyMzQ1NiwiZXhwIjoxNzQwODI3MDU2fQ.SIGNATURE_BYTES

三段分别是:

1. Header(头部)

1
2
3
4
{
  "alg": "RS256",
  "kid": "cipherhub-key-v1"
}
  • alg:签名算法,告诉验证方该用什么算法验签。
  • kid:密钥 ID,告诉验证方该去 JWKS 中找哪把公钥。

2. Payload(载荷)

1
2
3
4
5
6
7
{
  "iss": "https://oidc.cipherhub.cloud",
  "aud": "cipherhub-client",
  "sub": "cipherhub-wayfarer",
  "iat": 1740823456,
  "exp": 1740827056
}
声明含义要求
iss (Issuer)签发者必须与 AWS 中注册的 Provider URL 完全一致
aud (Audience)受众必须与 AWS 中注册的 Audience 完全一致
sub (Subject)主体标识 Token 所属的用户或实体
iat (Issued At)签发时间Unix 时间戳
exp (Expiration)过期时间Unix 时间戳,超过此时间 Token 失效

3. Signature(签名)

1
2
3
4
RSASHA256(
  base64urlEncode(header) + "." + base64urlEncode(payload),
  private_key
)

签名过程:将 Header 和 Payload 各自 Base64url 编码后用 . 拼接,再用 RSA 私钥对这段字符串做 SHA-256 哈希签名。

签发流程

oidc.cipherhub.cloud 中,一次完整的 Token 签发流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
客户端                              OIDC 服务器
                                        
    GET /generate-token                 
    ?audience=cipherhub-client          
    &timestamp=1740823456               
    &nonce=<32字节随机数的Base64>        
    &signature=<对上述参数的RSA签名>      
   ──────────────────────────────────>   
                                         1. 验证 audience 已注册
                                         2. 验证 timestamp  ±3s 
                                         3.  audience 证书验签
                                         4. 构造 JWT payload
                                         5.  OIDC 私钥签名
       { "id_token": "eyJ..." }         
    <──────────────────────────────────  

QQ_1772335684783

七、Token 是如何被 AWS 使用的?

AWS 验证 Token 的完整流程

当你调用 aws sts assume-role-with-web-identity --web-identity-token "$TOKEN" 时,AWS 并不是盲目信任这个 Token。它会执行一套严格的验证流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
你的应用                AWS STS                    oidc.cipherhub.cloud
  │                       │                              │
  │ AssumeRoleWith        │                              │
  │ WebIdentity(token)    │                              │
  │ ─────────────────>    │                              │
  │                       │  GET /.well-known/           │
  │                       │  openid-configuration        │
  │                       │  ───────────────────────>    │
  │                       │  { jwks_uri, issuer, ... }   │
  │                       │  <───────────────────────    │
  │                       │                              │
  │                       │  GET /keys                   │
  │                       │  ───────────────────────>    │
  │                       │  { keys: [{ kid, n, e }] }   │
  │                       │  <───────────────────────    │
  │                       │                              │
  │                       │ 1. 解码 JWT Header,取 kid    │
  │                       │ 2. 在 JWKS 中匹配公钥         │
  │                       │ 3. 验证签名                   │
  │                       │ 4. 检查 iss == Provider URL   │
  │                       │ 5. 检查 aud == 注册的 Audience │
  │                       │ 6. 检查 exp > 当前时间         │
  │                       │ 7. 检查信任策略 Condition      │
  │                       │                              │
  │  临时凭证              │                              │
  │  (AccessKeyId,        │                              │
  │   SecretAccessKey,    │                              │
  │   SessionToken)       │                              │
  │ <─────────────────    │                              │

AWS 使用 Token 的前提条件

  1. OIDC Provider 已注册:在 IAM 中创建了 Identity Provider,URL 指向你的服务。

    Clipboard_Screenshot_1772335765

  2. HTTPS 可达:AWS 需要能通过公网 HTTPS 访问你的 /.well-known/openid-configuration/keys 接口。

    QQ_1772335797476

  3. TLS 证书受信任:必须由公共 CA 签发,AWS 不接受自签名证书。

    QQ_1772335888579
  4. Issuer 精确匹配:Token 中的 iss 必须与注册时的 Provider URL 完全相同(包括协议头、无尾部斜杠)。

    Clipboard_Screenshot_1772335962
  5. Audience 匹配:Token 中的 aud 必须与 IAM 角色信任策略中配置的 Audience 一致。

  6. Token 未过期exp 必须大于 AWS 服务器当前时间。

  7. 签名可验:JWT Header 中的 kid 必须能在 /keys 返回的 JWKS 中找到对应公钥,且签名验证通过。

八、Fingerprint 与密钥变更:它们是一回事吗?

这是一个容易混淆的概念,需要区分两种"密钥":

概念所属层级用途
TLS 证书指纹 (Fingerprint)传输层 (HTTPS)确认 AWS 连接的是正确的服务器
JWT 签名密钥应用层 (OIDC)对 id_token 签名,验证 Token 完整性

Fingerprint 是什么?

在 AWS IAM 控制台注册 OIDC Provider 时,AWS 会连接你的服务器,计算 TLS 证书链中根证书(或中间证书)的 SHA-1 哈希,这就是 Thumbprint / Fingerprint

它的作用是:确保 AWS 后续访问你的 OIDC 接口时,连接到的确实是你的服务器,而非中间人。

OIDC 签名密钥变化会怎样?

不影响 Fingerprint,但会影响 Token 验证。

当你轮换 JWT 签名密钥时(例如从 key-v1 换到 key-v2),只要:

  1. /keys 接口同时返回新旧两把公钥
  2. 新签发的 Token 使用新密钥的 kid
  3. 旧 Token 过期前旧公钥仍在 JWKS 中

AWS 就能正常工作,因为 AWS 每次验证 Token 时都会实时请求 /keys 获取最新的公钥集合。

九、Token 的有效期由谁决定?

由 OIDC 服务器决定,但 AWS 有自己的上限约束。

OIDC 服务器决定 Token 的 exp

Token 中的 exp(Expiration Time)字段在签发时由 OIDC 服务器写入。

oidc.cipherhub.cloud 中,默认有效期为 3600 秒(1 小时)

1
DEFAULT_TOKEN_TTL_SECONDS: Final[int] = 3600

exp = iat + 3600,即签发后 1 小时过期。你可以根据需要调整这个值。

AWS 对换证有自己的约束

即使你的 Token 有效期设为 24 小时,AWS STS 换取的临时凭证也有独立的有效期限

  • AssumeRoleWithWebIdentity 默认返回 1 小时有效的临时凭证
  • 可通过 --duration-seconds 参数调整,范围为 900 秒(15 分钟)到 角色最大会话时长(默认 1 小时,最大可配 12 小时)

换句话说:

  • Token 的有效期 → OIDC 服务器说了算,决定这个 Token 多久内能用来换证
  • 临时凭证的有效期 → AWS 说了算,决定换到的 AccessKey 多久后失效

两个有效期是独立的。Token 过期后就不能再拿它去换新的凭证,但已换到的凭证在其自身有效期内仍可继续使用。

十、OIDC 服务器的安全性如何保障?

自建 OIDC Provider 意味着你接管了"信任根"——如果 Provider 被攻破,攻击者就能签发任意 Token 换取 AWS 权限。以下是必须满足的安全条件:

必备条件

1. HTTPS + 受信任证书

这是最基本的要求。所有 OIDC 接口必须通过 HTTPS 提供服务,且证书由公共 CA 签发。这不仅是 AWS 的硬性要求,也是防止中间人窃听和篡改的基础。

2. 私钥严格保护

OIDC 服务器的 RSA 私钥是整个信任链的核心。必须做到:

  • 不将私钥提交到版本控制系统
  • 限制私钥文件的文件系统权限(如 chmod 600
  • 生产环境考虑使用 HSM 或密钥管理服务存储

3. /generate-token 接口必须鉴权

这是安全的核心要求。 如果任何人都能调用 /generate-token 获取 Token,那么 OIDC 的信任模型就形同虚设。未鉴权的 /generate-token 等价于把 AWS 凭证开放给了公网。

4. Issuer 域名固定且专用

Issuer URL 一旦在 AWS 中注册就不应变更,且该域名应专用于 OIDC 服务,不要在同一域名下暴露其他无关服务。

5. 最小权限原则

OIDC 角色在 AWS 中的权限应尽可能收窄:

  • 信任策略中限定具体的 aud
  • 权限策略中限定具体的资源 ARN,避免使用 *
  • 考虑在信任策略中增加 sub 条件,限制特定主体才能换证

建议条件

  • 日志审计:记录每一次 Token 签发的客户端、audience、时间

  • 速率限制:对 /generate-token 接口设置请求速率限制

  • 密钥定期轮转:定期更换 JWT 签名密钥,降低密钥泄露后的影响范围

  • 网络隔离:如果 OIDC 服务仅供内部应用使用,考虑通过网络策略限制访问来源(但 /keys/.well-known/openid-configuration 必须对 AWS 可达)

十一、本次安全增强设计详解

相比上一版/generate-token 无任何鉴权的"裸奔"状态,本次重新设计的 OIDC 服务引入了一套完整的客户端身份验证机制。以下是各项安全增强及其工作原理。

1. 基于证书的客户端身份验证

每个 Audience(调用方)都有自己的 RSA 密钥对和自签名证书。注册流程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
audience_client generate --name cipherhub-client
  ┌──────────────────────────────┐
  │  生成 RSA 密钥对               │
  │  生成自签名 X.509 证书          │
  │  生成随机 32 字符密码           │
  │  私钥用密码加密存储             │
  └──────────────────────────────┘
  cipherhub-client/
  ├── cipherhub-client_private_key.pem   ← 客户端持有(加密)
  ├── cipherhub-client_certificate.pem   ← 注册到服务端
  └── cipherhub-client_password.txt      ← 客户端持有

服务端只保存证书(公钥),客户端持有加密的私钥和密码。

即使服务端被入侵,攻击者也无法伪造客户端签名。

2. 请求签名机制

每次调用 /generate-token 时,客户端必须提供四个鉴权参数:

参数说明
audience调用方身份标识
timestamp当前 Unix 时间戳(秒)
nonce32 字节随机数的 Base64 编码
signatureaudience|timestamp|nonce 的 RSA-SHA256 签名的 Base64 编码

服务端验证流程:

1
2
3
4
5
6
1. 检查 audience 是否在 audiences.yaml 中注册
2. 检查 timestamp 是否在当前时间 ±3 秒以内
3. 加载该 audience 的证书,提取公钥
4. 重新构造待签名消息:audience|timestamp|nonce
5. 用公钥验证 signature 是否合法
6. 全部通过 → 签发 id_token

3. 时间戳防重放

时间戳容忍窗口仅为 ±3 秒

1
TIMESTAMP_TOLERANCE_SECONDS: Final[int] = 3

这意味着即使攻击者截获了一个合法请求,它在 3 秒后就会失效。配合每次请求的随机 nonce,同一个签名被重用的概率大大降低。

4. 随机 Nonce

每次请求都包含 32 字节的密码学安全随机数:

1
nonce_bytes = secrets.token_bytes(32)

Nonce 参与签名计算,确保每次请求的签名都不同,即使 audience 和 timestamp 相同。

5. 客户端私钥加密保护

客户端的 RSA 私钥不以明文存储,而是使用一个随机生成的 32 字符 ASCII 密码加密:

1
2
3
4
password = _random_ascii_password(32)  # 字母+数字+标点
private_key.private_bytes(
    encryption_algorithm=serialization.BestAvailableEncryption(password_bytes),
)

私钥和密码分文件存储,即使其中一个文件泄露也无法直接使用。

6. 动态 Audience 管理

audiences.yaml 在每次请求时实时读取,无缓存:

  • 新增 audience:添加证书和配置后立即生效,无需重启服务
  • 吊销 audience:从配置中删除该条目后,下一次请求即被拒绝

这种设计在发现某个 audience 的私钥泄露时,可以立即切断其访问。

7. 多密钥支持与密钥轮转

通过 keys.yaml 配置多把 JWT 签名密钥,支持平滑的密钥轮转:

  1. 生成新密钥,添加到 keys.yaml 并标记为 default: true
  2. 旧密钥保留在列表中但不再用于签名
  3. 等待所有旧 Token 过期后,从列表中移除旧密钥

8. 审计日志

所有 OIDC 关键路径(/.well-known/openid-configuration/keys/generate-token)的请求都会记录:

  • 客户端 IP(支持 X-Forwarded-For,但仅信任本机代理)
  • 请求路径与方法
  • 响应状态码
  • Token 签发时的 sub 和 audience

日志文件按大小轮转(单文件最大 128MB,最多保留 3 个备份),防止磁盘被撑满。

9. 受信任代理限制

在解析客户端真实 IP 时,仅当直连方为本机(127.0.0.1 / ::1)时才信任 X-Forwarded-For 头,防止外部请求伪造来源 IP:

1
_TRUSTED_PROXY_HOSTS: Final[frozenset[str]] = frozenset({"127.0.0.1", "::1"})

十二、常见故障排查

在实际部署和调试 OIDC + AWS 联合身份时,以下是最常见的故障及其排查方法。

1. iss 不匹配

症状:AWS 返回 InvalidIdentityToken: Token issuer does not match provider

这是最高频的问题。iss 必须与 AWS IAM 中注册的 Provider URL 字符串完全一致,以下差异都会导致失败:

注册的 Provider URLToken 中的 iss结果
https://oidc.cipherhub.cloudhttps://oidc.cipherhub.cloud通过
https://oidc.cipherhub.cloudhttps://oidc.cipherhub.cloud/失败(多了尾部斜杠)
https://oidc.cipherhub.cloudhttp://oidc.cipherhub.cloud失败(协议不同)
https://oidc.cipherhub.cloudhttps://OIDC.cipherhub.cloud失败(大小写不同)

排查:解码 Token 的 Payload 段检查 iss 值:

1
echo "eyJpc3M..." | base64 -d 2>/dev/null | python3 -m json.tool

2. aud 不匹配

症状:AWS 返回 InvalidIdentityToken: Incorrect token audience

Token 中的 aud 必须与 IAM 角色信任策略中 Condition.StringEquals 配置的值一致。

常见错误:

  • 信任策略中写的是 oidc.cipherhub.cloud:aud,值为 cipherhub-client
  • 但请求 Token 时 audience 参数写成了 cipherhub_client(下划线 vs 连字符)

排查:同时检查 Token 的 aud 和 IAM 角色的信任策略。

3. Token 过期

症状:AWS 返回 ExpiredTokenException

id_token 签发后有效期为 3600 秒。如果你获取 Token 后等待太久才去换证,Token 就会过期。

排查:解码 Token 检查 expiat,与当前时间对比。注意服务器时钟偏差——如果 OIDC 服务器和 AWS 之间存在显著的时钟差异,即使 Token 理论上未过期也可能被拒绝。

4. kid 在 JWKS 中找不到

症状:AWS 返回 InvalidIdentityToken: OpenIDConnect provider's HTTPS certificate doesn't match configured thumbprint 或签名验证失败

当你轮换了 JWT 签名密钥但忘记在 /keys 中保留旧密钥,或者新密钥的 kid 与 Token Header 中的 kid 不一致时就会出现。

排查

  1. 解码 Token Header,查看 kid
  2. 请求 /keys 接口,确认返回的 JWKS 中包含该 kid
1
2
3
4
5
# 查看 Token Header 中的 kid
echo "eyJhbGci..." | base64 -d 2>/dev/null | python3 -m json.tool

# 查看 JWKS
curl -s https://oidc.cipherhub.cloud/keys | python3 -m json.tool

5. OIDC 服务不可达

症状:AWS 返回 IDPCommunicationError: Could not contact the OpenID provider

AWS 需要从其数据中心访问你的 OIDC 服务器。常见原因:

  • 服务器防火墙未放行 443 端口
  • DNS 解析失败
  • TLS 证书过期或无效
  • 服务进程未运行

排查:从外网测试接口是否可达:

1
2
curl -s https://oidc.cipherhub.cloud/.well-known/openid-configuration | python3 -m json.tool
curl -s https://oidc.cipherhub.cloud/keys | python3 -m json.tool

6. 客户端签名验证失败

症状/generate-token 返回 401 signature verification failed

这不是 AWS 端的问题,而是客户端调用 OIDC 服务时鉴权失败。常见原因:

  • 客户端私钥与服务端注册的证书不匹配(重新 generate 了但没更新服务端证书)
  • 密码文件被修改,导致私钥无法正确解密
  • 客户端与服务端时钟偏差超过 3 秒

排查

  1. 确认 audiences.yaml 中的证书是与客户端私钥配对的那份
  2. 检查客户端与服务端的系统时间是否同步(date -u
  3. 查看服务端日志中的详细错误信息

7. 时间戳超限

症状/generate-token 返回 401 timestamp out of range

客户端和服务端的系统时钟差异超过了 ±3 秒的容忍窗口。

排查:在两台机器上分别运行 date -u 对比时间,使用 NTP 同步时钟:

1
2
3
4
5
# macOS
sudo sntp -sS time.apple.com

# Linux
sudo ntpdate -u pool.ntp.org

故障排查速查表

错误信息关键词检查项
issuer does not matchToken 的 iss 与 Provider URL 是否完全一致
Incorrect token audienceToken 的 aud 与信任策略中的 audience 是否一致
ExpiredTokenExceptionToken 是否过期、服务器时钟是否同步
HTTPS certificate / thumbprintTLS 证书是否有效、Thumbprint 是否需要更新
Could not contactOIDC 服务是否可达、443 端口是否放行
signature verification failed客户端证书与私钥是否匹配
timestamp out of range客户端与服务端时钟是否同步

十三、与 GitHub Actions OIDC 的对比

GitHub Actions 从 2021 年底开始支持 OIDC,允许 CI/CD 工作流通过 OIDC Token 换取 AWS 临时凭证,从而消除在 GitHub 中存储长期 AWS 密钥的需要。它和 oidc.cipherhub.cloud 扮演着完全相同的角色——OIDC Provider。

架构对比

1
2
3
4
5
6
7
GitHub Actions 方案:
GitHub Runner  ──(OIDC Token)──>  AWS STS  ──(临时凭证)──>  AWS 资源

oidc.cipherhub.cloud 方案:
你的服务器    ──(OIDC Token)──>  AWS STS  ──(临时凭证)──>  AWS 资源
     └── 从 oidc.cipherhub.cloud 获取 Token

两者在 AWS 侧的工作方式完全一致:注册 OIDC Provider → 创建 IAM 角色 → 配置信任策略 → 用 Token 换凭证。

关键差异

维度GitHub Actions OIDCoidc.cipherhub.cloud
Provider URLhttps://token.actions.githubusercontent.comhttps://oidc.cipherhub.cloud
谁签发 TokenGitHub 平台自动注入我们自己的服务签发
Token 获取方式Runner 环境变量自动可用客户端签名后请求 /generate-token
适用场景CI/CD 流水线任意服务器/应用
信任根GitHub(你信任 GitHub 平台)你自己(你信任自己的服务器)
可控性不可控(GitHub 管理密钥和签发逻辑)完全可控(密钥、策略、鉴权全在手中)

Token Claims 对比

GitHub Actions 签发的 JWT 包含丰富的 CI/CD 上下文信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "iss": "https://token.actions.githubusercontent.com",
  "aud": "sts.amazonaws.com",
  "sub": "repo:cipherhub/my-repo:ref:refs/heads/main",
  "repository": "cipherhub/my-repo",
  "repository_owner": "cipherhub",
  "workflow": "deploy",
  "ref": "refs/heads/main",
  "actor": "cipherhub-dev",
  "event_name": "push"
}

oidc.cipherhub.cloud 的 Token 则更简洁:

1
2
3
4
5
6
7
{
  "iss": "https://oidc.cipherhub.cloud",
  "aud": "cipherhub-client",
  "sub": "cipherhub-wayfarer",
  "iat": 1740823456,
  "exp": 1740827056
}

信任策略的精细度差异

得益于 GitHub Token 中丰富的 Claims,AWS 信任策略可以做到非常精细的控制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
    },
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:cipherhub/my-repo:ref:refs/heads/main"
    }
  }
}

这可以限制只有特定仓库的特定分支上的 Actions 才能换取凭证。

oidc.cipherhub.cloud 目前通过 audsub 做条件限制,粒度相对较粗,但可以通过扩展 Token Claims 来增加更多维度的控制。

各自的优势场景

选 GitHub Actions OIDC:你的工作负载运行在 GitHub Actions 中,想要零密钥管理的 CI/CD 体验。

选自建 OIDC Provider

  • 你的工作负载运行在自己的服务器上,不在 GitHub Actions 中
  • 你需要完全掌控签发逻辑和密钥管理
  • 你需要对接多个不同的云平台或内部系统
  • 你有合规要求,不能将信任根交给第三方

两者也可以共存:GitHub Actions 用 GitHub 的 OIDC 获取 AWS 凭证做 CI/CD,你的生产服务器用 oidc.cipherhub.cloud 获取凭证做运行时调用,互不冲突。

总结

OIDC 不只是"给 AWS 签个 Token"这么简单。

它是一套建立在 OAuth 2.0 之上的完整身份传递机制:

  • 协议定位:OIDC 补全了 OAuth 2.0 缺失的身份层,SAML 是它的上一代替代品,SSO 是它们共同的应用目标
  • 流程选择:标准 OIDC 面向有用户交互的浏览器场景,机器对机器场景可以合理简化——但安全机制不能简化
  • 密钥管理决定了签名的强度与灵活性
  • Token 格式(JWT) 承载了身份声明与完整性证明
  • JWKS 端点让验证方能自主获取公钥,无需线下交换
  • 鉴权机制确保只有合法的调用方能获取 Token
  • TLS 与 Fingerprint保障了传输层的可信

自建 OIDC Provider 给了你完全的控制权,但也意味着安全责任完全在你手中。

本文讨论的每一项安全措施,都是在回答同一个问题:如何确保"只有该拿到 Token 的人,才能拿到 Token"。