fix(iam): port DenySelfMutation self-protection into the prod/dev deploy substrate

The seahaven-cfn-exec-iam-management policy in prod and dev carried only
DenyBoundaryTampering + DenyBoundaryPolicyEdit: the mgmt Phase A review
later showed a Deny-in-a-managed-policy control is self-detachable
(iam:DetachRolePolicy on * is unconditioned), so without DenySelfMutation
the exec role can detach the very policy carrying the Denies and
reinstate the boundary-removal escalation. Latent today (no PassRole
grants, zero SAM stacks in prod/dev) but must be closed before the first
SAM workload migrates.

Ports verbatim from .github/oidc-deploy-roles.yaml (mgmt, PRs #95/#98):
- DenySelfMutation over role/github-cfn-execution-role + githubdeploy-*
- DenyBoundaryPolicyEdit widened to policy/seahaven-*

Statement set verified byte-identical to the mgmt copy (9 sids);
provenance header updated - the two copies are reconciled.
This commit is contained in:
Adam Moussa 2026-07-27 18:37:39 -04:00
parent ea2224e326
commit 62f6a76e6c
No known key found for this signature in database

View file

@ -32,16 +32,14 @@ Description: >-
# set before and after the move (identical), since identity policies are
# unioned and an explicit Deny still wins. NOTE for the mgmt remediation:
# mgmt needs this same restructure before its Deny statements can be added,
# - SECURITY FIX, deliberate divergence: iam:DeleteRolePermissionsBoundary
# - SECURITY FIX (now in BOTH copies): iam:DeleteRolePermissionsBoundary
# removed from Sid IAMPutPermissionsBoundary and explicit Deny statements
# (DenyBoundaryTampering / DenyBoundaryPolicyEdit) added. The mgmt copy
# still carries the hole — verified live 2026-07-27 via
# simulate-principal-policy on the deployed mgmt role
# (iam:DeleteRolePermissionsBoundary = ALLOWED). New accounts must not be
# born with it. Remediating mgmt means changing a role that is actively
# executing production deploys, so it is tracked as a separate change
# with its own review gates rather than folded in here. Reconcile the two
# copies when that lands.
# (DenyBoundaryTampering / DenyBoundaryPolicyEdit / DenySelfMutation)
# added. The mgmt copy was remediated 2026-07-27 (.github PRs #95 Phase A
# + #98 Phase B); DenySelfMutation and the widened policy/seahaven-*
# DenyBoundaryPolicyEdit scope were then ported back here, so the two
# copies' statement sets are reconciled as of that date. If a substrate
# statement changes again, change BOTH files in the same piece of work.
#
# This template is deployed via lib/deploy-substrate-stack.ts
# (cloudformation-include) as stack seahaven-deploy-substrate, once per
@ -880,6 +878,12 @@ Resources:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
- !Sub "arn:aws:iam::${AWS::AccountId}:user/*"
# Scoped to the whole seahaven-* policy family, not just the boundary:
# this policy carries the Deny statements, so it is now a
# higher-value target than the boundary it protects. Safe to scope
# broadly — the role holds no iam:CreatePolicy anywhere and no SAM
# stack manages a managed policy through it (both verified
# 2026-07-27), so nothing legitimate writes policy versions here.
- Sid: DenyBoundaryPolicyEdit
Effect: Deny
Action:
@ -888,7 +892,50 @@ Resources:
- iam:DeletePolicyVersion
- iam:DeletePolicy
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
- !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-*"
# Self-protection. Without this the whole control is one API call
# from being undone: IAMRoleReadAndDelete below grants
# iam:DetachRolePolicy on Resource "*" with no condition, so this
# role could detach the very policy carrying these Denies from
# itself and reinstate the escalation. Verified live 2026-07-27:
# simulate-principal-policy returned "allowed" for DetachRolePolicy,
# DeleteRolePolicy and DeleteRole against this role's own ARN and
# against githubdeploy-* roles.
#
# Also closes a denial-of-service and a self-elevation precondition:
# iam:PutRolePermissionsBoundary is condition-pinned to the Lambda
# boundary ARN but NOT scoped by target, so this role could apply
# that runtime boundary to itself or to a githubdeploy-* role —
# bricking the pipelines, unrecoverable without an admin because
# removing a boundary is denied above, and making the otherwise-inert
# AttachRolePolicy/PutRolePolicy self-elevation conditions start
# matching.
#
# Costs nothing operationally: this role is only ever passed to
# CloudFormation for SAM application stacks. The substrate's own
# roles are managed by THIS stack (deployed through the CDK
# bootstrap execution role), and per-repo githubdeploy-* roles are
# provisioned at onboarding time outside any stack this role
# executes — so CloudFormation never exercises these actions
# against them as this role. SAM-generated roles are named
# <stack>-<Function>Role-<hash> and are unaffected.
- Sid: DenySelfMutation
Effect: Deny
Action:
- iam:AttachRolePolicy
- iam:DeleteRole
- iam:DeleteRolePolicy
- iam:DeleteRolePermissionsBoundary
- iam:DetachRolePolicy
- iam:PutRolePolicy
- iam:PutRolePermissionsBoundary
- iam:UpdateAssumeRolePolicy
- iam:UpdateRole
- iam:UpdateRoleDescription
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/github-cfn-execution-role"
- !Sub "arn:aws:iam::${AWS::AccountId}:role/githubdeploy-*"
# Read / tag / delete role and policy — no boundary condition needed
- Sid: IAMRoleReadAndDelete