Merge pull request #98 from Sea-Haven-Industries/fix/cfn-exec-role-phase-b
Some checks are pending
ci / ci / ci (push) Waiting to run

fix(iam): drop the redundant inline boundary-gated policy (Phase B)
This commit is contained in:
Adam Moussa 2026-07-27 18:23:01 -04:00 • committed by GitHub
commit 2139662f5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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`. > **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.) > ⚠️ **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 # to Lambda. Verified live on this very role 2026-07-27 via
# simulate-principal-policy (returned: allowed). # simulate-principal-policy (returned: allowed).
# #
# DEPLOY NOTE: this resource is added while the inline # THIS IS THE ONLY COPY. It was introduced alongside an inline
# iam-role-management-boundary-gated policy is left in place. The two # iam-role-management-boundary-gated policy that carried the older,
# overlap by design — an explicit Deny beats an Allow anywhere in the # vulnerable version of these statements; that inline copy was removed once
# policy set, so the escalation closes the moment this lands, with no # this one was deployed and verified. Consolidating here also freed the
# window in which the role lacks its IAM permissions. The redundant # role's inline budget from 10,006 to 8,261 of the 10,240-byte limit —
# inline copy is removed in a separate follow-up change. # 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: SamCfnIamManagementPolicy:
Type: AWS::IAM::ManagedPolicy Type: AWS::IAM::ManagedPolicy
@ -942,110 +953,6 @@ Resources:
- wafv2:ListResourcesForWebACL - wafv2:ListResourcesForWebACL
Resource: "*" 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) # SAM deploy roles (4 repos)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------