fix(iam): drop the redundant inline boundary-gated policy (Phase B)

Phase A moved the CFN execution role's boundary-gated IAM statements into
the attached seahaven-cfn-exec-iam-management managed policy, with the
escalation fixed and three Deny backstops, while deliberately leaving the
old inline iam-role-management-boundary-gated policy in place so that
deploy removed nothing. That is now redundant and this removes it.

Effective permissions are unchanged, proven statically before deploying:
of the 6 Allow statements being removed, 5 are byte-identical to the
managed policy's. The only difference is Sid IAMPutPermissionsBoundary,
where the inline copy also listed iam:DeleteRolePermissionsBoundary --
the action DenyBoundaryTampering explicitly denies, so that Allow was
already inert.

Frees the scarce budget: inline usage drops from 10,006 to 8,261 of IAM's
10,240-byte per-role limit, leaving 1,979 bytes of headroom on a role that
previously had 234.
This commit is contained in:
Adam Moussa 2026-07-27 18:20:48 -04:00
parent eb20573a8b
commit 6c9582f242
No known key found for this signature in database
2 changed files with 18 additions and 111 deletions

View file

@ -66,7 +66,7 @@ PR reviews are handled by the **official Claude Code GitHub App** (installed org
> **Constraint for future maintainers.** `github-cfn-execution-role` is explicitly denied from mutating the deploy substrate's own principals — itself, any `githubdeploy-*` role, and any `seahaven-*` managed policy. Those are owned by this stack and deployed manually with administrator credentials, so nothing legitimate needs that path. If you ever add automation that manages one of them, it must not run through `github-cfn-execution-role` or it will fail with `AccessDenied`.
> **Phase A / Phase B.** The boundary-gated statements are currently duplicated: the new managed policy carries the corrected set, and the older inline `iam-role-management-boundary-gated` policy is still present. That overlap is deliberate and temporary — an explicit Deny beats an Allow anywhere in the policy set, so the corrected version already governs, and keeping the inline copy meant CloudFormation removed nothing during the change. **Phase B deletes the inline copy** (inline usage 10,006 → 8,261). Do not delete it as "redundant" outside that planned change.
> **Where the exec role's IAM statements live.** All of them are in the attached `seahaven-cfn-exec-iam-management` managed policy — there is no inline copy. The role's inline policies were previously at 10,006 of the 10,240-byte limit, leaving no room to add anything; consolidating into the managed policy brought that to **8,261 bytes (1,979 free)**. If you need to add a permission to this role, prefer the managed policy: the inline budget is the scarce one.
> ⚠️ **This stack has no CD pipeline — it is deployed manually.** (It defines the very roles the pipelines use, so it can't deploy itself.)

View file

@ -267,12 +267,23 @@ Resources:
# to Lambda. Verified live on this very role 2026-07-27 via
# simulate-principal-policy (returned: allowed).
#
# DEPLOY NOTE: this resource is added while the inline
# iam-role-management-boundary-gated policy is left in place. The two
# overlap by design — an explicit Deny beats an Allow anywhere in the
# policy set, so the escalation closes the moment this lands, with no
# window in which the role lacks its IAM permissions. The redundant
# inline copy is removed in a separate follow-up change.
# THIS IS THE ONLY COPY. It was introduced alongside an inline
# iam-role-management-boundary-gated policy that carried the older,
# vulnerable version of these statements; that inline copy was removed once
# this one was deployed and verified. Consolidating here also freed the
# role's inline budget from 10,006 to 8,261 of the 10,240-byte limit —
# prefer adding future statements here rather than inline.
#
# OPERATIONAL NOTES
# - Detaching or deleting this policy does not just drop the Deny backstops,
# it drops every IAM permission the role has, so SAM deploys stop working
# immediately and loudly rather than silently becoming less safe. The role
# cannot do it to itself (DenySelfMutation below), but an administrator
# can — treat detach/delete as a break-glass action, not a cleanup step.
# - A managed policy keeps at most 5 versions. CloudFormation creates a new
# version on every change to this document, so if an update ever fails with
# LimitExceeded, prune old versions (list-policy-versions /
# delete-policy-version) rather than assuming the template is wrong.
# ---------------------------------------------------------------------------
SamCfnIamManagementPolicy:
Type: AWS::IAM::ManagedPolicy
@ -942,110 +953,6 @@ Resources:
- wafv2:ListResourcesForWebACL
Resource: "*"
# ── IAM role lifecycle — BOUNDARY-GATED ──────────────────────────
# This is the PRIMARY escalation control for INFRA-97.
#
# iam:CreateRole / iam:AttachRolePolicy / iam:PutRolePolicy are
# conditioned on iam:PermissionsBoundary StringEquals the
# seahaven-lambda-execution-boundary ARN. That condition means
# any role this execution role creates must have the boundary
# applied, so it can never exceed what the boundary allows
# (which is scoped to the services the five stacks actually use).
#
# iam:PassRole is also included here so CloudFormation can pass
# the auto-generated Lambda execution role to the Lambda service.
#
# Why not path-scoped (e.g. iam:ResourceTag / path /cfn-managed/)?
# SAM's AWS::Serverless::Function auto-generates execution roles at
# path / — there is no supported way to set a custom RolePath on
# SAM auto-roles. A path condition would therefore exclude the
# SAM auto-roles and break every deploy. The PermissionsBoundary
# condition achieves the same security goal without a path requirement.
- PolicyName: iam-role-management-boundary-gated
PolicyDocument:
Version: "2012-10-17"
Statement:
# Create role — MUST attach boundary
- Sid: IAMCreateRoleWithBoundary
Effect: Allow
Action:
- iam:CreateRole
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
Condition:
StringEquals:
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
# Attach managed policies — MUST have boundary already on role
- Sid: IAMAttachPolicyWithBoundary
Effect: Allow
Action:
- iam:AttachRolePolicy
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
Condition:
StringEquals:
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
# Put inline policy — MUST have boundary already on role
- Sid: IAMPutRolePolicyWithBoundary
Effect: Allow
Action:
- iam:PutRolePolicy
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
Condition:
StringEquals:
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
# Boundary management — can only put/delete the boundary itself
# (so SAM can set PermissionsBoundary on the roles it creates)
- Sid: IAMPutPermissionsBoundary
Effect: Allow
Action:
- iam:PutRolePermissionsBoundary
- iam:DeleteRolePermissionsBoundary
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
Condition:
StringEquals:
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
# Read / tag / delete role and policy — no boundary condition needed
- Sid: IAMRoleReadAndDelete
Effect: Allow
Action:
- iam:DeleteRole
- iam:DeleteRolePolicy
- iam:DetachRolePolicy
- iam:GetRole
- iam:GetRolePolicy
- iam:ListAttachedRolePolicies
- iam:ListRolePolicies
- iam:ListRoles
- iam:TagRole
- iam:UntagRole
- iam:UpdateRole
- iam:UpdateRoleDescription
- iam:UpdateAssumeRolePolicy
- iam:GetPolicy
- iam:GetPolicyVersion
- iam:ListPolicies
- iam:ListPolicyVersions
Resource: "*"
# PassRole — CloudFormation passes the Lambda execution role
# to the Lambda service. Scoped to SAM-generated role pattern.
- Sid: IAMPassRole
Effect: Allow
Action:
- iam:PassRole
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
Condition:
StringEquals:
"iam:PassedToService": "lambda.amazonaws.com"
# ---------------------------------------------------------------------------
# SAM deploy roles (4 repos)
# ---------------------------------------------------------------------------