Skip to content

Commit f560af1

Browse files
docs: ADR-002 least-privilege CDK bootstrap policies (#133)
* docs: ADR-002 least-privilege CDK bootstrap policies Records the design decisions from RFC #120: why policies as code, why triple-layer versioning, why two-layer preflight validation, and why cdk/src/bootstrap/ as the location. Refs #121 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(ADR-001): clarify mandatory post-merge rebase for stacked PRs Without force-push after rebase, GitHub shows already-merged commits in child PRs — reviewers cannot distinguish new work from old. Make the retarget→rebase→force-push sequence explicit and mandatory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(ADR-002): move References metadata to proper section The ADR template defines Status, Date, Supersedes, and Superseded-by as header metadata. Move the ADR-001 cross-reference from the non-standard header field to the ## References section for template conformance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(ADR-002): mark forward-looking artifacts with implementation status Address review feedback: distinguish shipped state from design intent. Add Implementation line noting progressive delivery, tag each artifact with its landing issue number, use future tense for unbuilt components. ADR-001 reference now a relative link for Starlight clickability. Note on ADR-001 amendment (§8 merge semantics): this is a clarification of existing intent (retarget/rebase protocol), not a reversal of the original decision — treated as additive refinement per README lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: bgagent <345885+scottschreckengaust@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8121ee0 commit f560af1

4 files changed

Lines changed: 158 additions & 4 deletions

File tree

docs/decisions/001-stacked-pull-requests.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,11 @@ When a lower PR changes after review feedback:
9494
The default topology is a **classic stack** — each PR targets its predecessor's branch. When an early PR merges to `main` before later PRs are reviewed:
9595

9696
1. **Retarget** all PRs that pointed at the merged branch to `main` (or to the next unmerged predecessor). Use `gh pr edit <N> --base main` or GitHub's "Retarget" button.
97-
2. **Rebase** each retargeted PR onto its new base so the diff is clean.
98-
3. **CI must pass** on each retargeted PR independently after rebase.
97+
2. **Rebase** each retargeted PR onto its new base so the diff is clean — use `git rebase --skip` for commits whose content is already in main via the merged predecessor.
98+
3. **Force-push with lease** (`--force-with-lease`) so the PR diff on GitHub shows only net-new changes, not already-merged content.
99+
4. **CI must pass** on each retargeted PR independently after rebase.
100+
101+
**This sequence is mandatory, not optional.** Until it completes, GitHub shows already-merged commits in the child PR's diff — reviewers cannot distinguish new work from old, defeating the purpose of stacking. A merge is not complete until all child PRs are rebased clean.
99102

100103
After retargeting, the remaining PRs form a shorter stack rooted on `main`. This is the expected, normal path — not an exception.
101104

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# ADR-002: Least-privilege CDK bootstrap policies as code
2+
3+
**Status:** accepted
4+
**Date:** 2026-05-19
5+
**Implementation:** Tracked in RFC #120; artifacts referenced below land progressively across the 8-PR stack and are not yet present on `main`.
6+
7+
## Context
8+
9+
CDK bootstrap creates five roles per account/region. The **CloudFormation execution role** (cdk-hnb659fds-cfn-exec-role) receives `AdministratorAccess` by default — CloudFormation assumes it to create, modify, and delete stack resources. This violates least-privilege and may conflict with organizational SCPs or compliance gates.
10+
11+
The ABCA project documented three scoped policies in `docs/design/DEPLOYMENT_ROLES.md` (PR #46), validated against a live deployment through 7 iterations and 36 CloudTrail-discovered actions. However, these policies exist only as JSON blobs in a Markdown file — unversioned, untested, and manually applied.
12+
13+
**Failure mode without automation:** When a new release adds a resource type (e.g., SQS queue), operators who pull and deploy hit a mid-rollback CloudFormation failure because their bootstrap policy predates the new permissions. The deploy fails 15 minutes in with no prior warning.
14+
15+
**Constraints:**
16+
- IAM managed policies have a 6,144-character limit — hence the three-policy split (Infrastructure, Application, Observability).
17+
- Bootstrap must exist before the CDK app can deploy — circular dependency prevents managing bootstrap from within the app stack.
18+
- The four other bootstrap roles (deploy, lookup, file-publishing, image-publishing) are already scoped by the default template and don't need modification.
19+
20+
## Decision
21+
22+
### Policies as typed TypeScript code in `cdk/src/bootstrap/` *(lands in #122)*
23+
24+
Rationale for location:
25+
- **Agent routing**`AGENTS.md` routes CDK/IAM changes to `cdk/`. An agent modifying a construct that adds a DynamoDB table naturally looks here for the policy it must update.
26+
- **Testability** — Jest tests can assert policy size limits, validate structure, and verify coverage against the synthesized template.
27+
- **Co-location** — the CDK app defines what resources exist (and therefore what permissions are needed); both live in the same package.
28+
- **Self-contained**`cdk/` has its own `mise.toml`, build, and test pipeline.
29+
30+
### Triple-layer versioning
31+
32+
| Layer | Purpose |
33+
|-------|---------|
34+
| **Semver** | Quick operator answer: "do I need to re-bootstrap?" Major = breaking. |
35+
| **SHA256 hash** | Detects console drift — manual IAM edits that diverge from code. |
36+
| **Action-set comparison** | Precise gap reporting: exactly which actions are missing. |
37+
38+
Semver and hash are emitted as CloudFormation outputs on the CDKToolkit stack, enabling automated preflight checks.
39+
40+
### Two-layer preflight validation
41+
42+
1. **CDK Aspect (synth-time)** *(lands in #125)* — will run during `mise //cdk:synth`, visiting every `CfnResource`, looking up required actions in a resource-action-map (#124), and comparing against declared policy. Catches issues at dev time.
43+
2. **Live-account validator (deploy-time)** *(lands in #126)*`mise //cdk:preflight` will read CDKToolkit stack outputs, compare version/hash against requirements, and fail fast with an actionable "re-bootstrap required" message before CloudFormation starts.
44+
45+
### Custom bootstrap template
46+
47+
*(Lands in #123)* — will be generated from the policy source code (not hand-maintained). Operators will run `mise //cdk:bootstrap` to provision least-privilege roles in a single command. The template replaces `AdministratorAccess` with the three managed policies while retaining all other default bootstrap resources.
48+
49+
### Delivery via stacked PRs (ADR-001)
50+
51+
The implementation is decomposed into 8 sub-issues, each independently reviewable and deployable. See RFC #120 for the full stack.
52+
53+
## Consequences
54+
55+
- (+) Policies are diffable in PRs — IAM changes are code-reviewed like any other code
56+
- (+) Tests enforce the 6,144-char limit and structural validity on every commit
57+
- (+) Preflight prevents the "deploy, wait 15 minutes, fail, rollback" loop
58+
- (+) Single `mise //cdk:bootstrap` command replaces the multi-step manual process
59+
- (+) Agents can automatically update policies when they add new resource types
60+
- (-) Resource-action-map requires maintenance when new AWS resource types are added
61+
- (-) Rebase complexity from the 8-PR stack
62+
- (!) Bootstrap template drift — CDK upstream may change defaults; requires rebase on CDK major upgrades
63+
- (!) Operators with existing deployments must re-bootstrap (documented upgrade path provided)
64+
65+
## References
66+
67+
- [ADR-001](./001-stacked-pull-requests.md) — delivery methodology (stacked PRs)
68+
- RFC #120 — parent issue with full design and sub-issue breakdown
69+
- `docs/design/DEPLOYMENT_ROLES.md` — current documentation (will become generated)
70+
- PR #46 — original policy derivation and validation methodology
71+
- [CDK default bootstrap template](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml)
72+
- [IAM managed policy size limit](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html)

docs/src/content/docs/decisions/001-stacked-pull-requests.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,11 @@ When a lower PR changes after review feedback:
9898
The default topology is a **classic stack** — each PR targets its predecessor's branch. When an early PR merges to `main` before later PRs are reviewed:
9999

100100
1. **Retarget** all PRs that pointed at the merged branch to `main` (or to the next unmerged predecessor). Use `gh pr edit <N> --base main` or GitHub's "Retarget" button.
101-
2. **Rebase** each retargeted PR onto its new base so the diff is clean.
102-
3. **CI must pass** on each retargeted PR independently after rebase.
101+
2. **Rebase** each retargeted PR onto its new base so the diff is clean — use `git rebase --skip` for commits whose content is already in main via the merged predecessor.
102+
3. **Force-push with lease** (`--force-with-lease`) so the PR diff on GitHub shows only net-new changes, not already-merged content.
103+
4. **CI must pass** on each retargeted PR independently after rebase.
104+
105+
**This sequence is mandatory, not optional.** Until it completes, GitHub shows already-merged commits in the child PR's diff — reviewers cannot distinguish new work from old, defeating the purpose of stacking. A merge is not complete until all child PRs are rebased clean.
103106

104107
After retargeting, the remaining PRs form a shorter stack rooted on `main`. This is the expected, normal path — not an exception.
105108

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
title: 002 least privilege bootstrap policies
3+
---
4+
5+
# ADR-002: Least-privilege CDK bootstrap policies as code
6+
7+
**Status:** accepted
8+
**Date:** 2026-05-19
9+
**Implementation:** Tracked in RFC #120; artifacts referenced below land progressively across the 8-PR stack and are not yet present on `main`.
10+
11+
## Context
12+
13+
CDK bootstrap creates five roles per account/region. The **CloudFormation execution role** (cdk-hnb659fds-cfn-exec-role) receives `AdministratorAccess` by default — CloudFormation assumes it to create, modify, and delete stack resources. This violates least-privilege and may conflict with organizational SCPs or compliance gates.
14+
15+
The ABCA project documented three scoped policies in `docs/design/DEPLOYMENT_ROLES.md` (PR #46), validated against a live deployment through 7 iterations and 36 CloudTrail-discovered actions. However, these policies exist only as JSON blobs in a Markdown file — unversioned, untested, and manually applied.
16+
17+
**Failure mode without automation:** When a new release adds a resource type (e.g., SQS queue), operators who pull and deploy hit a mid-rollback CloudFormation failure because their bootstrap policy predates the new permissions. The deploy fails 15 minutes in with no prior warning.
18+
19+
**Constraints:**
20+
- IAM managed policies have a 6,144-character limit — hence the three-policy split (Infrastructure, Application, Observability).
21+
- Bootstrap must exist before the CDK app can deploy — circular dependency prevents managing bootstrap from within the app stack.
22+
- The four other bootstrap roles (deploy, lookup, file-publishing, image-publishing) are already scoped by the default template and don't need modification.
23+
24+
## Decision
25+
26+
### Policies as typed TypeScript code in `cdk/src/bootstrap/` *(lands in #122)*
27+
28+
Rationale for location:
29+
- **Agent routing**`AGENTS.md` routes CDK/IAM changes to `cdk/`. An agent modifying a construct that adds a DynamoDB table naturally looks here for the policy it must update.
30+
- **Testability** — Jest tests can assert policy size limits, validate structure, and verify coverage against the synthesized template.
31+
- **Co-location** — the CDK app defines what resources exist (and therefore what permissions are needed); both live in the same package.
32+
- **Self-contained**`cdk/` has its own `mise.toml`, build, and test pipeline.
33+
34+
### Triple-layer versioning
35+
36+
| Layer | Purpose |
37+
|-------|---------|
38+
| **Semver** | Quick operator answer: "do I need to re-bootstrap?" Major = breaking. |
39+
| **SHA256 hash** | Detects console drift — manual IAM edits that diverge from code. |
40+
| **Action-set comparison** | Precise gap reporting: exactly which actions are missing. |
41+
42+
Semver and hash are emitted as CloudFormation outputs on the CDKToolkit stack, enabling automated preflight checks.
43+
44+
### Two-layer preflight validation
45+
46+
1. **CDK Aspect (synth-time)** *(lands in #125)* — will run during `mise //cdk:synth`, visiting every `CfnResource`, looking up required actions in a resource-action-map (#124), and comparing against declared policy. Catches issues at dev time.
47+
2. **Live-account validator (deploy-time)** *(lands in #126)*`mise //cdk:preflight` will read CDKToolkit stack outputs, compare version/hash against requirements, and fail fast with an actionable "re-bootstrap required" message before CloudFormation starts.
48+
49+
### Custom bootstrap template
50+
51+
*(Lands in #123)* — will be generated from the policy source code (not hand-maintained). Operators will run `mise //cdk:bootstrap` to provision least-privilege roles in a single command. The template replaces `AdministratorAccess` with the three managed policies while retaining all other default bootstrap resources.
52+
53+
### Delivery via stacked PRs (ADR-001)
54+
55+
The implementation is decomposed into 8 sub-issues, each independently reviewable and deployable. See RFC #120 for the full stack.
56+
57+
## Consequences
58+
59+
- (+) Policies are diffable in PRs — IAM changes are code-reviewed like any other code
60+
- (+) Tests enforce the 6,144-char limit and structural validity on every commit
61+
- (+) Preflight prevents the "deploy, wait 15 minutes, fail, rollback" loop
62+
- (+) Single `mise //cdk:bootstrap` command replaces the multi-step manual process
63+
- (+) Agents can automatically update policies when they add new resource types
64+
- (-) Resource-action-map requires maintenance when new AWS resource types are added
65+
- (-) Rebase complexity from the 8-PR stack
66+
- (!) Bootstrap template drift — CDK upstream may change defaults; requires rebase on CDK major upgrades
67+
- (!) Operators with existing deployments must re-bootstrap (documented upgrade path provided)
68+
69+
## References
70+
71+
- [ADR-001](/architecture/001-stacked-pull-requests) — delivery methodology (stacked PRs)
72+
- RFC #120 — parent issue with full design and sub-issue breakdown
73+
- `docs/design/DEPLOYMENT_ROLES.md` — current documentation (will become generated)
74+
- PR #46 — original policy derivation and validation methodology
75+
- [CDK default bootstrap template](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml)
76+
- [IAM managed policy size limit](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html)

0 commit comments

Comments
 (0)