mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
Merge pull request #62 from Sea-Haven-Industries/fix/cfn-exec-role-policy-size
fix(deploy-substrate): move boundary-gated IAM policy off the role's inline budget
This commit is contained in:
commit
ea2224e326
2 changed files with 170 additions and 132 deletions
|
|
@ -21,7 +21,10 @@ export interface DeploySubstrateStackProps extends cdk.StackProps {
|
|||
* - `seahaven-lambda-execution-boundary` permissions boundary (the ceiling
|
||||
* applied to every SAM-generated Lambda execution role),
|
||||
* - `github-cfn-execution-role` (the shared CloudFormation execution role
|
||||
* that cd-sam callers pass as cfn-role-arn).
|
||||
* that cd-sam callers pass as cfn-role-arn), plus
|
||||
* `seahaven-cfn-exec-iam-management`, the attached managed policy holding
|
||||
* that role's boundary-gated IAM statements (separated from the inline
|
||||
* policies to stay under IAM's 10,240-byte per-role inline limit).
|
||||
*
|
||||
* Deliberately NOT here: per-repo githubdeploy-* roles. Those are provisioned
|
||||
* per repo at migration/onboarding time (deploy-role-first playbook) so an
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ Description: >-
|
|||
# for mgmt where both resources already exist, so mgmt's copy is
|
||||
# deliberately unchanged),
|
||||
# - DeletionPolicy/UpdateReplacePolicy Retain on the OIDC provider,
|
||||
# - the boundary-gated IAM block moved from an INLINE role policy into an
|
||||
# attached managed policy (SamCfnIamManagementPolicy). Forced by IAM's
|
||||
# 10,240-byte per-role inline limit: mgmt's inline set is ~10.1 KB, i.e.
|
||||
# ~94 bytes from the cap, so the added Deny statements did not fit and the
|
||||
# first deploy failed with ServiceLimitExceeded (2026-07-27). Effective
|
||||
# permissions are unchanged — verified by comparing the full 27-statement
|
||||
# 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
|
||||
# removed from Sid IAMPutPermissionsBoundary and explicit Deny statements
|
||||
# (DenyBoundaryTampering / DenyBoundaryPolicyEdit) added. The mgmt copy
|
||||
|
|
@ -319,6 +328,8 @@ Resources:
|
|||
DependsOn: LambdaExecutionBoundary
|
||||
Properties:
|
||||
RoleName: github-cfn-execution-role
|
||||
ManagedPolicyArns:
|
||||
- !Ref SamCfnIamManagementPolicy
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
|
|
@ -751,142 +762,166 @@ 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"
|
||||
# ---------------------------------------------------------------------------
|
||||
# IAM role lifecycle - BOUNDARY-GATED (attached managed policy)
|
||||
#
|
||||
# Lives in a MANAGED policy, not inline on the role, because the role's
|
||||
# inline policies total ~10.1 KB against IAM's hard 10,240-byte per-role
|
||||
# inline limit - adding the Deny statements below inline exceeds it and
|
||||
# fails the deploy (ServiceLimitExceeded, hit live 2026-07-27). Attached
|
||||
# managed policies have their own separate 6,144-byte budget, so moving this
|
||||
# block out both fits the Denies and leaves ~1.9 KB of inline headroom for
|
||||
# future statements. Identity policies are unioned and an explicit Deny still
|
||||
# wins, so effective permissions are unchanged by the relocation.
|
||||
#
|
||||
# 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.
|
||||
# ---------------------------------------------------------------------------
|
||||
SamCfnIamManagementPolicy:
|
||||
Type: AWS::IAM::ManagedPolicy
|
||||
Properties:
|
||||
# Fixed name: changing it makes CloudFormation create a replacement policy
|
||||
# and detach this one, which briefly drops the role's IAM permissions
|
||||
# mid-update. Treat a rename as a coordinated migration, not an edit. This
|
||||
# is the role's FIRST attached managed policy (per-role quota is 10).
|
||||
ManagedPolicyName: seahaven-cfn-exec-iam-management
|
||||
Description: >-
|
||||
Boundary-gated IAM role lifecycle for github-cfn-execution-role, plus the
|
||||
explicit Deny backstops that keep the permissions boundary from being
|
||||
detached or rewritten. Separated from the role's inline policies to stay
|
||||
under IAM's 10,240-byte inline limit.
|
||||
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"
|
||||
# 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"
|
||||
# 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 — SET the boundary only. DELETE is NOT
|
||||
# granted: for a delete, the iam:PermissionsBoundary condition key
|
||||
# reflects the boundary CURRENTLY attached to the target role, so
|
||||
# a StringEquals condition on the boundary ARN MATCHES exactly the
|
||||
# roles the gate protects. Granting delete under that condition
|
||||
# lets this role create a boundary-gated role with an inline *:*
|
||||
# policy, strip the boundary, and pass the now-unbounded role to
|
||||
# Lambda — defeating the primary escalation control. Verified live
|
||||
# against the mgmt copy 2026-07-27 (simulate-principal-policy:
|
||||
# iam:DeleteRolePermissionsBoundary = allowed). SAM never needs
|
||||
# the delete: it only SETS the boundary on roles it creates, and
|
||||
# stack teardown calls DeleteRole, not DeleteRolePermissionsBoundary.
|
||||
- Sid: IAMPutPermissionsBoundary
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:PutRolePermissionsBoundary
|
||||
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 — SET the boundary only. DELETE is NOT
|
||||
# granted: for a delete, the iam:PermissionsBoundary condition key
|
||||
# reflects the boundary CURRENTLY attached to the target role, so
|
||||
# a StringEquals condition on the boundary ARN MATCHES exactly the
|
||||
# roles the gate protects. Granting delete under that condition
|
||||
# lets this role create a boundary-gated role with an inline *:*
|
||||
# policy, strip the boundary, and pass the now-unbounded role to
|
||||
# Lambda — defeating the primary escalation control. Verified live
|
||||
# against the mgmt copy 2026-07-27 (simulate-principal-policy:
|
||||
# iam:DeleteRolePermissionsBoundary = allowed). SAM never needs
|
||||
# the delete: it only SETS the boundary on roles it creates, and
|
||||
# stack teardown calls DeleteRole, not DeleteRolePermissionsBoundary.
|
||||
- Sid: IAMPutPermissionsBoundary
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:PutRolePermissionsBoundary
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
|
||||
# Explicit Deny backstop (AWS's documented NoBoundaryPolicyEdit /
|
||||
# NoBoundaryDelete delegation pattern). A Deny is required, not
|
||||
# merely omitting the Allow: without it, any future Allow added to
|
||||
# this role — or a broader managed policy attached to it — silently
|
||||
# reopens the escalation. Covers both removing a boundary from a
|
||||
# role and rewriting the boundary POLICY DOCUMENT itself (the
|
||||
# latter is only implicitly denied today).
|
||||
- Sid: DenyBoundaryTampering
|
||||
Effect: Deny
|
||||
Action:
|
||||
- iam:DeleteRolePermissionsBoundary
|
||||
- iam:DeleteUserPermissionsBoundary
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:user/*"
|
||||
# Explicit Deny backstop (AWS's documented NoBoundaryPolicyEdit /
|
||||
# NoBoundaryDelete delegation pattern). A Deny is required, not
|
||||
# merely omitting the Allow: without it, any future Allow added to
|
||||
# this role — or a broader managed policy attached to it — silently
|
||||
# reopens the escalation. Covers both removing a boundary from a
|
||||
# role and rewriting the boundary POLICY DOCUMENT itself (the
|
||||
# latter is only implicitly denied today).
|
||||
- Sid: DenyBoundaryTampering
|
||||
Effect: Deny
|
||||
Action:
|
||||
- iam:DeleteRolePermissionsBoundary
|
||||
- iam:DeleteUserPermissionsBoundary
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:user/*"
|
||||
|
||||
- Sid: DenyBoundaryPolicyEdit
|
||||
Effect: Deny
|
||||
Action:
|
||||
- iam:CreatePolicyVersion
|
||||
- iam:SetDefaultPolicyVersion
|
||||
- iam:DeletePolicyVersion
|
||||
- iam:DeletePolicy
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
- Sid: DenyBoundaryPolicyEdit
|
||||
Effect: Deny
|
||||
Action:
|
||||
- iam:CreatePolicyVersion
|
||||
- iam:SetDefaultPolicyVersion
|
||||
- iam:DeletePolicyVersion
|
||||
- iam:DeletePolicy
|
||||
Resource:
|
||||
- !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: "*"
|
||||
# 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"
|
||||
# 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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue