SN-2026-025: IAM Cluster Import Re-Enabling Disabled Credentials and Console Inherited-Policy Freezing
Status: Shipped in SILO Console 2.7.1 (1be8a74bf, a1a84cab4, 1fdb279fd, 3825ebc44) and fixed on Server main (1d714834c, 34ba7cc1f, pending Server publication)
Identifier: SN-2026-025 (Security Advisory Ledger, #339, #345)
Severity: High
Affected scope: SILO Server (RELEASE.2026-11-09T00-00-00Z and earlier) and inherited upstream MinIO since 2022-06-23 (580d9db85); SILO Console (v2.7.0 and earlier, standalone and embedded) and inherited upstream MinIO Console since 2024-02-29 (0afea6399)
Client & UI notes: Pair the repaired Server and Console 2.7.1 together (mcli 20261112 d7262c2a also aligns explicit empty-policy resets on accesskey edit)
Following the audit of service-account policy updates in SN-2026-021, we reviewed the full lifecycle of service accounts and IAM groups across backup/restore (ExportIAM / ImportIAM), the Web Console, and expiring credentials. That review uncovered two inherited privilege-state defects:
- Cluster IAM import (
mcli admin cluster iam import) silently reactivated disabled credentials and groups:importIAM(cmd/admin-handlers-users.go) omitted the exportedStatusfield when recreating service accounts and ignored disabled group status when importinggroups.json, turning every disabled/revoked service account and disabled group back"on"during disaster recovery or cluster migration. - Console metadata edits froze inherited parent policies and blocked disabling keys under large parent policies: Because
InfoServiceAccountreturns the parent user’s effective policy text alongsideImpliedPolicy: true, Console’sEditServiceAccountmodal populated its inline policy editor with the parent’s policy and submitted it back on any metadata save—converting aninherited-policykey into a staticembedded-policycopy, and failing outright (400 errSessionPolicyTooLarge, with the error hidden behind the modal) when an operator tried to disable an inherited service account whose parent policy exceeded 4 KiB.
Summary
- What went wrong:
ImportIAMdroppedStatuson service accounts and groups (#345): Incmd/admin-handlers-users.go,importIAMbuiltnewServiceAccountOptswithoutstatus: svcAcctReq.Status(causingNewServiceAccountto default every imported key toAccountOn = "on"), never calledSetGroupStatus(ctx, grp, false)whengrpInfo.Status == statusDisabled, and recordedError: err(nil) instead ofgerrinfailed.GroupswhenAddUsersToGroupfailed.- Console
EditServiceAccountconflatedImpliedPolicydisplay text with an inline restriction (#339): Whenever an operator renamed, described, set expiry on, or toggled status for aninherited-policyaccess key in Console, the UI sent the parent user’s combined policy JSON asnewPolicy, permanently embedding a point-in-time copy of the parent’s policy. When the parent’s combined policy exceededmaxSVCSessionPolicySize(4096 bytes after whitespace compaction), operators could not even disable or edit the inherited key in Console—and the modal swallowed the API error notification. - Expiring service accounts omitting
"Version"failed authorization at runtime:NewServiceAccountaccepted inline policies without"Version", which worked for non-expiring keys (isAllowedLocally) becausePolicy.IsAllowedignoresVersion, whereasisAllowedBySessionPolicy(cmd/iam.go, used whenevercred.IsTemp()is true—including any service account with anExpirationtimestamp) requiredsubPolicy.Version != ""and denied every S3/Admin request with403.
- Upstream provenance:
ImportIAMstatus omission andnilgroup error reporting: upstream MinIO580d9db85(2022-06-23, minio/minio#15014).- Expiring service-account
"Version"mismatch: upstream MinIOeae9c2f65(2021-07-15, minio/minio#12716) combined with4d708cebe(2023-02-27, minio/minio#16430). - Console inherited-policy submission: upstream MinIO Console
0afea6399(2024-02-29,#3221).
- Fix: Server commit
1d714834cpassesstatus: svcAcctReq.StatusduringimportIAM, applies disabled group status after group creation, records the realgerron group import failure, and normalizes non-empty versionless inline policies topolicy.DefaultVersion("2012-10-17"). Console commits1be8a74bf,a1a84cab4, and1fdb279fdtrackImpliedPolicyseparately from inline restrictions and surface save errors directly inside the modal.
1. Upstream Provenance and Root Cause
ImportIAM Reactivating Disabled Service Accounts and Groups
ExportIAM (GET /minio/admin/v3/export-iam) serializes every service account (including "accountStatus": "off") into svcaccts.json and every group (including "status": "disabled") into groups.json. When upstream introduced importIAM in 580d9db85 (2022-06-23), the user import loop checked u.Status == AccountOff and persisted disabled users accurately—but both the service-account loop and the group loop dropped status:
- Service accounts:
importIAMdecodedsvcAcctReq.Status, yet omittedstatus:when constructingnewServiceAccountOpts. InsideNewServiceAccount(cmd/iam.go), an emptyopts.statusdefaults toauth.AccountOn("on"). Every service account that an administrator had disabled (mcli admin user svcacct disable)—for example, to quarantine a leaked key while preserving audit history—became active again immediately uponmcli admin cluster iam import. - Groups:
AddUsersToGroupalways creates or updates groups in"enabled"status unlessSetGroupStatus(ctx, grp, false)is called. BecauseimportIAMnever checkedgrpInfo.Status == statusDisabled, every disabled group resumed granting all of its attachedAllowpolicies to member users after an IAM import. Furthermore, whenAddUsersToGroupreturned an errorgerr, the handler appendedmadmin.IAMErrEntity{Name: grp, Error: err}—where outererrwasnilfromjson.Unmarshal—reporting a failed group import with an empty error reason.
Console EditServiceAccount Freezing Parent Policy and Blocking Disable
On the server, InfoServiceAccount (cmd/admin-handlers-users.go) sets ImpliedPolicy: true when an access key inherits its parent’s permissions, and populates Policy with the JSON rendering of the parent user’s (and groups’) combined policies for read-only display.
In upstream Console (0afea6399, 2024-02-29), EditServiceAccount.tsx initialized its Restrict beyond user policy toggle to false when impliedPolicy was true, yet pre-filled the backing policy state string with account.policy and unconditionally sent policy whenever policy !== "" on save:
- Silent conversion from inherited to frozen inline policy: Editing only an access key’s name, description, expiration, or enabled/disabled toggle in Console sent the parent’s current policy text as
newPolicy, turningsa-policy-typefrominherited-policyintoembedded-policy. Later policy grants added to the parent user or parent groups silently failed to apply to that key. - Inability to disable inherited keys under
> 4 KiBparent policies: Service-account inline session policies are capped atmaxSVCSessionPolicySize = 4096compacted bytes (because they live inside the JWTSessionToken), whereas parent user/group combined policies have no 4 KiB ceiling. Whenever a parent user had more than 4 KiB of combined policy statements, clicking Save in Console to disable an inherited service account sent the oversized parent policy asnewPolicy, failed on the server with400 session policy size exceeds 4KiB limit, and—because the modal lacked an inline error alert—left the key enabled without telling the operator why Save failed.
2. Impact Scope
| Workflow / Component | Security & Operational Impact |
|---|---|
mcli admin cluster iam import |
Silently re-enables every disabled service account (accountStatus: "off" -> "on") and every disabled IAM group (status: "disabled" -> "enabled"). |
Console EditServiceAccount (<= 4 KiB) |
Any metadata or status edit on an inherited access key converts it to a static embedded policy snapshot, causing silent permission drift when parent policies change. |
Console EditServiceAccount (> 4 KiB) |
Operators cannot disable, rename, or set expiry on an inherited access key via Console (400 error swallowed by the modal). |
Expiring service accounts without Version |
Inline policies omitting "Version" succeed on creation (200), work on non-expiring keys, and fail every request (403) whenever Expiration is set. |
3. Solution Design: Why It Is Necessary and Sufficient
Server Repairs (1d714834c)
- Exact state restoration in
importIAM(cmd/admin-handlers-users.go):- Passes
status: svcAcctReq.StatusinnewServiceAccountOpts(defaulting toAccountOnonly when an older or handcrafted JSON omitsaccountStatus), preserving"off"credentials in both.minio.sys/iam/andetcd. - Calls
globalIAMSys.SetGroupStatus(ctx, grp, false)wheneverstrings.EqualFold(grpInfo.Status, statusDisabled)afterAddUsersToGroupsucceeds, and recordsError: gerr(or the status error) infailed.Groups.
- Passes
- Version normalization at credential write boundaries (
normalizeServiceAccountPolicyincmd/iam.go):- Clones any non-empty
sessionPolicywhoseVersionis""and setsVersion = policy.DefaultVersion("2012-10-17") before storing or updating the credential, while leaving empty policy documents (Version == ""andlen(Statements) == 0) untouched as the explicit reset-to-inheritance signal established inSN-2026-021.
- Clones any non-empty
Console Repairs (1be8a74bf, a1a84cab4, 1fdb279fd)
- Separate
impliedPolicystate from inline editor text (EditServiceAccount.tsx,api/users.go):- When Restrict beyond user policy remains off on an
inherited-policykey, Console omitspolicyfrom thePUTpayload, preserving inheritance across name, description, expiry, and status edits regardless of parent policy size (even> 4 KiB). - When an operator turns Restrict beyond user policy off on an existing
embedded-policykey (or enters{}/{"Statement":[]}), Console sends{}to explicitly reset the key back toinherited-policy. - If Restrict beyond user policy is toggled on while the editor is blank, Console blocks submission with clear inline validation instead of sending an ambiguous empty string; backend errors (such as invalid JSON/ARN or
> 4 KiBinline policy) render inside the modal viaModalErrorAlert.
- When Restrict beyond user policy remains off on an
Sufficiency and Necessity
- Necessary: Fixing only
SN-2026-021on the server could not prevent Console from sending a 3 KiB parent policy string asnewPolicy(because to the server, a valid non-empty JSON policy innewPolicyis indistinguishable from an intentional inline restriction), nor could it preventimportIAMfrom droppingsvcAcctReq.StatusandgrpInfo.Status. - Sufficient: Together with
SN-2026-021andmclid7262c2a, the tri-state policy contract (omitted/null= preserve,{}/{"Statement":[]}= reset to inherited, non-empty = embed with normalizedVersion) and status fidelity hold uniformly across Server Admin APIs, site replication, IAM export/import, Console, andmcli(cmd/iam-import-state_test.go,cmd/iam-svcacct-policy-edit_test.go).
4. Trade-Offs, Caveats, and Operational Costs
| Dimension | Engineering Trade-Off & Upgrade Boundary |
|---|---|
| Coordinated Server + Console upgrade required | Upgrading standalone Console without upgrading SILO Server would cause Console’s omitted-policy save on an embedded-policy key (or {} reset) to hit pre-SN-2026-021 Server behavior; conversely, upgrading Server without updating embedded/standalone Console leaves Console v2.7.0 freezing inherited policies on edit. Deploy the matched Server and Console release together. |
Historical iam import reactivation is durable |
Like SN-2026-021, upgrading the binary prevents future reactivation during iam import, but cannot distinguish credentials that were intentionally enabled from credentials accidentally re-enabled by an earlier mcli admin cluster iam import. |
Remediation and Audit Guidance
- Upgrade SILO Server, SILO Console 2.7.1, and mcli 20261112 together when the post-
20261109Server release is published (Servermain:1d714834c,34ba7cc1f; standalone Consolev2.7.1:3825ebc44;mcli 20261112:a37a9f05). - Audit clusters populated or restored via
mcli admin cluster iam import: If you previously raniam importfrom an export bundle containing disabled service accounts or disabled groups, inspectsvcaccts.jsonandgroups.jsonin that archive and re-disable any unintended active entries: - Audit Console-edited access keys for unintended
embeddedpolicies: If an access key was meant to track its parent user’s policies dynamically (Policy: implied) but was previously edited in Console, checkmcli admin user svcacct info myminio/ <access-key>and reset it to inheritance if needed by toggling off Restrict beyond user policy in the updated Console (or passing{}tomcli admin user svcacct edit --policy).
See the Security Advisory Ledger, SN-2026-021, and Console Compatibility Notes.