Skip to content

sec(auth): execute:ri-exchange missing from adminCarvedOuts; admin:* bypasses every exchange guardrail #1644

Description

@cristim

Summary

execute:ri-exchange is missing from adminCarvedOuts (internal/auth/types.go), which lists only {execute, purchases}, {approve_any, purchases} and {retry_any, purchases}. Because of that omission, permissionsAllow (internal/auth/service_group.go) returns true unconditionally for an admin:* holder on execute:ri-exchange, bypassing the account, provider, service and region dimensions and the spend cap.

Why this matters now

This is pre-existing, but it has just become load-bearing. The Azure RI exchange execute work (issue #596 / PR #1515) extends the verb's blast radius from AWS-only to Azure, on an operation the code itself repeatedly describes as financially irreversible. Every guardrail added on that path - including the source-region fix in PR #1515 - is skipped for an admin-wildcard holder.

The separation-of-duties rationale that put execute:purchases in the carve-out list (#923) applies at least as strongly here: an RI exchange consumes existing commitments and buys replacements, and cannot be undone.

Fix direction

Add {execute, ri-exchange} to adminCarvedOuts so an admin must hold an explicit, constrained execute:ri-exchange grant like everyone else. Check for callers/tests that rely on the wildcard shortcut before landing, and mirror whatever migration/communication #923 used for execute:purchases.

Found during adversarial review of PR #1515. Out of scope for that PR.

Activity

  1. cristim commented on Aug 8, 2026

    @cristim
    MemberAuthor

    Note from the #1550 work (PR #1737, now open): this is a one-line change once that PR lands, and it will then be covered end to end automatically.

    PR #1737 adds a grant ceiling to the group-permission write paths. Two things there make this issue smaller than it currently looks:

    1. grantCeilingAllows (internal/auth/group_ceiling.go) re-checks adminCarvedOuts inside its admin:* branch, deliberately mirroring permissionsAllow branch for branch. So adding {ActionExecute, ResourceRIExchange} to adminCarvedOuts is picked up by both the enforcement path and the new grant ceiling from the same edit; there is no second place to remember.

    2. checkGrantCeiling refuses to grant any pair in adminCarvedOuts at all, so the moment execute:ri-exchange joins that map, an admin also stops being able to write it onto a group to route around the carve-out. That closes the permission-administration bypass for this verb at the same time, without a separate change.

    Worth confirming before making the edit, since it is the substantive question this issue turns on and #1737 does not answer it: ResourceRIExchange's doc comment in internal/auth/types.go says RI exchanges are financially irreversible because the AWS API has no rollback once submitted, which is the same argument #923 used for the three purchase verbs. If that reasoning holds, the omission looks like an oversight rather than a decision. But adding it to adminCarvedOuts means an admin holding only admin:* can no longer execute an RI exchange, and every current admin would need explicit membership in a group granting the verb — the same operational step #923 imposed for purchases. That is a policy call, not a mechanical one.

    One consequence to weigh: execute:ri-exchange is constraint-gated via requirePermissionConstraints, unlike the purchase verbs' usual path. PR #1737's ceiling handles constraint containment (a holder capped at MaxPurchaseAmount: 100 cannot hand out an uncapped grant), so that dimension is already covered whichever way this goes.

    Adjacent: #1550, #1629, #1738.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions