审计员会按名称询问 21-compliance.md 第 14 节中的控制措施。此处介绍操作流程;流程生成的记录就是审计证据。
密钥
每个已存储凭据都会使用新生成的数据加密密钥(DEK)加密。DEK 由主密钥(KEK)包装;主密钥的引用会与其一起保存(key_ref,或打包值中的引用)。轮换主密钥时,会重新包装 DEK,但绝不会解密或重新加密凭据。
| 设置 | 含义 |
|---|---|
QUIRE_MASTER_KEY |
当前主密钥:32 字节、base64。所有新密钥都使用它包装 |
QUIRE_MASTER_KEY_VERSION |
版本标签;未设置时为 v1。每次更换密钥时递增 |
QUIRE_MASTER_KEY_RETIRED |
仍可能用于包装已存储密钥的旧密钥,格式如 v1=<base64>,v0=<base64>。仅用于读取,不会写入 |
Web 层、worker 和 bun run kek:rotate 命令都会读取这三个设置。它们的值必须一致,否则某个组件就无法解开其他组件包装的密钥。
如果未设置 QUIRE_MASTER_KEY,每个子系统会保留从 QUIRE_SECRET_KEY 派生的密钥。这种方式可以正常工作,系统健康页面会将其标记为降级状态;设置主密钥后,该派生密钥仍可读取,因此首次轮换会将所有内容从派生密钥迁出。任何能够读取进程环境的人员都能解密所有已存储凭据,因此生产环境应设置主密钥,将其保存在密钥存储服务中,并与数据库备份分开保存。
轮换
主密钥的使用时长会显示在“平台控制台”>“安全”>“主密钥”页面,并以天数记录在指标 quire.secrets.master_key.age 中。每日运行的 platform.key_age 计划(UTC 03:41)会在密钥达到 365 天时向平台审计链写入提醒记录,并在此后每 30 天再次提醒,直到密钥完成轮换。收到提醒时应轮换;如果密钥可能已泄露,也应立即轮换。
- 生成新密钥:
openssl rand -base64 32。 - 将
QUIRE_MASTER_KEY设为新值,并将QUIRE_MASTER_KEY_VERSION设为下一个标签(v2)。将旧密钥移到QUIRE_MASTER_KEY_RETIRED,格式为v1=<old base64>。在此主机之外保存新旧密钥的副本。 - 使用新设置部署 Web 层和 worker。现在所有新密钥都会用
env:QUIRE_MASTER_KEY:v2包装;旧密钥仍可通过退休密钥读取。 - 提交轮换请求,并说明将写入审计记录的原因:
- 控制台中选择“安全”>“主密钥”>“轮换主密钥”;或
- 使用相同环境的 shell 执行
bun run kek:rotate request --reason "Annual rotation, ticket SEC-114"。
- Worker 每分钟重新包装一批密钥(由 scheduler 的
platform.key_rotation计划执行),并会在重启后继续。要一次性完成,请执行bun run kek:rotate run。使用bun run kek:rotate status查看进度。 - 记录显示轮换完成,且未解决数和失败数都为零后,从
QUIRE_MASTER_KEY_RETIRED中移除退休密钥并重新部署。在此之前请保留它:无法迁移的值仍由旧密钥包装。
任务会处理哪些内容
任务会遍历所有存有已包装 DEK 的存储区,即 SEALED_STORES(apps/worker/src/key-rotation.ts)中的项目。控制数据库存储区会在控制数据库上遍历;组织存储区会在行级安全机制下逐个组织遍历,所用数据库取决于组织所在位置,因此固定到专用数据库的租户会在该数据库内完成轮换。新增已包装密钥列却未列入清单时,某项测试会失败;凭据审查将某个已密封列分类为需要轮换,但清单遗漏时,另一项测试也会失败。
记录
ops.key_rotation:每次轮换对应一行,记录原因、请求人、状态和统计数据(已重新包装、已是最新、未解决、失败)。ops.key_rotation_progress:每个存储区和范围处理后记录一行,其中包含无法读取的密钥引用及每种引用对应的值数量。恢复轮换时会跳过这些记录。- 平台审计链:
platform/key_rotation_request(含原因)、每个存储区对应的platform/key_rotation_store(含计数),以及platform/key_rotation_complete或platform/key_rotation_fail;提醒会生成platform/key_age_reminder。 - 指标:
quire.secrets.master_key.age和quire.secrets.rewrap.outstanding(上次轮换无法迁移的值)。
存在未解决值时
未解决值是指由本安装不持有的密钥引用包装的值,或其格式与列定义不符的值。进度记录会指出密钥引用(例如 env:QUIRE_MASTER_KEY:v0 (unreadable))。将该密钥重新加入 QUIRE_MASTER_KEY_RETIRED 并再次运行轮换;如果密钥永久丢失,则请组织管理员重新填写凭据,系统会使用当前密钥重新包装。如果轮换失败,记录中会显示错误;请修复原因后重新提交请求。
签名密钥
组织使用各自的 RSA 密钥为 OpenID Connect token 和 LTI 消息签名,并在 /.well-known/jwks.json 发布公钥。此项工作不需要运维人员操作。每小时运行的 platform.signing_keys 计划会在当前密钥的 90 天有效期结束前七天发布后继密钥;一周后,后继密钥开始签名,旧密钥进入退役状态;再过 90 天,旧密钥会被删除并从密钥集中移除。每个步骤都会作为 platform/signing_key_advance 写入平台审计链。
如需提前更换某组织的密钥(例如发生泄露):
- 在控制台中选择“安全”>“主密钥”>“发布新的签名密钥”(需要
platform/keys_manage);或 - 在使用 worker 环境的 shell 中执行以下命令:
bun run kek:rotate signing-keys rotate --tenant <slug or id> --reason "Key exposed, INC-3310".bun run kek:rotate signing-keys status会按阶段列出所有组织的密钥。
新密钥会立即发布,并在七天后开始签名,此时当前密钥会退役。特意保留这一周的间隔,是因为依赖方会缓存密钥集,重叠期太短会让所有工具同时失败。退役密钥会继续保留在密钥集中 90 天,以便已使用该密钥签名的 token 继续通过验证。如果泄露情况要求更早停止信任该密钥,可由运维人员使用自己的数据库访问权限、依据变更记录删除对应行(紧急访问机制是只读的);此后由该密钥签名的 token 将无法通过验证。强制轮换会以含原因的 platform/signing_key_rotate 记录写入审计链。Worker 必须使用与 Web 层相同的 QUIRE_MASTER_KEY 设置才能包装新密钥;运行 bun run kek:rotate 轮换主密钥时,也会重新包装签名密钥及其他密钥(oauth_signing_key 位于 SEALED_STORES 中)。
生产环境紧急访问
没有人拥有生产环境的常驻访问权限。遇到无法等待的情况时,由所有者签发紧急访问授权:在“平台控制台”>“安全”>“紧急访问”中操作。
- 授权范围可以是一家组织或平台注册表;原因至少 20 个字符,且需注明事故或工单;有效期为 5 到 240 分钟。授权会自动过期,每条语句执行时都会检查时钟。
- 可签发给签发授权的所有者本人,也可签发给另一位所有者(双人流程)。只有授权指定的人员可以使用。签发需要
platform/break_glass_issue,使用需要platform/break_glass_use;默认情况下这两项权限仅授予所有者。 - 语句通过网关运行,而非直接使用数据库登录:只读、逐条执行、范围限定为指定组织或控制注册表、超时为五秒,最多返回 500 行。二进制值只显示大小。
- 平台审计链会记录签发(含原因)、撤销、每条语句(执行前记录,事件为
platform/break_glass_statement;拒绝的语句结果为denied)和每条结果(platform/break_glass_result)。ops.break_glass_statement会保存审计记录 ID,因此授权记录可关联到对应审计条目。 - 不提供写入操作。如果某项更改无法等到发布,应在产品之外使用运维人员自己的数据库访问权限,并建立专属变更记录;记录中应引用此处使用的事故编号。
为什么不直接签发数据库凭据:Postgres 登录权限会在申请该权限的会话结束后继续有效,会绕过应用依赖的行级安全,也无法写入本产品的审计链;因此,相关语句只能在有人交付服务器日志的范围内审计。网关让审计记录成为访问流程本身的一部分,而不是依赖流程外的惯例。
处理审计请求时,请列出该时间段的授权(“紧急访问”页面),打开某项授权的历史以查看语句和审计记录 ID,再到平台审计链读取这些记录(运行 bun run audit:verify --platform 可证明审计链完整)。