跳转到主要内容

持久化 IAM 撤销

SILO 保留已删除 IAM 记录的版本(Revision),使离线站点在重新连接后无法将已删除的旧身份或旧授权恢复为有效状态。该机制覆盖内置用户、服务账号、组、策略文档,以及用户、STS 父身份和组各自命名空间中的策略映射。内置用户被删除后,即使管理员有意重建同名用户,此前签发的旧服务账号、STS 临时凭证与组授权也不会因此复活。

持久化 IAM 撤销架构(PR #191、#192,提交 f99ed829b5eba549160725f035156c9e020b6a07)已随 Server 20260916 发布并关闭 SN-2026-013;针对直接 RevokeTokens 的按范围 STS 撤销扩展(0cd388c66)已随 Server 20261018 发布。多站点协调升级步骤见 IAM 升级与恢复手册。

十月十八日:按范围撤销 STS

Server 20261018 中的提交 0cd388c66 将九月持久化撤销机制扩展到直接 RevokeTokens 操作。

在旧实现中,节点可能漏掉某次签发通知,进而在执行撤销时只清理自身内存缓存中已有的令牌。修复后,RevokeTokens 会读取持久化的 STS 身份记录并与内存缓存合并,按操作截点(Cutoff)筛选指定父身份及可选令牌类型,为每个命中的不可变令牌 key 提交独立的删除墓碑(Tombstone)。即使批次中某个 key 保存失败,所有已提交的 key 仍会立即通知同集群兄弟节点并发送给站点复制对端,同时向调用者返回保存错误以便重试。

私有 IAM 复制消息携带令牌删除标记与过期时间。即使撤销消息先于创建消息抵达,墓碑仍能阻止随后的重放;带有更晚事件时间戳的创建消息也无法复活这一不可变令牌。既有删除自愈(Deletion Healing)机制负责离线重投。对象存储和 etcd 的完整 IAM 刷新会重建 STS 修订记录——即使并发 IAM 写入推迟了整表缓存替换,也会先从活动缓存中剔除已撤销条目。墓碑保留至令牌自然到期时间加上时钟偏差容限;若过期时间未知则采取保守保留。

单次撤销快照覆盖请求集群在截点之前已知的 STS 身份;在孤立站点签发且从未复制到本集群的令牌不在该快照内。截点之后新签发的 key、无关用户及可复用的父身份策略映射不受影响。所有参与节点须协调升级,旧版本节点未实现新增的私有 STS 复制消息。

执行开销: 管理端撤销会枚举持久化的 STS 身份并按父身份与类型过滤,其读取量与临时映射内存随活跃令牌数量线性增长。扫描期间会释放鉴权缓存锁,但仍持有 IAM 写串行锁,因此其他并发 IAM 写操作可能短暂等待。etcd 既有的周期性刷新也会读取 STS 记录。普通缓存命中鉴权不增加任何网络调用,也未引入新的后台定时器或外部依赖。

回归测试位于 iam-sts-revocation_test.go,覆盖三站点断连/重启恢复以及四进程共享 IAM 存储场景。升级后应逐端点验证旧令牌被拒绝、新令牌继续有效以及节点重启后的状态恢复,详见升级流程。

排序与持久化

删除记录保存在原 IAM 配置路径下,包含源时间戳、Deleted 标志以及必要的 RevokedBefore 边界;身份删除记录不保存 secret key 或 session token。常规 IAM 列表查询与策略求值自动隐藏已删除记录。对象存储与 etcd 均通过分布式锁串行化每个配置路径的版本比较与写入;接收端直接持久化源时间戳,绝不将其替换为接收节点的本地时钟。

版本演进遵循以下确定性规则:

  • 旧事件不能覆盖新版本;相同时间戳下,删除记录优先于活记录。
  • 有意的本地同名重建会获得比已存储删除记录更新的版本号。
  • 若旧的用户或组删除消息在同名重建之后才延迟到达,系统会更新并保留其 RevokedBefore 撤销边界,同时完整保留较新的重建活记录。
  • 重复收到已应用的 tombstone 不会触发重写或推进版本号。
  • 节点冷启动加载持久化 IAM 状态时同样执行上述合并规则。

用户或组的版本写入是删除操作的权威提交点,策略映射与子凭证清理在其后执行。即使后续附属清理失败,也不会回滚已提交的父身份撤销;API 会向调用方返回清理错误,但仍会通知同后端兄弟节点重载已提交的撤销状态。因此,管理接口返回 HTTP 500 清理错误并不代表旧身份仍然有效。管理员可在暂停同名重建与其他 IAM 变更的前提下安全重试该删除操作。当发生“撤销已提交但附属清理失败”时,Admin Handler 不会触发即时跨站点推送钩子,而是交由常规后台删除自愈循环(Deletion Healing)重试跨站点传播。同后端兄弟节点收到删除通知后会重读当前共享状态,因此延迟到达的通知不会误删随后合法重建的同名身份;etcd 部署下兄弟节点还会通过 watch 机制接收持久化变更。

运维上必须保持各节点时钟同步并监控时钟偏移。版本排序基于源端墙钟时间戳,并对同一路径的本地连续写入保证单调递增,但不跨站点建立并发写入的因果偏序。

用户、子凭证与组

同名重建后的用户会保留历史 RevokedBefore 边界。新创建的服务账号与内置 STS 临时凭证均携带经加密签名的 siloParentRevocation 声明(Claim),记录签发时刻已知的父身份撤销边界。编辑或重放旧子凭证不会更新这一签名声明;因此即使旧子凭证自身的更新时间晚于父身份删除时间,鉴权时仍会被判定为失效,而针对重建后父身份新签发的子凭证则正常有效。凭证加载与写入时均从已验签的令牌中提取该声明。

在站点复制中,版本号更新的服务账号快照可以替换相同 AccessKey 的旧服务账号(包括有意更换所属父用户或轮换密钥),而本地直接重复创建相同 AccessKey 仍会被拒绝。复制快照会保留禁用状态(Disabled)与服务账号自身的撤销边界,防止旧策略映射重新附着到新服务账号上。相同版本号的重试仅触发重新加载已提交身份而不重复落盘。周期性活记录自愈(Live Healing)会直接比较源版本号(即使公开状态摘要未发生变化),并将已禁用身份一并纳入自愈源。

服务账号快照同时保留绝对过期时间,接收站点不会将本地新签发凭证的最短有效期规则重新套用到复制快照上。版本号更新但已过期的快照仍会覆盖旧密钥、在鉴权时被拒绝,并在常规加载流程中被归集为持久化服务账号墓碑。若缓存或声明加载失败,接收端会直接返回错误以触发重试,而不会提前确认成功;一旦替换提交完成,内存中陈旧的缓存密钥会被立即驱逐。若复制快照与现有内置用户或已缓存 STS 身份发生 AccessKey 类型冲突,系统会拒绝静默转换凭证类型并报错,交由管理员显式处置。

用户组的每个成员都拥有独立的 MemberGrants 授权时间戳。增删其他成员或切换组的启用状态不会刷新既有成员的授权时间。成员关系唯有在其 MemberGrants 时间戳同时晚于该用户与该组各自保留的 RevokedBefore 边界时才算有效;列表展示与 IAM 策略求值共用同一套有效成员判定逻辑。对端复制快照会保留原始授权时间戳(包括旧格式下的未知授权时间),不会把近期快照同步时间当作对已撤销身份的新授权;唯有管理员发起新的显式入组授权才能恢复其组权限。

继承自上游的活组快照合并逻辑仅作增量添加成员,不会自动消解站点断连期间漏掉的普通成员移出操作;本文机制聚焦于用户或组删除边界以及同名重建后的隔离保证。

保留期与过期回收

永久身份(内置用户、服务账号)、用户组、策略文档及策略映射的墓碑记录不设自动 TTL。断连的远端站点或旧备份可能在任意时长后重新接入;对端重放成功确认仅用于抑制重复网络发送,不作为物理回收墓碑历史的依据。

不可变 STS 临时令牌自然过期时,系统会物理删除其 token-key 记录及遗留映射,不会产生永久墓碑。而被提前显式撤销的 STS 令牌墓碑则会保留至该令牌原定过期时间加上时钟偏差容限,从而阻止带更晚事件时间戳的重放请求将其复活。etcd 后端利用原生过期租约(Lease)自动回收;对象存储后端则在既有的凭证加载与清理周期中顺带清理已过期的 STS 墓碑。过期时间未知的记录按保守策略持久保留。

可选的过期回收写入使用短锁超时预算并以尽力而为(Best-Effort)方式执行:若某次清理写入失败,当轮加载会停止可选回收、记录错误日志并继续加载正常用户;已过期凭证依然处于拒绝访问状态并保留既有持久化版本,留待下一轮加载重试。外部 IdP 身份禁用属于提前撤销而非自然过期,其关联的缓存 STS 与服务账号同样纳入清理。带过期时间的服务账号由于其 AccessKey 可被更新复用且旧版本可能未设过期时间,仍会保留持久化修订记录。

在九月 20260916 实现中,删除内置父用户通过持久化父边界即可使所有旧子凭证失效(含未出现在当前删除节点缓存中的子凭证);而跨站点逐令牌的直接 RevokeTokens 同步则由上文十月十八日按范围撤销 STS 补齐。

自愈循环、故障处理与运维成本

每个服务进程启动单个后台自愈循环(Healing Loop)。当节点失去分布式 Leader 租约时,循环暂停等待重新获得领导权,不会永久退出;配置热重载也不会派生额外循环。每次取得领导权后以及每轮自愈完成后等待 30 秒再启动下一轮。

常规 IAM 加载器在内存中维护一份不含敏感密钥的删除记录与撤销边界索引。自愈循环直接读取该内存索引,避免在每个 30 秒周期内重复全量遍历磁盘上的 config/iam/ 目录;仅在进程启动与全量 IAM 刷新时扫描包含墓碑在内的持久化存储记录。

自愈同步按每个对端每批最多 128 条记录发送。发送方记录每个对端已确认的路径与版本号,任一站点上无关路径的新变更不会重置既有确认进度。若某批次发送失败,该批次保持挂起待重试,后续独立批次仍可继续推进;响应丢失只会触发安全的幂等重放。单轮自愈设有总时长上限,超时前已收到的成功确认会被保留。同步协议会汇报节点名称与进程实例 ID:负载均衡器在已知实例间切换不会重置确认进度,唯有检测到全新进程实例首次出现时才会保守地作废旧确认并重新核对一次。因此在从备份恢复持久化状态前,必须先停止相关服务进程,防止恢复后误复用旧进程留下的内存确认记录。

稳态运行下,自愈循环仅遍历并排序内存中的保留集合、检查对端协议状态,并在对端确认后抑制重复的删除 PUT 请求。内存占用随保留路径数与对端站点数量线性增长,冷启动存储读取量随历史记录数增长。首次升级后,若部分存量活记录此前在各接收站点上被赋予了不同的本地时间戳,升级后会触发一轮初始版本对齐写入;规划维护窗口时应预留相应的存储写入与兄弟节点通知开销。

缓存凭证查找仅比对内存中的父版本索引,不产生额外存储读取;STS 签发与冷凭证加载则会核对持久化父版本。版本 I/O 与分布式锁等待期间会释放 IAM 缓存读/写锁,由独立的本地写入互斥锁(writeMu)维护本节点写入顺序,且所有存储与 etcd 锁/租约操作均受超时上下文约束(详见 IAM 策略重载:锁粒度、请求取消与超时预算隔离)。

复制协议与协调升级要求

服务端专用的版本化复制路由为 /minio/admin/v3/site-replication/peer/iam-revisions,在内部传输源版本号、成员授权时间戳以及独立的用户/组撤销条目,不改变公开 Admin 客户端 SDK 或 S3 API。旧版本服务端会拒绝该路由,发送方会直接报告同步失败,而不会降级回退到会丢失版本元数据的旧路由。既有旧 IAM 路由仍保留读取兼容。

集群中的所有参与节点(包括未启用跨站点复制但共享同一 IAM 存储后端的节点)必须统一升级,不支持同一后端下新旧二进制混跑或滚动降级,因为旧版本二进制无法识别墓碑记录与签名父边界。请按以下维护窗口步骤执行协调升级:

  1. 暂停所有 IAM 变更,并隔离状态未知的离线站点或旧备份。
  2. 完整备份各站点的底层 IAM 存储及所需 KMS/加密材料;常规在线 IAM Admin 导出不包含删除墓碑历史,不能替代底层存储备份。
  3. 停止共享同一 IAM 后端的全部节点,替换二进制文件后以新版本启动。所有参与站点完成升级后再恢复 IAM 写操作。
  4. 检查 IAM 加载日志、站点复制状态与撤销收敛指标,验证代表性的已撤销旧凭证被拒绝、新签发凭证正常可用。
  5. 显式处置升级前已遗失的撤销记录:若某些旧记录仍残留在部分站点,应在这些站点上再次执行删除;长期离线的陈旧站点应在重新接入前基于已批准的最新状态重建,切勿为了查看其旧凭证而直接将未知旧快照并网。

旧版本服务端曾为同名重建父身份签发的子凭证缺少签名父边界声明,升级后必须由新版本服务端重新签发;从未发生过删除与同名重建的父身份则完全保持既有凭证行为。

20261026 内置策略 Reset 更新: 未经修改的内置默认策略仍受保护,禁止本地直接删除。在 Server 20261026中,删除对内置策略的显式覆盖内容会立即恢复原生内置定义,并保留删除版本以拒绝旧覆盖回放(取代了 Server 20260916/20261018 阻止自动恢复默认策略的旧行为)。对端与 etcd 通知会重新加载已提交状态,保留延迟删除事件之后的新编辑;在用策略检查与自定义策略删除语义保持不变。本地删除不存在的策略保持幂等,不新增墓碑;对端同步来的未知策略删除则保留其版本记录。

如需回滚,必须先停止并隔离受影响站点,评估自备份以来的权限变更后再恢复兼容状态。恢复旧备份本身会丢失备份点之后的撤销记录,重新开放访问前必须完成权限核对与密钥轮换。切勿在线手动删除墓碑文件或仅转换活记录以强行启动旧二进制。

回归与性能验证

专项测试集位于 iam-revocation_test.go、iam-revision_test.go、iam-revision-lock_test.go、iam-revision-boundary_test.go、iam-replication-protocol_test.go、iam-credential-retention_test.go、iam-peer-reload_test.go 与 iam-replay_test.go。将环境变量 SILO_TEST_IAM_REVOCATION_ETCD 指向测试用 etcd 实例即可运行包含隔离命名空间的 etcd 生命周期、分布式锁与边界测试。

基准测试覆盖:

  • BenchmarkIAMCachedCredential 与 BenchmarkIAMSetTempUser:测量缓存鉴权与临时用户签发的基线开销。
  • BenchmarkIAMRevisionConvergedHealing:覆盖 1,000 与 10,000 条保留记录下的稳态内存索引遍历与零重复 PUT 断言。
  • BenchmarkIAMColdLoadExpiredServices:测量存在 100 或 1,000 个已过期可复用凭证时的冷启动加载与清理耗时(使用 -benchtime=1x 运行)。

错误语义与可观测性

十月二十三日补充(pgsty/silo#250):LDAP 长 DN:恢复登录可用性并守住 IAM 撤销安全边界区分了“单分量超过 255 字节、物理上无法存入对象后端的纯 ASCII 键”与“当前无法读取的已存在记录”。loadIAMConfigBytesWithMetadata 仅将前者按 errConfigNotFound(记录不存在)处理;完整路径超限(PATH_MAX)与真实存储故障继续按 Fail-Closed 向上报错,确保磁盘上已有的撤销墓碑绝不会被掩盖。对象存储后端仍不支持写入超长路径分量的个人策略映射或父身份版本记录。

当权威撤销版本已成功落盘、但后续附属清理步骤失败时,Admin 删除请求可能返回 HTTP 500;这并不代表旧身份仍可使用。此时应在暂停 IAM 写入与同名重建的前提下重试删除,并在各站点验证旧凭证已被拒绝。当持久化父身份版本不可读或签发记录无法提交时,STS 会通过内部错误路径(含 STSInternalError)Fail-Closed 拒绝签发,绝不通过猜测缺失边界来放行新令牌。

可通过每个进程需认证的 /minio/metrics/v3/cluster/iam 端点采集以下指标:

指标名称 含义说明
minio_cluster_iam_revocation_records 当前进程内存索引中保留的删除墓碑记录与父撤销边界总数
minio_cluster_iam_revocation_heal_failures 进程启动以来失败的自愈收敛轮数
minio_cluster_iam_revocation_heal_duration_millis 最近一轮自愈收敛耗时(毫秒)
minio_cluster_iam_revocation_heal_last_success_timestamp_seconds 最近一轮自愈成功的 Unix 时间戳(秒)

上述索引指标按单进程统计,对共享同一后端的多个节点直接求和会重复计数。自愈指标仅在启用了跨站点复制且当前节点持有 Leader 租约时推进;仅共享后端而未启用站点复制的部署不运行跨站点自愈循环,后三项指标为零属于正常现象。

为什么更简单的方案不成立

在多站点与离线重连场景下,以下看似更简单的方案均存在安全漏洞:

  1. 直接物理删除配置记录:一旦物理删除,重新连入的离线节点或延迟复制消息将缺少判断新旧顺序的墓碑证据,导致已删除身份被旧快照重新写回。
  2. 仅比较子凭证自身的更新时间(UpdatedAt):父用户被删除并重建后,攻击者或延迟重放若对旧子凭证执行一次元数据编辑,其更新时间就会晚于父用户删除时间并借此恢复访问;而签发时写入加密令牌的 siloParentRevocation 签名父边界不会随编辑而改变。
  3. 用接收节点的本地时间覆盖源事件时间戳:会破坏跨站点因果与源端顺序,甚至将一次幂等网络重试变成一次具有更新时间戳的新写入。本实现严格保留源时间戳,仅对有意的本地写入推进单调版本号。
  4. 为永久身份墓碑设置固定 TTL 或仅导入导出活记录:离线站点或冷备份的断连时长无法预设上限,提前淘汰永久身份墓碑或仅恢复活记录导出会直接抹去阻止旧身份重放的历史边界。

具体实现契约参见 cmd/iam-revision.go 与 cmd/metrics-v3-cluster-iam.go。