Review of the DenySelfMutation port found the header's 'reconciled' claim
was not yet true: mgmt Phase A also added the CloudWatch Logs
metric-filter actions (afterhours-shift-manager creates an
AWS::Logs::MetricFilter through this role), and without them a migrating
SAM stack fails mid-deploy with AccessDenied. Ports those three actions
and corrects two stale header notes. Every IAM statement in the three
shared resources is now byte-identical across both files, verified
programmatically; the only delta left is the DependsOn ordering line.
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.
The first deploy of seahaven-deploy-substrate failed in both prod and dev
with ServiceLimitExceeded: 'Maximum policy size of 10240 bytes exceeded
for role github-cfn-execution-role'. The role's inline policies already
sat ~94 bytes under IAM's hard 10,240-byte per-role limit, so the two
Deny statements added to close the boundary-removal escalation did not
fit (10,656 total).
Moves the whole boundary-gated IAM block (6 Allow + 2 Deny statements)
into an attached managed policy, which carries its own separate
6,144-byte budget. Inline drops to 8,285 with ~1.9 KB of headroom;
the managed policy sits at 2,371.
Effective permissions are unchanged: the union of role statements
(inline + attached) is byte-identical as a sorted set before and after
the move (27 statements both sides), identity policies are unioned, and
an explicit Deny still wins. Boundary and trust policy untouched.
Both failed stacks rolled back cleanly with zero orphaned resources and
were deleted before this retry.
SAM repos migrating off the frozen management account need the shared
deploy plumbing (permissions boundary + github-cfn-execution-role) in
their target account; none of it existed outside mgmt, so there was no
OIDC SAM deploy path into seahaven-prod or seahaven-dev at all.
Adds a templated, per-account substrate stack so onboarding a future
account is one bin/app.ts instance plus one CD job, not a hand-rolled
copy. Per-repo githubdeploy-* roles stay out by design: they are
provisioned per repo at migration time so an account never accumulates
trust for repos that do not deploy to it.
The template is a verbatim extraction of the reviewed mgmt substrate,
with deliberate, documented divergences — notably the removal of
iam:DeleteRolePermissionsBoundary plus explicit Deny backstops, which
closes a confirmed privilege-escalation path (see PR body).