mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
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.
927 lines
41 KiB
YAML
927 lines
41 KiB
YAML
AWSTemplateFormatVersion: "2010-09-09"
|
|
Description: >-
|
|
Per-account GitHub Actions deploy substrate for Sea Haven Industries:
|
|
the shared account-level resources every SAM deploy pipeline needs
|
|
(GitHub OIDC provider, Lambda execution permissions boundary, and the
|
|
shared CloudFormation execution role). Per-repo githubdeploy-* roles
|
|
are NOT here — they are provisioned per repo at migration/onboarding
|
|
time in the target account.
|
|
|
|
# PROVENANCE / DRIFT WARNING
|
|
# The Resources below are a VERBATIM extraction of the substrate section
|
|
# (OIDC provider + LambdaExecutionBoundary + SamCfnExecutionRole) of
|
|
# Sea-Haven-Industries/.github/oidc-deploy-roles.yaml at commit 786dcfe8,
|
|
# which remains the deployed source of truth for the management account
|
|
# (328440206208) until that account's stacks finish migrating out. If a
|
|
# substrate resource must change while both copies are live, change BOTH
|
|
# files in the same piece of work. Documented deltas from the source:
|
|
# - unused GitHubOrg parameter dropped (only serves the per-repo roles
|
|
# left behind),
|
|
# - DependsOn: LambdaExecutionBoundary added to SamCfnExecutionRole (the
|
|
# role only names the boundary ARN inside Condition strings, so CFN
|
|
# infers no edge; first-create needs the boundary to exist first — moot
|
|
# 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
|
|
# 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.
|
|
#
|
|
# This template is deployed via lib/deploy-substrate-stack.ts
|
|
# (cloudformation-include) as stack seahaven-deploy-substrate, once per
|
|
# member account that hosts SAM workloads.
|
|
|
|
Parameters:
|
|
CreateOIDCProvider:
|
|
Type: String
|
|
Default: "false"
|
|
AllowedValues: ["true", "false"]
|
|
Description: Set to true only if the GitHub OIDC provider does not already exist in this account
|
|
|
|
Conditions:
|
|
ShouldCreateOIDCProvider: !Equals [!Ref CreateOIDCProvider, "true"]
|
|
|
|
Resources:
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OIDC Provider (conditional — most accounts already have it; seahaven-prod
|
|
# and seahaven-dev both do, from their githubdeploy-* role provisioning)
|
|
# ---------------------------------------------------------------------------
|
|
GitHubOIDCProvider:
|
|
Type: AWS::IAM::OIDCProvider
|
|
Condition: ShouldCreateOIDCProvider
|
|
Properties:
|
|
Url: https://token.actions.githubusercontent.com
|
|
ClientIdList:
|
|
- sts.amazonaws.com
|
|
ThumbprintList:
|
|
- 6938fd4d98bab03faadb97b34396831e3780aea1
|
|
# An account has exactly ONE provider per URL and every githubdeploy-* role
|
|
# trusts it. Retain so that flipping createOidcProvider back to false (or
|
|
# deleting this stack) can never delete the account's federation anchor and
|
|
# break every deploy into it.
|
|
DeletionPolicy: Retain
|
|
UpdateReplacePolicy: Retain
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lambda execution permissions boundary (INFRA-103)
|
|
#
|
|
# This managed policy is the CEILING for every Lambda execution role that the
|
|
# five SAM stacks auto-generate via AWS::Serverless::Function. Applying it as
|
|
# PermissionsBoundary on those roles means the effective permissions are the
|
|
# intersection of the role's own policies and this boundary, so a misconfigured
|
|
# SAM role can never exceed what is listed here.
|
|
#
|
|
# The boundary is intentionally a SUPERSET of the union of all runtime
|
|
# permissions currently granted across the five stacks. Being slightly broad
|
|
# is the correct trade-off at this stage — a boundary that is too tight will
|
|
# break Lambda functions at runtime after deploy, which is worse than a slightly
|
|
# loose boundary that is tightened in a follow-up.
|
|
#
|
|
# Permission sources per stack:
|
|
#
|
|
# afterhours-shift-manager
|
|
# - DynamoDB CRUD (afterhours-shifts table)
|
|
# - secretsmanager:GetSecretValue (afterhours-shift-manager/*)
|
|
# - ses:SendEmail (SES identity)
|
|
# - CloudWatch Logs (all functions)
|
|
#
|
|
# payments-dashboard
|
|
# - DynamoDB CRUD / Read (PaymentsDashboard table)
|
|
# - S3 GetObject (payroll-emails, payments-csv buckets)
|
|
# - secretsmanager:GetSecretValue (payments-dashboard/*)
|
|
# - sqs:SendMessage + sqs:ReceiveMessage + sqs:DeleteMessage etc.
|
|
# (PayrollBatchQueue + DLQs)
|
|
# - lambda:InvokeFunction (ExpenseReceiver → ExpenseProcessor)
|
|
# - ec2:CreateNetworkInterface / DescribeNetworkInterfaces /
|
|
# DeleteNetworkInterface (VPC-attached functions)
|
|
# - CloudWatch Logs
|
|
#
|
|
# meal-order-manager
|
|
# - DynamoDB CRUD / Read (meal-order-manager-orders table)
|
|
# - S3 CRUD (ReportsBucket) + s3:GetObject (ReportsBucket presigned URLs)
|
|
# - secretsmanager:GetSecretValue (meal-order-manager/*)
|
|
# - ssm:GetParameter (/meal-order-manager/*)
|
|
# - lambda:InvokeFunction (submit-order → slack-notifier,
|
|
# close-form → aggregate-orders)
|
|
# - ses:SendRawEmail
|
|
# - CloudWatch Logs
|
|
#
|
|
# front-integrations
|
|
# - DynamoDB CRUD (front-sla-alerts table)
|
|
# - secretsmanager:GetSecretValue (by ARN, various)
|
|
# - CloudWatch Logs
|
|
#
|
|
# afi-backup-monitor
|
|
# - secretsmanager:GetSecretValue (by ARN)
|
|
# - CloudWatch Logs
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
LambdaExecutionBoundary:
|
|
Type: AWS::IAM::ManagedPolicy
|
|
Properties:
|
|
ManagedPolicyName: seahaven-lambda-execution-boundary
|
|
Description: >-
|
|
Permissions boundary ceiling for all SAM-managed Lambda execution roles.
|
|
Applied via PermissionsBoundary on every Globals.Function in the five
|
|
SAM stacks (INFRA-103). Effective permissions are the intersection of
|
|
this policy and the role's own inline policies.
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
|
|
# ── CloudWatch Logs (every Lambda) ──────────────────────────────────
|
|
- Sid: CloudWatchLogs
|
|
Effect: Allow
|
|
Action:
|
|
- logs:CreateLogGroup
|
|
- logs:CreateLogStream
|
|
- logs:PutLogEvents
|
|
- logs:DescribeLogGroups
|
|
- logs:DescribeLogStreams
|
|
Resource: "*"
|
|
|
|
# ── X-Ray tracing (standard Lambda execution) ────────────────────
|
|
- Sid: XRay
|
|
Effect: Allow
|
|
Action:
|
|
- xray:PutTraceSegments
|
|
- xray:PutTelemetryRecords
|
|
Resource: "*"
|
|
|
|
# ── VPC / ENI management (payments-dashboard VPC functions) ────────
|
|
# Matches AWSLambdaVPCAccessExecutionRole exactly.
|
|
# AssignPrivateIpAddresses / UnassignPrivateIpAddresses are for EFA
|
|
# and secondary IPs — not part of the Lambda ENI lifecycle — omitted.
|
|
- Sid: Ec2Eni
|
|
Effect: Allow
|
|
Action:
|
|
- ec2:CreateNetworkInterface
|
|
- ec2:DescribeNetworkInterfaces
|
|
- ec2:DeleteNetworkInterface
|
|
- ec2:DescribeSubnets
|
|
- ec2:DescribeSecurityGroups
|
|
- ec2:DescribeVpcs
|
|
Resource: "*"
|
|
|
|
# ── DynamoDB (afterhours, payments, meal-order, front-integrations) ─
|
|
# Table/* covers base-table operations; table/*/index/* is required for
|
|
# Query/Scan on Global Secondary Indexes.
|
|
- Sid: DynamoDB
|
|
Effect: Allow
|
|
Action:
|
|
- dynamodb:GetItem
|
|
- dynamodb:PutItem
|
|
- dynamodb:UpdateItem
|
|
- dynamodb:DeleteItem
|
|
- dynamodb:Query
|
|
- dynamodb:Scan
|
|
- dynamodb:BatchGetItem
|
|
- dynamodb:BatchWriteItem
|
|
- dynamodb:DescribeTable
|
|
- dynamodb:ConditionCheckItem
|
|
Resource:
|
|
- !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*"
|
|
- !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*/index/*"
|
|
|
|
# ── S3 (payments-dashboard read, meal-order-manager CRUD) ──────────
|
|
- Sid: S3
|
|
Effect: Allow
|
|
Action:
|
|
- s3:GetObject
|
|
- s3:PutObject
|
|
- s3:DeleteObject
|
|
- s3:ListBucket
|
|
- s3:GetBucketLocation
|
|
- s3:GetObjectVersion
|
|
- s3:GetObjectTagging
|
|
- s3:PutObjectTagging
|
|
Resource:
|
|
- !Sub "arn:aws:s3:::*-${AWS::AccountId}"
|
|
- !Sub "arn:aws:s3:::*-${AWS::AccountId}/*"
|
|
# meal-order-manager ReportsBucket (non-AccountId suffix pattern)
|
|
- !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}"
|
|
- !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*"
|
|
- !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}"
|
|
- !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*"
|
|
|
|
# ── Secrets Manager (all stacks) ──────────────────────────────────
|
|
- Sid: SecretsManager
|
|
Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
- secretsmanager:DescribeSecret
|
|
Resource:
|
|
- !Sub "arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:*"
|
|
|
|
# ── SSM Parameter Store (meal-order-manager, afterhours) ──────────
|
|
- Sid: SSMParameterRead
|
|
Effect: Allow
|
|
Action:
|
|
- ssm:GetParameter
|
|
- ssm:GetParameters
|
|
- ssm:GetParametersByPath
|
|
Resource:
|
|
- !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*"
|
|
|
|
# ── SQS (payments-dashboard batch queues) ─────────────────────────
|
|
- Sid: SQS
|
|
Effect: Allow
|
|
Action:
|
|
- sqs:SendMessage
|
|
- sqs:ReceiveMessage
|
|
- sqs:DeleteMessage
|
|
- sqs:GetQueueAttributes
|
|
- sqs:GetQueueUrl
|
|
- sqs:ChangeMessageVisibility
|
|
Resource:
|
|
- !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*"
|
|
|
|
# ── Lambda invocation (payments, meal-order inter-function calls) ──
|
|
- Sid: LambdaInvoke
|
|
Effect: Allow
|
|
Action:
|
|
- lambda:InvokeFunction
|
|
Resource:
|
|
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:*"
|
|
|
|
# ── SES (afterhours weekly-post, meal-order email-report) ──────────
|
|
- Sid: SES
|
|
Effect: Allow
|
|
Action:
|
|
- ses:SendEmail
|
|
- ses:SendRawEmail
|
|
Resource:
|
|
- !Sub "arn:aws:ses:us-east-1:${AWS::AccountId}:identity/*"
|
|
- !Sub "arn:aws:ses:us-east-1:${AWS::AccountId}:configuration-set/*"
|
|
|
|
# ── KMS (CMK-encrypted resources) ─────────────────────────────────
|
|
# Required for Lambda functions that read/write CMK-encrypted AWS
|
|
# resources. Verified live state:
|
|
# - PaymentsDashboard DynamoDB table: CMK key/0b660af3 (KMS:ENABLED)
|
|
# - payments-dashboard CloudWatch log groups: CMK key/b748750c
|
|
# Secrets Manager + SQS queues in these stacks use AWS-managed keys
|
|
# (aws/secretsmanager, aws/sqs) which do not require explicit kms:*
|
|
# actions in the execution role policy. The CMK keys are scoped to
|
|
# this account to prevent cross-account KMS calls.
|
|
- Sid: KMS
|
|
Effect: Allow
|
|
Action:
|
|
- kms:Decrypt
|
|
- kms:GenerateDataKey
|
|
- kms:DescribeKey
|
|
Resource:
|
|
- !Sub "arn:aws:kms:us-east-1:${AWS::AccountId}:key/*"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped
|
|
#
|
|
# Replaces the previous blanket managed-policy set (IAMFullAccess +
|
|
# *FullAccess) with per-service inline statements that cover exactly
|
|
# what the five SAM stacks need during a CloudFormation deploy/update.
|
|
#
|
|
# PRIMARY ESCALATION CONTROL
|
|
# iam:CreateRole and iam:AttachRolePolicy / iam:PutRolePolicy are
|
|
# conditioned on iam:PermissionsBoundary StringEquals the boundary ARN
|
|
# (seahaven-lambda-execution-boundary, created in INFRA-103). That
|
|
# condition is what prevents the CFN execution role from minting an
|
|
# unconstrained admin role.
|
|
#
|
|
# SAM RolePath deviation note
|
|
# The original cross-review suggestion mentioned scoping IAM role
|
|
# creation to a specific path (/cfn-managed/). AWS::Serverless::Function
|
|
# does NOT support a custom RolePath on auto-generated execution roles —
|
|
# the PermissionsBoundary property is supported, but the role always lands
|
|
# at path /. Relying on a path condition (iam:ResourceTag or path-prefix)
|
|
# would therefore exclude the SAM auto-roles and break every deploy.
|
|
# The iam:PermissionsBoundary condition achieves the same security goal
|
|
# without requiring a path. For any explicit AWS::IAM::Role resources
|
|
# in SAM templates (e.g. AdminAuthorizerInvokeRole in meal-order-manager)
|
|
# where we can control the path, path scoping can be added in a follow-up.
|
|
#
|
|
# DEPLOY ORDER DEPENDENCY
|
|
# This role references the boundary ARN only as literal !Sub strings inside
|
|
# Condition values, so CloudFormation infers NO creation edge from the
|
|
# references alone. The explicit DependsOn below is what guarantees the
|
|
# boundary exists before the role on first create (IAM would otherwise
|
|
# accept the role, leaving a window where the role exists unbounded-gated
|
|
# against a not-yet-existing boundary policy).
|
|
# ---------------------------------------------------------------------------
|
|
SamCfnExecutionRole:
|
|
Type: AWS::IAM::Role
|
|
DependsOn: LambdaExecutionBoundary
|
|
Properties:
|
|
RoleName: github-cfn-execution-role
|
|
ManagedPolicyArns:
|
|
- !Ref SamCfnIamManagementPolicy
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Service: cloudformation.amazonaws.com
|
|
Action: sts:AssumeRole
|
|
Policies:
|
|
|
|
# ── CloudFormation transforms (SAM macro) ─────────────────────────
|
|
- PolicyName: cloudformation-transforms
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: AllowSAMTransform
|
|
Effect: Allow
|
|
Action:
|
|
- cloudformation:CreateChangeSet
|
|
Resource:
|
|
- arn:aws:cloudformation:us-east-1:aws:transform/*
|
|
|
|
# ── Lambda management ─────────────────────────────────────────────
|
|
# Covers function create/update/delete, aliases, event source
|
|
# mappings, and Lambda layers — all needed for SAM deploys.
|
|
- PolicyName: lambda-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: LambdaFunctions
|
|
Effect: Allow
|
|
Action:
|
|
- lambda:AddPermission
|
|
- lambda:CreateFunction
|
|
- lambda:DeleteFunction
|
|
- lambda:GetFunction
|
|
- lambda:GetFunctionConfiguration
|
|
- lambda:ListFunctions
|
|
- lambda:RemovePermission
|
|
- lambda:UpdateFunctionCode
|
|
- lambda:UpdateFunctionConfiguration
|
|
- lambda:UpdateFunctionEventInvokeConfig
|
|
- lambda:PutFunctionEventInvokeConfig
|
|
- lambda:DeleteFunctionEventInvokeConfig
|
|
- lambda:GetFunctionEventInvokeConfig
|
|
- lambda:ListTags
|
|
- lambda:TagResource
|
|
- lambda:UntagResource
|
|
- lambda:GetPolicy
|
|
- lambda:ListVersionsByFunction
|
|
- lambda:PublishVersion
|
|
- lambda:CreateAlias
|
|
- lambda:DeleteAlias
|
|
- lambda:UpdateAlias
|
|
- lambda:GetAlias
|
|
Resource:
|
|
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:*"
|
|
- Sid: LambdaLayers
|
|
Effect: Allow
|
|
Action:
|
|
- lambda:PublishLayerVersion
|
|
- lambda:DeleteLayerVersion
|
|
- lambda:GetLayerVersion
|
|
- lambda:ListLayerVersions
|
|
- lambda:ListLayers
|
|
- lambda:AddLayerVersionPermission
|
|
- lambda:RemoveLayerVersionPermission
|
|
Resource:
|
|
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:layer:*"
|
|
- Sid: LambdaEventSourceMappings
|
|
Effect: Allow
|
|
Action:
|
|
- lambda:CreateEventSourceMapping
|
|
- lambda:DeleteEventSourceMapping
|
|
- lambda:GetEventSourceMapping
|
|
- lambda:ListEventSourceMappings
|
|
- lambda:UpdateEventSourceMapping
|
|
Resource: "*"
|
|
|
|
# ── API Gateway (HTTP APIs + REST APIs) ───────────────────────────
|
|
- PolicyName: apigateway-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: ApiGateway
|
|
Effect: Allow
|
|
Action:
|
|
- apigateway:GET
|
|
- apigateway:POST
|
|
- apigateway:PUT
|
|
- apigateway:PATCH
|
|
- apigateway:DELETE
|
|
Resource:
|
|
- "arn:aws:apigateway:us-east-1::*"
|
|
|
|
# ── DynamoDB ──────────────────────────────────────────────────────
|
|
- PolicyName: dynamodb-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: DynamoDBTables
|
|
Effect: Allow
|
|
Action:
|
|
- dynamodb:CreateTable
|
|
- dynamodb:DeleteTable
|
|
- dynamodb:DescribeTable
|
|
- dynamodb:UpdateTable
|
|
- dynamodb:ListTables
|
|
- dynamodb:TagResource
|
|
- dynamodb:UntagResource
|
|
- dynamodb:DescribeTimeToLive
|
|
- dynamodb:UpdateTimeToLive
|
|
- dynamodb:DescribeContinuousBackups
|
|
- dynamodb:UpdateContinuousBackups
|
|
Resource:
|
|
- !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*"
|
|
|
|
# ── S3 ────────────────────────────────────────────────────────────
|
|
# Covers bucket create/configure + object operations for SAM
|
|
# artifact buckets and application buckets.
|
|
- PolicyName: s3-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: S3BucketOps
|
|
Effect: Allow
|
|
Action:
|
|
- s3:CreateBucket
|
|
- s3:DeleteBucket
|
|
- s3:GetBucketLocation
|
|
- s3:GetBucketPolicy
|
|
- s3:PutBucketPolicy
|
|
- s3:DeleteBucketPolicy
|
|
- s3:GetBucketTagging
|
|
- s3:PutBucketTagging
|
|
- s3:GetBucketVersioning
|
|
- s3:PutBucketVersioning
|
|
- s3:GetLifecycleConfiguration
|
|
- s3:PutLifecycleConfiguration
|
|
- s3:GetBucketPublicAccessBlock
|
|
- s3:PutBucketPublicAccessBlock
|
|
# Explicit BucketEncryption blocks (first: payments-dashboard
|
|
# BoaRawBucket, 2026-07-22) need the encryption config pair.
|
|
- s3:GetEncryptionConfiguration
|
|
- s3:PutEncryptionConfiguration
|
|
- s3:GetBucketNotification
|
|
- s3:PutBucketNotification
|
|
- s3:GetBucketWebsite
|
|
- s3:PutBucketWebsite
|
|
- s3:DeleteBucketWebsite
|
|
- s3:GetBucketAcl
|
|
- s3:PutBucketAcl
|
|
Resource:
|
|
- "arn:aws:s3:::*"
|
|
- Sid: S3ObjectOps
|
|
Effect: Allow
|
|
Action:
|
|
- s3:GetObject
|
|
- s3:PutObject
|
|
- s3:DeleteObject
|
|
- s3:ListBucket
|
|
- s3:ListBucketVersions
|
|
- s3:GetObjectVersion
|
|
Resource:
|
|
- "arn:aws:s3:::*"
|
|
- "arn:aws:s3:::*/*"
|
|
|
|
# ── CloudWatch Logs ───────────────────────────────────────────────
|
|
- PolicyName: cloudwatch-logs-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: CWLogs
|
|
Effect: Allow
|
|
Action:
|
|
- logs:CreateLogGroup
|
|
- logs:DeleteLogGroup
|
|
- logs:DescribeLogGroups
|
|
- logs:PutRetentionPolicy
|
|
- logs:DeleteRetentionPolicy
|
|
- logs:ListTagsLogGroup
|
|
- logs:TagLogGroup
|
|
- logs:UntagLogGroup
|
|
- logs:ListTagsForResource
|
|
- logs:TagResource
|
|
- logs:UntagResource
|
|
- logs:CreateLogDelivery
|
|
- logs:GetLogDelivery
|
|
- logs:UpdateLogDelivery
|
|
- logs:DeleteLogDelivery
|
|
- logs:ListLogDeliveries
|
|
- logs:PutResourcePolicy
|
|
- logs:DescribeResourcePolicies
|
|
- logs:PutDestination
|
|
- logs:DeleteDestination
|
|
- logs:DescribeDestinations
|
|
- logs:AssociateKmsKey
|
|
- logs:DisassociateKmsKey
|
|
Resource: "*"
|
|
|
|
# ── EventBridge / CloudWatch Events (scheduled Lambdas) ───────────
|
|
- PolicyName: eventbridge-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: EventBridge
|
|
Effect: Allow
|
|
Action:
|
|
- events:DeleteRule
|
|
- events:DescribeRule
|
|
- events:EnableRule
|
|
- events:DisableRule
|
|
- events:ListRules
|
|
- events:ListTargetsByRule
|
|
- events:PutRule
|
|
- events:PutTargets
|
|
- events:RemoveTargets
|
|
- events:TagResource
|
|
- events:UntagResource
|
|
- events:ListTagsForResource
|
|
- events:PutPermission
|
|
- events:RemovePermission
|
|
Resource: "*"
|
|
|
|
# ── SES (afterhours weekly-post, meal-order email-report) ─────────
|
|
- PolicyName: ses-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: SESRules
|
|
Effect: Allow
|
|
Action:
|
|
- ses:CreateReceiptRule
|
|
- ses:DeleteReceiptRule
|
|
- ses:DescribeReceiptRule
|
|
- ses:UpdateReceiptRule
|
|
- ses:CreateReceiptRuleSet
|
|
- ses:DescribeActiveReceiptRuleSet
|
|
- ses:DescribeReceiptRuleSet
|
|
- ses:SetActiveReceiptRuleSet
|
|
- ses:ReorderReceiptRuleSet
|
|
- ses:GetIdentityVerificationAttributes
|
|
- ses:ListIdentities
|
|
Resource: "*"
|
|
|
|
# ── SQS (payments-dashboard queues + DLQs) ────────────────────────
|
|
- PolicyName: sqs-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: SQSQueues
|
|
Effect: Allow
|
|
Action:
|
|
- sqs:CreateQueue
|
|
- sqs:DeleteQueue
|
|
- sqs:GetQueueAttributes
|
|
- sqs:SetQueueAttributes
|
|
- sqs:GetQueueUrl
|
|
- sqs:ListQueues
|
|
- sqs:TagQueue
|
|
- sqs:UntagQueue
|
|
- sqs:ListQueueTags
|
|
- sqs:AddPermission
|
|
- sqs:RemovePermission
|
|
Resource:
|
|
- !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*"
|
|
|
|
# ── SNS (validation / alarm notifications) ────────────────────────
|
|
- PolicyName: sns-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: SNS
|
|
Effect: Allow
|
|
Action:
|
|
- sns:CreateTopic
|
|
- sns:DeleteTopic
|
|
- sns:GetTopicAttributes
|
|
- sns:SetTopicAttributes
|
|
- sns:Subscribe
|
|
- sns:Unsubscribe
|
|
- sns:ListSubscriptionsByTopic
|
|
- sns:ListTopics
|
|
- sns:TagResource
|
|
- sns:UntagResource
|
|
Resource:
|
|
- !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:*"
|
|
|
|
# ── CloudWatch Alarms ─────────────────────────────────────────────
|
|
- PolicyName: cloudwatch-alarms-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: CWAlarms
|
|
Effect: Allow
|
|
Action:
|
|
- cloudwatch:PutMetricAlarm
|
|
- cloudwatch:DeleteAlarms
|
|
- cloudwatch:DescribeAlarms
|
|
- cloudwatch:EnableAlarmActions
|
|
- cloudwatch:DisableAlarmActions
|
|
- cloudwatch:ListTagsForResource
|
|
- cloudwatch:TagResource
|
|
- cloudwatch:UntagResource
|
|
Resource: "*"
|
|
|
|
# ── EC2 / VPC / NAT / EIP / Security Groups ───────────────────────
|
|
# payments-dashboard deploys a VPC, NAT gateway, EIP, route tables,
|
|
# subnets, security groups, and gateway VPC endpoints.
|
|
- PolicyName: ec2-vpc-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: EC2VPC
|
|
Effect: Allow
|
|
Action:
|
|
- ec2:AllocateAddress
|
|
- ec2:AssociateRouteTable
|
|
- ec2:AttachInternetGateway
|
|
- ec2:AuthorizeSecurityGroupEgress
|
|
- ec2:AuthorizeSecurityGroupIngress
|
|
- ec2:CreateInternetGateway
|
|
- ec2:CreateNatGateway
|
|
- ec2:CreateRoute
|
|
- ec2:CreateRouteTable
|
|
- ec2:CreateSecurityGroup
|
|
- ec2:CreateSubnet
|
|
- ec2:CreateVpc
|
|
- ec2:CreateVpcEndpoint
|
|
- ec2:CreateTags
|
|
- ec2:DeleteInternetGateway
|
|
- ec2:DeleteNatGateway
|
|
- ec2:DeleteRoute
|
|
- ec2:DeleteRouteTable
|
|
- ec2:DeleteSecurityGroup
|
|
- ec2:DeleteSubnet
|
|
- ec2:DeleteVpc
|
|
- ec2:DeleteVpcEndpoints
|
|
- ec2:DescribeAddresses
|
|
- ec2:DescribeAvailabilityZones
|
|
- ec2:DescribeInternetGateways
|
|
- ec2:DescribeNatGateways
|
|
- ec2:DescribeRouteTables
|
|
- ec2:DescribeSecurityGroups
|
|
- ec2:DescribeSubnets
|
|
- ec2:DescribeVpcEndpoints
|
|
- ec2:DescribeVpcs
|
|
- ec2:DescribePrefixLists
|
|
- ec2:DetachInternetGateway
|
|
- ec2:DisassociateAddress
|
|
- ec2:DisassociateRouteTable
|
|
- ec2:ModifySubnetAttribute
|
|
- ec2:ModifyVpcAttribute
|
|
- ec2:ModifyVpcEndpoint
|
|
- ec2:ReleaseAddress
|
|
- ec2:RevokeSecurityGroupEgress
|
|
- ec2:RevokeSecurityGroupIngress
|
|
- ec2:UpdateSecurityGroupRuleDescriptionsEgress
|
|
- ec2:UpdateSecurityGroupRuleDescriptionsIngress
|
|
Resource: "*"
|
|
|
|
# ── CloudFront + OAC (meal-order-manager form distribution) ───────
|
|
- PolicyName: cloudfront-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: CloudFront
|
|
Effect: Allow
|
|
Action:
|
|
- cloudfront:CreateDistribution
|
|
- cloudfront:DeleteDistribution
|
|
- cloudfront:GetDistribution
|
|
- cloudfront:GetDistributionConfig
|
|
- cloudfront:UpdateDistribution
|
|
- cloudfront:TagResource
|
|
- cloudfront:UntagResource
|
|
- cloudfront:ListTagsForResource
|
|
- cloudfront:CreateOriginAccessControl
|
|
- cloudfront:DeleteOriginAccessControl
|
|
- cloudfront:GetOriginAccessControl
|
|
- cloudfront:GetOriginAccessControlConfig
|
|
- cloudfront:UpdateOriginAccessControl
|
|
- cloudfront:ListOriginAccessControls
|
|
- cloudfront:CreateInvalidation
|
|
- cloudfront:GetInvalidation
|
|
Resource: "*"
|
|
|
|
# ── SSM Parameter Store (meal-order-manager, afterhours) ──────────
|
|
# Write is needed because meal-order-manager creates
|
|
# /meal-order-manager/slack-channel-id via AWS::SSM::Parameter.
|
|
- PolicyName: ssm-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: SSMParameters
|
|
Effect: Allow
|
|
Action:
|
|
- ssm:GetParameter
|
|
- ssm:GetParameters
|
|
- ssm:GetParametersByPath
|
|
- ssm:PutParameter
|
|
- ssm:DeleteParameter
|
|
- ssm:DeleteParameters
|
|
- ssm:DescribeParameters
|
|
- ssm:AddTagsToResource
|
|
- ssm:RemoveTagsFromResource
|
|
- ssm:ListTagsForResource
|
|
Resource:
|
|
- !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*"
|
|
# WAF association needs SSM parameter read at deploy time
|
|
# (/seahaven/waf/app-web-acl-arn value lookup)
|
|
- Sid: SSMParameterDescribe
|
|
Effect: Allow
|
|
Action:
|
|
- ssm:DescribeParameters
|
|
Resource: "*"
|
|
|
|
# ── WAF (meal-order-manager CloudFront WebACL association) ────────
|
|
- PolicyName: waf-management
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Sid: WAF
|
|
Effect: Allow
|
|
Action:
|
|
- wafv2:GetWebACL
|
|
- wafv2:GetWebACLForResource
|
|
- wafv2:ListWebACLs
|
|
- wafv2:AssociateWebACL
|
|
- wafv2:DisassociateWebACL
|
|
- wafv2:ListResourcesForWebACL
|
|
Resource: "*"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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"
|
|
|
|
# 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"
|
|
|
|
# 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"
|
|
|
|
# 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"
|
|
|