aws-samples / aws-samples/sample-autonomous-cloud-coding-agents
fix(bootstrap): least-privilege policies deny all nested-stack resources — default-on Agent Registry cannot deploy without AdministratorAccess
- Dominant language
- TypeScript
- Stars
- 143
- Forks
- 46
- Avg merge
- 3d 9h
- Merged PRs (30d)
- 20
Description
**Component:** cdk (bootstrap policies)
## Describe the bug
The least-privilege bootstrap policies scope every resource ARN to the **parent stack's** name prefix (`backgroundagent-dev-*`). Resources created inside **nested** stacks do not carry that prefix in their generated physical names, so the CFN execution role is denied on all of them.
The Agent Registry is **default-on** (`enableAgentRegistry !== false`, `cdk/src/stacks/agent.ts:115-128`) and contributes two nested stacks — `AgentRegistryStack` and `RegistryApi`. So on a clean account with default context, `mise //cdk:deploy` **cannot succeed** with the repo's own least-privilege bootstrap.
This is not one missing action; it is a whole class of denials.
## Expected behavior
A default-context deploy (Agent Registry enabled) succeeds against the least-privilege bootstrap, with **no** `AdministratorAccess` attached to `cdk-hnb659fds-cfn-exec-role-*`.
## Current behavior
Two distinct denials, hit in sequence:
**1. `iam:PassRole` on the exec role itself.** Creating any nested stack makes CloudFormation pass the execution role to itself. No statement permits this:
```
User: arn:aws:sts:::assumed-role/cdk-hnb659fds-cfn-exec-role--us-east-1/AWSCloudFormation
is not authorized to perform: iam:PassRole on resource:
arn:aws:iam:::role/cdk-hnb659fds-cfn-exec-role--us-east-1
because no identity-based policy allows the iam:PassRole action
```
The existing `IAMPassRole` statement scopes to `arn:aws:iam::*:role/backgroundagent-dev-*` and conditions on `iam:PassedToService`, which does not cover this case.
**2. Resource-name mismatch inside nested stacks.** After granting the above, the next failure:
```
User: ...AWSCloudFormation is not authorized to perform: states:CreateStateMachine on resource:
arn:aws:states:us-east-1::stateMachine:AgentRegistryProviderwaiterstatemachineE27177B2-arZJxYWqV4wX
because no identity-based policy allows the states:CreateStateMachine action
```
The action **is** granted; the `StepFunctions` statement scopes it to `arn:aws:states:*:*:stateMachine:backgroundagent-dev-*`. The waiter state machine is named `AgentRegistryProviderwaiterstatemachine…` — no parent-stack prefix — so the ARN never matches.
The same prefix assumption appears across the generated policies (SNS, CloudFront, Lambda, IAM roles/policies …), so fixing only `states:*` would just surface the next resource type. Patching one action at a time is not viable — each cycle is an ~8-minute deploy.
## Reproduction steps
1. Fresh account; bootstrap with the least-privilege template (see #864 for the size workaround needed to do this at all).
2. Do **not** attach `AdministratorAccess` to the cfn-exec-role.
3. `export MISE_EXPERIMENTAL=1 && mise run //cdk:deploy -- --require-approval never` with default context.
4. Deploy reaches changeset creation, then fails on `iam:PassRole`. Grant that, redeploy, and it fails on `states:CreateStateMachine` inside the nested stack.
## Possible solution
1. **Allow the exec role to pass itself**, scoped to exactly that role ARN, for nested-stack creation.
2. **Stop assuming the parent-stack prefix.** Nested-stack physical names are not prefixed. Either:
- widen the affected `Resource` patterns to also admit nested-stack-generated names, or
- drive scoping off a stable tag/condition (e.g. `aws:ResourceTag/aws:cloudformation:stack-id`) rather than a name prefix, or
- give nested-stack constructs explicit physical names carrying the prefix, so the existing scoping holds.
The tag-condition route is the most robust — it stays genuinely least-privilege without depending on CDK's name generation, which is not a stable contract.
3. **Add coverage.** A synth/policy test asserting every resource ARN in the synthesized template (including nested templates) is authorized by the generated policies would catch this class before deploy. Note `cdk/test/bootstrap/synth-coverage.test.ts` already exists and did not catch it.
**Workaround used to get unblocked:** attached `AdministratorAccess` to the cfn-exec-role plus an inline `iam:PassRole` policy. That defeats the purpose of the custom bootstrap and should not be the documented path.
## Environment
- Node: v22.23.2 (mise) · mise: 2026.7.0 macos-arm64 · Region: us-east-1
- Bootstrap: `BootstrapVariant = "ABCA: Least-Privilege Bootstrap"`, version 32, `ComputeTypes=agentcore`
- Commit: `6e865f2d`
- Related: #120 (least-privilege bootstrap policies as code — this is a gap in that design), #350 (prior stale-policy AccessDenied), #864 (blocks reaching this step at all)
Contributor guide
Research direction
Start with cdk/src/stacks/agent.ts:115-128 and cdk/test/bootstrap/synth-coverage.test.ts, then reproduce the default-context deploy using the least-privilege bootstrap and no AdministratorAccess. Trace the generated policies and nested templates to cover the exec role PassRole case and nested-stack resource names. Done means the default Agent Registry deployment succeeds and the cfn-exec role has no AdministratorAccess, with synth coverage preventing regressions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- cloud, devops, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100