feat(deploy): support an IAM permissions boundary for project roles - #2305
Open
DongZhaoXiong wants to merge 1 commit into
Open
DongZhaoXiong wants to merge 1 commit into
DongZhaoXiong wants to merge 1 commit into
Conversation
In an account whose CDK CloudFormation execution role carries an organization boundary that denies iam:CreateRole unless the new role carries a boundary itself, `agentcore deploy` could not succeed, and the failure gave the user nothing to act on. Accept a boundary from three sources, most specific first: AGENTCORE_PERMISSIONS_BOUNDARY, the project's `iam.permissionsBoundary` in agentcore.json, and `permissionsBoundary` in ~/.agentcore/config.json. The value is a policy name or a policy ARN; a bare name is resolved against each target's own partition and account, so one value works across accounts and partitions. A blank value reads as unset everywhere, which is how the machine default gets cleared — `agentcore config` can only write keys, never remove them. A boundary is a property of the account you deploy into, not of the project, so the machine config is usually the right home: set once, applies to every project, not committed, and it cannot break a teammate whose account has no such policy. It sits alongside the existing corporate-environment settings there (uvIndex, disableDependencyManagement). The project field is for teams that all deploy into the same enforcing account and want the constraint reviewed in git. No prompt was added: at `agentcore create` the user does not yet know which account they will deploy into, and the boundary is chosen by whoever set that account up. The boundary is applied as the `@aws-cdk/core:permissionsBoundary` context entry, layered onto the app's own context store. aws-cdk-lib turns that into a stack-wide aspect over AWS::IAM::Role / AWS::IAM::User, which reaches the roles created inside the @aws/agentcore-cdk L3 constructs without those constructs exposing a prop, and without changing the vended CDK project. Resolution lives in CdkToolkitWrapper.initialize() so synth, deploy, diff and destroy agree; a boundary applied on deploy but not on diff would read as permanent drift. The A/B test execution role is created through the IAM API, which cannot resolve a bare policy name, so that path expands the boundary to a full ARN first. Recognizing the failure needs the toolkit's message stream, not the thrown error. CloudFormation rolls the stack back and toolkit-lib then throws `NoStack: CloudFormationStack object does not hold a stack` with an empty cause; the IAM denial only ever appears on a CDK_TOOLKIT_I5502 progress message. So the ioHost is wrapped with a transparent pass-through that remembers the first boundary denial, and deploy() falls back to it, scoped per call so an earlier denial cannot be pinned on a later unrelated failure. Reading CloudFormation stack events instead was rejected: cloudformation:DescribeStackEvents is only in the CFN execution role policy, not the developer policy, so it would force admins to widen user permissions just to get an error message. Detection is narrow on purpose — iam:CreateRole denied by a boundary — since that is the case with this specific remedy. docs/PERMISSIONS.md described this configuration as blocked on construct changes; that note is replaced with the actual setup step plus a troubleshooting entry. docs/configuration.md gains an IAM Settings section and states the intent of the `iam` block as constraints imposed from outside the project, to keep it from drifting into a role-authoring surface. Deploy telemetry records whether a boundary was applied; the value itself is customer-identifying and is not emitted. Closes aws#2292
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Lets a permissions boundary be declared for every IAM role the project creates, and turns the
resulting deploy failure into something actionable when it is missing.
Three sources, most specific first:
The value is a policy name or a policy ARN. A bare name is resolved against each deployment
target's own partition and account, so one value works across accounts and partitions. A blank
value reads as unset at every source, which is how the machine default gets cleared — the
configcommand can only write keys, never remove them.Why three places, and no prompt
A boundary is a property of the account you deploy into, not of the project. Committing it breaks
a teammate whose account has no such policy, and it has to be repeated in every project. So the
machine config is usually the right home — set once, applies everywhere, not committed. It sits
alongside the existing corporate-environment settings there (
uvIndex,disableDependencyManagement). The project-level field exists for teams that all deploy into thesame enforcing account and want the constraint reviewed in git.
No interactive prompt was added. At
agentcore createthe user does not yet know which accountthey will deploy into, and the boundary is chosen by whoever set that account up, so there is no
answer for them to give. Like CDK bootstrap, this is an account-level prerequisite best surfaced
when it actually blocks a deploy.
How the boundary reaches the roles
Passed as the
@aws-cdk/core:permissionsBoundarycontext entry, layered onto the app's owncontext store via
CdkAppMultiContext(projectDir, context).aws-cdk-libturns that into astack-wide aspect over
AWS::IAM::Role/AWS::IAM::User, so it reaches the roles created insidethe
@aws/agentcore-cdkL3 constructs without those constructs exposing a prop, and with nochange to the vended CDK project — existing projects benefit on CLI upgrade, and
src/assets/isuntouched.
Resolution lives in
CdkToolkitWrapper.initialize()rather than at each call site so synth,deploy, diff and destroy all agree. A boundary applied on deploy but not on diff would read as
permanent drift, and a path that silently skipped it would produce roles the account's boundary is
meant to cap.
The A/B test execution role is created through the IAM API, which cannot resolve a bare policy
name the way CloudFormation can, so that path expands the boundary to a full ARN via
arnPrefix(region)beforeCreateRole.How the failure is recognized — the part worth reviewing closely
The denial never reaches the thrown error. When the role create is denied, CloudFormation rolls
the stack back and toolkit-lib throws
NoStack: CloudFormationStack object does not hold a stackwith an empty
cause. Recording all 88 ioHost messages of a failing deploy shows the reasonappears only on
CDK_TOOLKIT_I5502progress messages.So the toolkit's ioHost is wrapped with a transparent pass-through that remembers the first
boundary denial it sees, and
deploy()falls back to that when the thrown error isunrecognizable. Details worth knowing:
deploy()call, so a denial from an earlier deploy on the samewrapper cannot be pinned on a later unrelated failure.
iam:CreateRoledenied by a boundary — because that is thecase with this specific remedy. A boundary denying any other action passes through untouched.
Reading CloudFormation stack events instead was the obvious alternative and was rejected:
cloudformation:DescribeStackEventsappears only in the CFN execution role policy, not thedeveloper policy, so it would force every admin to widen user permissions just to get an error
message. The TUI already scrapes these same progress messages for resource status.
Before / after
Same CLI version, same account, same project.
Before:
After:
The ARN is read out of the denial, so the command is copy-pasteable. When a boundary was applied
but rejected as the wrong one, the message lays out Applied/Required instead of the setup
instructions.
Compatibility
iamis a new optional field, soschemas/agentcore.schema.v1.jsonstays v1-compatible.@aws/agentcore-cdkproject schema is not.strict(), so the new field is ignoredby the vended app and projects pinned to older construct versions still synth. Verified against
0.1.0-alpha.50.fromCdkAppkeeps its default context store and the synthesizedtemplate is unchanged.
Docs and telemetry
docs/PERMISSIONS.mddescribed this configuration as blocked on construct changes; that note isreplaced with the actual setup step, plus a troubleshooting entry for the error above.
docs/configuration.mdgains an IAM Settings section and states the intent of theiamblock —constraints imposed on the project from outside, not a place to author roles — to keep it from
drifting into a general IAM configuration surface.
Deploy telemetry records whether a boundary was applied. The value itself is customer-identifying
and is not emitted.
Not in scope
A
--permissions-boundarydeploy flag, per-target boundaries inaws-targets.json, and an unsetoperation for
agentcore config(which affects every key, not just this one). All noted in #2292.Related Issue
Closes #2292
Documentation PR
None. The docs that describe this behaviour live in this repo (
docs/PERMISSIONS.md,docs/configuration.md) and are updated here. Happy to open an agent-docs PR ifiam.permissionsBoundaryshould also appear on the docs site.Type of Change
Testing
How have you tested the change?
npm run test:unitandnpm run test:integnpm run typechecknpm run lintsrc/assets/, I rannpm run test:update-snapshotsand committed the updated snapshotssrc/assets/is deliberately untouched, so no snapshot updates were needed.New test files:
src/cli/aws/__tests__/permissions-boundary.test.ts— precedence across all four sources, blankhandling, ARN vs name detection, ARN expansion for aws / aws-cn / aws-us-gov.
src/cli/cdk/__tests__/permissions-boundary.test.ts— config reading, and error rewritingincluding the real
NoStack+ captured-progress-message shape plus the negative cases (otheractions, plain CreateRole denials, self-referential cause chains).
src/cli/cdk/toolkit-lib/__tests__/wrapper.test.ts— context store wiring, ioHost capture,per-call scoping, and pass-through transparency.
Extended: project schema validation,
computeDeployAttrs, A/B testCreateRoleinput. Tests thatresolve a boundary stub the environment and
~/.agentcore/config.json, so they do not depend onthe developer's machine having (or not having) a boundary configured.
Verified end-to-end against a real boundary-enforcing account:
aws iam get-roleconfirmsPermissionsBoundaryArnon the runtime execution role.
produces no
PermissionsBoundaryon any role.Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the
terms of your choice.