SN-2026-025:IAM 集群导入重启用已禁用凭据及 Console 编辑固化继承策略
发布状态: 已随 SILO Console 2.7.1 发布(1be8a74bf、a1a84cab4、1fdb279fd、3825ebc44),Server 主干(1d714834c、34ba7cc1f)已修复,待服务端正式发布
安全编号: SN-2026-025(安全公告台账,#339,#345)
严重程度: 高危(High)
影响范围: SILO Server(RELEASE.2026-11-09T00-00-00Z 及更早版本)与自 2022-06-23(580d9db85)以来的上游原版 MinIO;SILO Console(独立版与内嵌版 v2.7.0 及更早版本)与自 2024-02-29(0afea6399)以来的上游原版 MinIO Console
客户端与控制台配套: 需协同升级配套的 Server 与 Console 2.7.1(mcli 20261112 d7262c2a 同步对齐了 accesskey edit 传入显式空策略文档重置继承的能力)
继 SN-2026-021 排查服务账号策略更新链路后,我们进一步对 IAM 备份恢复(ExportIAM / ImportIAM)、Web 控制台(Console)编辑表单与带过期时间的服务账号开展了端到端联动审查,确认了另外两处继承自上游的凭据状态与权限边界缺陷:
- 集群 IAM 导入(
mcli admin cluster iam import)静默重新启用已禁用凭据与用户组:importIAM(cmd/admin-handlers-users.go)在重建服务账号时漏传了导出的Status字段,在导入groups.json时忽略了禁用组状态,导致灾备恢复或集群迁移后所有曾被禁用隔离的服务账号密钥与禁用组全部恢复为启用("on"/"enabled")状态。 - Console 编辑继承型服务账号时静态固化父策略,且父策略超 4 KiB 时无法在界面禁用密钥: 服务端
InfoServiceAccount在返回ImpliedPolicy: true的同时会附带父用户的合并策略文本供展示;上游 Console 的EditServiceAccount弹窗将该父策略文本载入编辑器并在保存任意属性修改时作为newPolicy回传,静默将“动态继承父策略”的密钥固化为一份静态内联策略拷贝,且在父策略超过 4 KiB 时直接阻塞禁用操作并吞掉弹窗报错。
摘要
- 问题本质:
ImportIAM丢失服务账号与用户组的禁用状态(#345):importIAM构造newServiceAccountOpts时未填入status: svcAcctReq.Status(导致NewServiceAccount一律默认设为AccountOn = "on");导入组时从未检查grpInfo.Status == statusDisabled并调用SetGroupStatus(ctx, grp, false);且在AddUsersToGroup失败时把外层为nil的err(而非真实错误gerr)记入failed.Groups。- Console
EditServiceAccount混淆只读继承展示与内联限制策略(#339): 在 Console 中对继承型(ImpliedPolicy: true)访问密钥执行改名、改描述、改过期时间或禁用/启用切换时,前端均会将父用户当前策略作为newPolicy提交,使其变为embedded-policy;当父用户合并策略压缩后超过maxSVCSessionPolicySize(4096 字节)时,管理员在 Console 中甚至无法禁用该密钥(服务端返回400,而弹窗遮罩吞掉了错误提示)。 - 带过期时间的无
"Version"内联策略在运行时鉴权恒被拒:NewServiceAccount允许创建省略"Version"字段的内联策略,永不过期密钥(走isAllowedLocally)可正常工作,但设置了Expiration的服务账号(cred.IsTemp() == true,走isAllowedBySessionPolicy)因强制要求subPolicy.Version != ""而在每次请求时返回403。
- 上游溯源:
ImportIAM丢失禁用状态与组错误记录为空:源自上游 MinIO2022-06-23提交580d9db85(minio/minio#15014)。- 带过期时间服务账号
"Version"校验不一致:源自上游 MinIO2021-07-15提交eae9c2f65(minio/minio#12716)与2023-02-27提交4d708cebe(minio/minio#16430)的叠加。 - Console 写回继承策略:源自上游 MinIO Console
2024-02-29提交0afea6399(#3221)。
- 修复方式: Server 提交
1d714834c在importIAM中透传服务账号与用户组状态、记录真实组导入错误,并在写入凭据前将省略"Version"的非空内联策略规范化补齐默认版本"2012-10-17";Console 提交1be8a74bf、a1a84cab4与1fdb279fd分离跟踪继承态与内联限制态,并在弹窗内部显式展示保存错误。
一、上游溯源与根因分析
ImportIAM 重新激活已禁用服务账号与用户组
ExportIAM(GET /minio/admin/v3/export-iam)会将每个服务账号(含 "accountStatus": "off")写入 svcaccts.json,将每个用户组(含 "status": "disabled")写入 groups.json。当上游在 580d9db85(2022-06-23)中实现 importIAM 时,仅在导入普通用户(users.json)时处理了禁用状态,却遗漏了服务账号和用户组:
- 服务账号被无条件启用:
importIAM虽然从 JSON 解出了svcAcctReq.Status,但在构造newServiceAccountOpts时未赋值status:字段。NewServiceAccount(cmd/iam.go)遇到空opts.status一律回退为auth.AccountOn("on")。管理员此前为应急止损或离职审计而禁用(svcacct disable)的所有历史 Access Key,一旦经历一次mcli admin cluster iam import就会全部静默复活。 - 用户组被无条件启用且报错丢失:
AddUsersToGroup创建的新组默认处于"enabled"状态,由于importIAM从未在grpInfo.Status == statusDisabled时调用SetGroupStatus(ctx, grp, false),所有已禁用的权限组在导入后立即重新向组内成员赋予权限。同时,当AddUsersToGroup返回错误gerr时,代码误写了Error: err(此时外层json.Unmarshal的err恒为nil),导致返回给客户端的失败清单中错误原因为空。
Console 编辑继承型密钥固化父策略并阻塞超长父策略禁用
在服务端,InfoServiceAccount(cmd/admin-handlers-users.go)在服务账号处于继承模式时返回 ImpliedPolicy: true,同时把父用户(及所属组)当前的合并策略序列化到 Policy 字段供界面预览。
上游 Console(0afea6399,2024-02-29)在 EditServiceAccount.tsx 中虽然根据 impliedPolicy 将“Restrict beyond user policy(在用户策略之外进一步限制)”开关默认置为关闭,却仍把 account.policy 填入了底层 policy 状态变量,并在点击保存时只要 policy !== "" 就原样随 PUT 请求发出:
- 继承态被静默改写为内联快照: 管理员仅在 Console 中修改密钥名称、描述、有效期或切换启用/禁用开关,就会把父用户那一刻的策略永久写入 JWT 的
sessionPolicy中,使密钥从inherited-policy变为embedded-policy,后续给父用户新增的授权均不再对该密钥生效。 - 父策略超过 4 KiB 时无法在 Console 中禁用密钥: 内联会话策略受限于 JWT 体积,上限为压缩后 4096 字节(
maxSVCSessionPolicySize),而父用户及多组叠加策略并无 4 KiB 限制。一旦父用户合并策略超过 4 KiB,管理员在 Console 中尝试禁用某个继承型密钥时,前端附带的超长父策略会被服务端以400 session policy size exceeds 4KiB limit拒绝,且错误提示渲染在模态框遮罩后方不可见,导致密钥维持启用而界面无反应。
二、影响范围
| 操作路径 / 组件 | 安全与运维影响 |
|---|---|
mcli admin cluster iam import |
所有已禁用服务账号(accountStatus: "off")与已禁用组(status: "disabled")在导入后被静默重新启用。 |
Console 编辑继承型密钥(父策略 <= 4 KiB) |
任意改名、改描述、改过期时间或状态切换均会将继承型密钥固化为静态内联策略,导致后续权限演进偏离预期。 |
Console 编辑继承型密钥(父策略 > 4 KiB) |
管理员无法通过 Console 禁用或修改该继承型密钥(触发 400 超长报错且弹窗内无报错提示)。 |
带过期时间且省略 "Version" 的内联策略 |
创建成功(200),但在设置了 Expiration 的服务账号上每次请求均因 subPolicy.Version == "" 被拒绝(403)。 |
三、修复方案:设计思考与充分必要性
Server 侧修复(1d714834c)
importIAM完整还原禁用状态与真实错误(cmd/admin-handlers-users.go):- 在
newServiceAccountOpts中传入status: svcAcctReq.Status(仅当旧格式归档未包含状态字段时默认"on"),在.minio.sys/iam/与etcd后端一致保留"off"禁用态; - 在
AddUsersToGroup成功后检查strings.EqualFold(grpInfo.Status, statusDisabled)并调用SetGroupStatus(ctx, grp, false),同时将组创建或状态设置阶段的真实错误记入failed.Groups。
- 在
- 凭据写入边界统一补齐策略默认
"Version"(cmd/iam.go中的normalizeServiceAccountPolicy):- 对包含语句但
Version == ""的非空内联策略克隆副本并补齐policy.DefaultVersion("2012-10-17"),消除永不过期密钥与带过期时间密钥之间的鉴权分歧;同时严格保持显式空策略文档({}或{"Statement":[]},Version == ""且len(Statements) == 0)不变,继续作为SN-2026-021定义的“清除内联策略、恢复继承”信号。
- 对包含语句但
Console 侧修复(1be8a74bf、a1a84cab4、1fdb279fd)
- 分离跟踪
impliedPolicy与内联策略文本(EditServiceAccount.tsx、api/users.go):- 当继承型密钥的 Restrict beyond user policy 开关保持关闭时,保存请求省略
policy字段,无论父策略是否超过 4 KiB 均可正常改名、设置过期时间或禁用密钥,且严格保持inherited-policy; - 当管理员在已有限制策略的密钥上主动关闭 Restrict beyond user policy 开关(或填入
{}/{"Statement":[]})时,Console 显式发送{}将其恢复为继承父策略; - 当开启限制开关但策略文本框为空时,前端直接给出校验提示并阻止提交;后端返回的任何错误均通过
ModalErrorAlert直接显示在弹窗内部。
- 当继承型密钥的 Restrict beyond user policy 开关保持关闭时,保存请求省略
为什么该设计既必要又充分
- 必要性: 仅靠服务端
SN-2026-021无法阻止旧版 Console 把合法的父策略 JSON 当作newPolicy发往服务端(对服务端接口而言,客户端显式发来的非空策略 JSON 与管理员故意配置的内联策略在语法上完全一致),也无法修复importIAM丢弃Status字段的问题。 - 充分性: 结合
SN-2026-021与mclid7262c2a,三态策略契约(缺省/null保留、显式空文档重置继承、非空文档补齐Version嵌入)与启用/禁用状态在 Server Admin API、站点复制、IAM 导入导出、Console 及mcli全栈保持完全一致(见cmd/iam-import-state_test.go与cmd/iam-svcacct-policy-edit_test.go)。
四、利弊权衡、潜在代价与边界
| 权衡维度 | 工程取舍与升级边界 |
|---|---|
| Server 与 Console 需配套升级 | 若仅升级独立版 Console 而不升级 SILO Server(早于 SN-2026-021),省略 newPolicy 的编辑仍会触发旧服务端清空策略;反之若仅升级 Server 而不更新独立/内嵌 Console,旧版 Console 仍会在编辑继承型密钥时写回父策略。请配套升级两个组件。 |
历史 iam import 已复活凭据需人工复核 |
升级代码可保证今后的 iam import 严格保留禁用状态,但无法自动区分存量集群中哪些账号是正常启用、哪些是被历史导入操作意外重新启用的。 |
升级与排查建议
- 在
20261109之后的下一版 Server 正式发布时协同升级 SILO Server、SILO Console 2.7.1 与 mcli 20261112(Server 主干:1d714834c、34ba7cc1f;已发布独立 Consolev2.7.1:3825ebc44;已发布mcli 20261112:a37a9f05)。 - 核查曾执行过
mcli admin cluster iam import的集群: 若集群此前曾从包含禁用服务账号或禁用组的备份包执行过iam import,请对照当时备份包内的svcaccts.json与groups.json检查并重新禁用不应启用的条目: - 核查曾在 Console 编辑过的继承型密钥: 若某些原本应动态跟随父用户权限的服务账号曾通过旧版 Console 编辑过属性并意外变为
Policy: embedded,可在升级后于新版 Console 中关闭 Restrict beyond user policy(或使用mcli admin user svcacct edit --policy传入{})恢复继承模式。
更多信息请参阅安全公告台账、SN-2026-021 与 Console 兼容性说明。