Skip to content

feat(deploy): support an IAM permissions boundary for project roles - #2305

Open
DongZhaoXiong wants to merge 1 commit into
aws:mainfrom
DongZhaoXiong:feat/iam-permissions-boundary
Open

DongZhaoXiong wants to merge 1 commit into
aws:mainfrom
DongZhaoXiong:feat/iam-permissions-boundary

Conversation

@DongZhaoXiong

Copy link
Copy Markdown

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:

AGENTCORE_PERMISSIONS_BOUNDARY           # env, for CI
agentcore.json → iam.permissionsBoundary # committed, reviewed with the code
agentcore config permissionsBoundary     # ~/.agentcore/config.json, per machine

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
config command 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 the
same enforcing account and want the constraint reviewed in git.

No interactive 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, 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:permissionsBoundary context entry, layered onto the app's own
context store via CdkAppMultiContext(projectDir, context). aws-cdk-lib turns that into a
stack-wide aspect over AWS::IAM::Role / AWS::IAM::User, so it reaches the roles created inside
the @aws/agentcore-cdk L3 constructs without those constructs exposing a prop, and with no
change to the vended CDK project — existing projects benefit on CLI upgrade, and src/assets/ is
untouched.

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) before CreateRole.

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 stack
with an empty cause. Recording all 88 ioHost messages of a failing deploy shows the reason
appears only on CDK_TOOLKIT_I5502 progress 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 is
unrecognizable. Details worth knowing:

  • The capture is scoped per deploy() call, so a denial from an earlier deploy on the same
    wrapper cannot be pinned on a later unrelated failure.
  • Callers that supply no ioHost keep the toolkit's own default, unwrapped.
  • Detection is deliberately narrow — iam:CreateRole denied by a boundary — because that is the
    case 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:DescribeStackEvents appears only in the CFN execution role policy, not the
developer 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:

CDK deploy failed: ❌  AgentCore-repro-default failed: NoStack: CloudFormationStack object does not hold a stack

After:

This account requires every new IAM role to carry a permissions boundary, and CloudFormation
was denied iam:CreateRole because the role it tried to create had none.

Set it for every project on this machine:
  agentcore config permissionsBoundary arn:aws:iam::111122223333:policy/AgentCoreExecutionRoleBoundary

Or commit it with the project, in agentcore/agentcore.json:
  "iam": { "permissionsBoundary": "arn:aws:iam::111122223333:policy/AgentCoreExecutionRoleBoundary" }

Then re-run `agentcore deploy`.

See docs/PERMISSIONS.md ("Hardening with permission boundaries") for details.

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

  • iam is a new optional field, so schemas/agentcore.schema.v1.json stays v1-compatible.
  • The published @aws/agentcore-cdk project schema is not .strict(), so the new field is ignored
    by the vended app and projects pinned to older construct versions still synth. Verified against
    0.1.0-alpha.50.
  • With no boundary configured, fromCdkApp keeps its default context store and the synthesized
    template is unchanged.

Docs and telemetry

docs/PERMISSIONS.md described this configuration as blocked on construct changes; that note is
replaced with the actual setup step, plus a troubleshooting entry for the error above.
docs/configuration.md gains an IAM Settings section and states the intent of the iam block —
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-boundary deploy flag, per-target boundaries in aws-targets.json, and an unset
operation 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 if
iam.permissionsBoundary should also appear on the docs site.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Other (please describe):

Testing

How have you tested the change?

  • I ran npm run test:unit and npm run test:integ
  • I ran npm run typecheck
  • I ran npm run lint
  • If I modified src/assets/, I ran npm run test:update-snapshots and committed the updated snapshots

src/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, blank
    handling, 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 rewriting
    including the real NoStack + captured-progress-message shape plus the negative cases (other
    actions, 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 test CreateRole input. Tests that
resolve a boundary stub the environment and ~/.agentcore/config.json, so they do not depend on
the developer's machine having (or not having) a boundary configured.

Verified end-to-end against a real boundary-enforcing account:

  • Deploy with no boundary configured produces the message above.
  • With the boundary set, deploy succeeds and aws iam get-role confirms PermissionsBoundaryArn
    on the runtime execution role.
  • Precedence verified at synth level for all four source combinations; the no-boundary case
    produces no PermissionsBoundary on any role.

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the
terms of your choice.

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
@DongZhaoXiong
DongZhaoXiong requested a review from a team September 15, 2026 01:29
@github-actions github-actions Bot added the size/xl PR size: XL label Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agentcore deploy fails with "NoStack" when the account requires a permissions boundary

1 participant