From 6c9582f242e1c829e9e25ead3ef846d5992796e1 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 27 Jul 2026 18:20:48 -0400 Subject: [PATCH] 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. --- README.md | 2 +- oidc-deploy-roles.yaml | 127 ++++++----------------------------------- 2 files changed, 18 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index d8657af..8d1fc95 100644 --- a/README.md +++ b/README.md @@ -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.) diff --git a/oidc-deploy-roles.yaml b/oidc-deploy-roles.yaml index d027e55..ad64e94 100644 --- a/oidc-deploy-roles.yaml +++ b/oidc-deploy-roles.yaml @@ -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) # ---------------------------------------------------------------------------