From 83d0d0eb0df1f3f7d98d83ed171e69283b921a24 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 27 Jul 2026 19:15:08 -0400 Subject: [PATCH] docs(readme): document the deploy substrate's escalation controls The substrate section described what the stack contains but not the three Deny statements that make the boundary gate hold, so a future editor could remove or weaken them without knowing what they defend. Records why DenySelfMutation is required (the role holds unconditioned DetachRolePolicy on * and could detach the Deny-carrying policy from itself), how to verify a change by simulation, the prod/dev caveat that simulation cannot see this role's inline policies, and the rollback-wedge recovery the mgmt README already carried. --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index ac223fb..5a3a0cf 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,36 @@ of the substrate — they are provisioned per repo at migration/onboarding time so an account never carries trust relationships for repos that do not deploy to it. +**Escalation controls on `github-cfn-execution-role`.** Every `iam:CreateRole`, +`AttachRolePolicy` and `PutRolePolicy` is conditioned on the target carrying +`seahaven-lambda-execution-boundary`. That condition alone is not sufficient, +so the attached `seahaven-cfn-exec-iam-management` managed policy also carries +three explicit Deny statements: + +- `DenyBoundaryTampering` — no removing a boundary from any role or user. + Granting the delete under the same `StringEquals` condition self-defeats the + gate, because for a delete the condition key resolves to the boundary already + on the target. +- `DenyBoundaryPolicyEdit` — no rewriting any `seahaven-*` managed policy. +- `DenySelfMutation` — the role cannot modify or delete itself or any + `githubdeploy-*` role. Without it the control is one API call from being + undone: `IAMRoleReadAndDelete` grants `iam:DetachRolePolicy` on `Resource: + "*"` unconditioned, so the role could detach the very policy carrying these + Denies. + +Verify a change to these with `aws iam simulate-principal-policy` against the +role's own ARN (expect `explicitDeny`) and against a `-Role-` +name (expect `allowed`, no regression for normal SAM deploys). Note that +simulation currently does **not** see this role's *inline* policies in +seahaven-prod or seahaven-dev — read those back with `get-role-policy` instead. + +Known consequence of `DenyBoundaryTampering`: a CloudFormation rollback of an +update that *adds* a boundary to an existing role wedges in +`UPDATE_ROLLBACK_FAILED`. Recovery is an administrator action, not a pipeline +retry — `aws cloudformation continue-update-rollback --stack-name +--resources-to-skip `. Unreachable while every SAM role is +created with the boundary already attached. + **Onboarding a future account as a deploy target:** 1. CDK-bootstrap the account (`npx cdk bootstrap aws:///us-east-1`