mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 05:53:12 +00:00
The shared CloudFormation execution role could remove the permissions boundary from the very roles that boundary was gating. Its iam-role-management-boundary-gated policy allows iam:DeleteRolePermissionsBoundary on role/* under a StringEquals condition on iam:PermissionsBoundary -- and for a delete that condition key reflects the boundary CURRENTLY attached to the target role, so it matches exactly the roles the gate protects. Create a boundary-gated role with an inline *:* policy, strip its boundary, PassRole it to Lambda, and the result is unbounded admin in the management account. Confirmed live with simulate-principal-policy, not inferred. Phase A adds an attached managed policy, seahaven-cfn-exec-iam-management, carrying the corrected statement set: the boundary-gated Allows without iam:DeleteRolePermissionsBoundary, plus three Deny backstops -- DenyBoundaryTampering (boundary removal), DenyBoundaryPolicyEdit (rewriting a seahaven-* policy document) and DenySelfMutation. DenySelfMutation exists because the first draft of this fix was not durable: the role holds iam:DetachRolePolicy, iam:DeleteRolePolicy and iam:DeleteRole on Resource "*" with no condition, so it could detach the Deny-carrying policy from itself in one call and reinstate the escalation. It now cannot modify its own role, any githubdeploy-* role, or any seahaven-* policy. Nothing legitimate needs that: the deploy substrate's own principals are owned by this stack, which is deployed manually with administrator credentials rather than through this role. The change is additive. The old inline policy stays in place, so CloudFormation removes nothing and there is no window in which the role lacks its IAM permissions -- an explicit Deny beats an Allow anywhere in the policy set, so the corrected version governs from the moment this lands. Phase B removes the redundant inline copy. The split is also required by size: inline sits at 10,006 of IAM's 10,240-byte per-role limit, and the Deny statements do not fit there. Also reconciles drift. The deployed role carries three logs:*MetricFilter actions added out-of-band on 2026-06-29 and never back-ported. afterhours-shift-manager creates an AWS::Logs::MetricFilter through this role, so they are load-bearing; the template now matches the live policy exactly, which keeps inline at 10,006 and stops a future write-back from silently stripping them.
1505 lines
64 KiB
YAML
1505 lines
64 KiB
YAML
AWSTemplateFormatVersion: "2010-09-09"
|
|
Description: >-
|
|
GitHub Actions OIDC deploy roles for Sea Haven Industries repos.
|
|
Each repo gets a scoped IAM role that GitHub Actions assumes via OIDC.
|
|
|
|
Parameters:
|
|
GitHubOrg:
|
|
Type: String
|
|
Default: Sea-Haven-Industries
|
|
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 — already exists for seahaven-site)
|
|
# ---------------------------------------------------------------------------
|
|
GitHubOIDCProvider:
|
|
Type: AWS::IAM::OIDCProvider
|
|
Condition: ShouldCreateOIDCProvider
|
|
Properties:
|
|
Url: https://token.actions.githubusercontent.com
|
|
ClientIdList:
|
|
- sts.amazonaws.com
|
|
ThumbprintList:
|
|
- 6938fd4d98bab03faadb97b34396831e3780aea1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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/*"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# IAM role lifecycle for the CFN execution role — BOUNDARY-GATED
|
|
# (attached managed policy)
|
|
#
|
|
# Ported verbatim from seahaven-org-baseline
|
|
# lib/deploy-substrate/deploy-substrate.template.yaml (stack
|
|
# seahaven-deploy-substrate, deployed and verified in seahaven-prod and
|
|
# seahaven-dev 2026-07-27). Keep the two copies in lockstep.
|
|
#
|
|
# Why a MANAGED policy and not inline on the role: this role's inline
|
|
# policies total ~10,006 bytes against IAM's hard 10,240-byte per-role
|
|
# inline limit — about 234 bytes of headroom. The Deny statements below
|
|
# do not fit inline (the equivalent attempt in prod/dev failed with
|
|
# ServiceLimitExceeded). Attached managed policies carry their own
|
|
# separate 6,144-byte budget.
|
|
#
|
|
# SECURITY: iam:DeleteRolePermissionsBoundary is deliberately ABSENT from
|
|
# the Allow below, and explicitly Denied further down. Granting it under
|
|
# the StringEquals iam:PermissionsBoundary condition is self-defeating:
|
|
# for a delete, that condition key reflects the boundary CURRENTLY
|
|
# attached to the target role, so it matches exactly the roles the gate
|
|
# protects — letting this role create a boundary-gated role with an
|
|
# inline *:* policy, strip the boundary, and pass the now-unbounded role
|
|
# 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.
|
|
# ---------------------------------------------------------------------------
|
|
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).
|
|
#
|
|
# OPERATIONAL CONSEQUENCE — read before debugging a stuck stack.
|
|
# SAM does not need the delete for the common paths: it SETS the
|
|
# boundary on roles it creates, and stack teardown calls DeleteRole.
|
|
# But there IS one path that now fails by design: updating an
|
|
# existing AWS::IAM::Role to REMOVE its PermissionsBoundary property
|
|
# makes CloudFormation call DeleteRolePermissionsBoundary, which is
|
|
# denied. The stack update fails and rolls back, and because cd-sam's
|
|
# pre-flight hard-fails on *ROLLBACK_COMPLETE, that repo's deploys
|
|
# stay blocked until it is cleared. Recovery is an out-of-band admin
|
|
# action (remove the boundary directly, or replace the role by
|
|
# renaming its logical id) — not a pipeline retry. Removing the
|
|
# boundary from a SAM function is a security regression anyway, so
|
|
# failing loudly here is the intent.
|
|
- 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/*"
|
|
|
|
# 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:
|
|
- iam:CreatePolicyVersion
|
|
- iam:SetDefaultPolicyVersion
|
|
- iam:DeletePolicyVersion
|
|
- iam:DeletePolicy
|
|
Resource:
|
|
- !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: the deploy substrate's own roles are
|
|
# managed by THIS stack, which is deployed manually with
|
|
# administrator credentials (no --role-arn), 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
|
|
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"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 by literal value, so the
|
|
# seahaven-lambda-execution-boundary managed policy must exist before this
|
|
# stack is deployed. It is created by this same stack above, and is not
|
|
# modified by the Phase A change.
|
|
# ---------------------------------------------------------------------------
|
|
SamCfnExecutionRole:
|
|
Type: AWS::IAM::Role
|
|
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
|
|
# Reconciles drift: these three exist on the DEPLOYED role
|
|
# (added out-of-band 2026-06-29) but were never back-ported
|
|
# here. afterhours-shift-manager creates an
|
|
# AWS::Logs::MetricFilter through this role, so omitting them
|
|
# risks a future write-back silently stripping them.
|
|
- logs:PutMetricFilter
|
|
- logs:DeleteMetricFilter
|
|
- logs:DescribeMetricFilters
|
|
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 ──────────────────────────
|
|
# 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)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
AfterhoursShiftManagerDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-afterhours-shift-manager
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/afterhours-shift-manager:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: sam-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DeleteChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:DescribeStackEvents
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:GetTemplate
|
|
- cloudformation:ListStackResources
|
|
- cloudformation:UpdateStack
|
|
- cloudformation:CreateStack
|
|
- cloudformation:TagResource
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/afterhours-shift-manager/*
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:GetTemplateSummary
|
|
Resource: "*"
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:CreateStack
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*
|
|
- Effect: Allow
|
|
Action:
|
|
- s3:PutObject
|
|
- s3:GetObject
|
|
- s3:ListBucket
|
|
- s3:GetBucketLocation
|
|
- s3:CreateBucket
|
|
- s3:PutBucketPolicy
|
|
- s3:GetBucketPolicy
|
|
- s3:PutLifecycleConfiguration
|
|
- s3:PutBucketVersioning
|
|
- s3:DeleteObject
|
|
Resource:
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*
|
|
- Effect: Allow
|
|
Action:
|
|
- iam:PassRole
|
|
Resource:
|
|
- !GetAtt SamCfnExecutionRole.Arn
|
|
|
|
FrontIntegrationsDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-front-integrations
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/front-integrations:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: sam-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DeleteChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:DescribeStackEvents
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:GetTemplate
|
|
- cloudformation:ListStackResources
|
|
- cloudformation:UpdateStack
|
|
- cloudformation:CreateStack
|
|
- cloudformation:TagResource
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/front-integrations/*
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:GetTemplateSummary
|
|
Resource: "*"
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:CreateStack
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*
|
|
- Effect: Allow
|
|
Action:
|
|
- s3:PutObject
|
|
- s3:GetObject
|
|
- s3:ListBucket
|
|
- s3:GetBucketLocation
|
|
- s3:CreateBucket
|
|
- s3:PutBucketPolicy
|
|
- s3:GetBucketPolicy
|
|
- s3:PutLifecycleConfiguration
|
|
- s3:PutBucketVersioning
|
|
- s3:DeleteObject
|
|
Resource:
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*
|
|
- Effect: Allow
|
|
Action:
|
|
- iam:PassRole
|
|
Resource:
|
|
- !GetAtt SamCfnExecutionRole.Arn
|
|
|
|
AfiBackupMonitorDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-afi-backup-monitor
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/afi-backup-monitor:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: sam-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DeleteChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:DescribeStackEvents
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:GetTemplate
|
|
- cloudformation:ListStackResources
|
|
- cloudformation:UpdateStack
|
|
- cloudformation:CreateStack
|
|
- cloudformation:TagResource
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/afi-backup-monitor/*
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:GetTemplateSummary
|
|
Resource: "*"
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:CreateStack
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*
|
|
- Effect: Allow
|
|
Action:
|
|
- s3:PutObject
|
|
- s3:GetObject
|
|
- s3:ListBucket
|
|
- s3:GetBucketLocation
|
|
- s3:CreateBucket
|
|
- s3:PutBucketPolicy
|
|
- s3:GetBucketPolicy
|
|
- s3:PutLifecycleConfiguration
|
|
- s3:PutBucketVersioning
|
|
- s3:DeleteObject
|
|
Resource:
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*
|
|
- Effect: Allow
|
|
Action:
|
|
- iam:PassRole
|
|
Resource:
|
|
- !GetAtt SamCfnExecutionRole.Arn
|
|
|
|
PaymentsDashboardDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-payments-dashboard
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/payments-dashboard:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: sam-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DeleteChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:DescribeStackEvents
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:GetTemplate
|
|
- cloudformation:ListStackResources
|
|
- cloudformation:UpdateStack
|
|
- cloudformation:CreateStack
|
|
- cloudformation:TagResource
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/payments-dashboard/*
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:GetTemplateSummary
|
|
Resource: "*"
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:DescribeStacks
|
|
- cloudformation:CreateChangeSet
|
|
- cloudformation:DescribeChangeSet
|
|
- cloudformation:ExecuteChangeSet
|
|
- cloudformation:CreateStack
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*
|
|
- Effect: Allow
|
|
Action:
|
|
- s3:PutObject
|
|
- s3:GetObject
|
|
- s3:ListBucket
|
|
- s3:GetBucketLocation
|
|
- s3:CreateBucket
|
|
- s3:PutBucketPolicy
|
|
- s3:GetBucketPolicy
|
|
- s3:PutLifecycleConfiguration
|
|
- s3:PutBucketVersioning
|
|
- s3:DeleteObject
|
|
Resource:
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*
|
|
- arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*
|
|
- Effect: Allow
|
|
Action:
|
|
- iam:PassRole
|
|
Resource:
|
|
- !GetAtt SamCfnExecutionRole.Arn
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CDK deploy roles (4 repos)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ExecAideDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-exec-aide
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/exec-aide:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: cdk-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- sts:AssumeRole
|
|
Resource:
|
|
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
|
|
SeahavenDoorUnlockApiDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-seahaven-door-unlock-api
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/seahaven-door-unlock-api:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: cdk-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- sts:AssumeRole
|
|
Resource:
|
|
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
|
|
ProcurementIngestDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-procurement-ingest
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/procurement-ingest:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: cdk-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- sts:AssumeRole
|
|
Resource:
|
|
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
|
|
ApmWoAnalysisDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-apm-wo-analysis
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/apm-wo-analysis:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: cdk-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- sts:AssumeRole
|
|
Resource:
|
|
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
|
|
SeahavenAccountBaselineDeployRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: githubdeploy-seahaven-account-baseline
|
|
AssumeRolePolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Principal:
|
|
Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
|
|
Action: sts:AssumeRoleWithWebIdentity
|
|
Condition:
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
StringLike:
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/seahaven-org-baseline:ref:refs/heads/main
|
|
Policies:
|
|
- PolicyName: cdk-deploy
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- sts:AssumeRole
|
|
Resource:
|
|
- !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-hnb659fds-*
|
|
|
|
Outputs:
|
|
LambdaExecutionBoundaryArn:
|
|
Value: !Ref LambdaExecutionBoundary
|
|
Description: >-
|
|
ARN of the Lambda execution permissions boundary. Set this as
|
|
PermissionsBoundary on Globals.Function in all five SAM stacks.
|
|
Export:
|
|
Name: seahaven-lambda-execution-boundary-arn
|
|
SamCfnExecutionRoleArn:
|
|
Value: !GetAtt SamCfnExecutionRole.Arn
|
|
Export:
|
|
Name: github-cfn-execution-role-arn
|
|
AfterhoursShiftManagerDeployRoleArn:
|
|
Value: !GetAtt AfterhoursShiftManagerDeployRole.Arn
|
|
FrontIntegrationsDeployRoleArn:
|
|
Value: !GetAtt FrontIntegrationsDeployRole.Arn
|
|
AfiBackupMonitorDeployRoleArn:
|
|
Value: !GetAtt AfiBackupMonitorDeployRole.Arn
|
|
PaymentsDashboardDeployRoleArn:
|
|
Value: !GetAtt PaymentsDashboardDeployRole.Arn
|
|
# SeahavenSlackBotDeployRoleArn removed 2026-07-27: the role was deleted
|
|
# out-of-band on 2026-07-23, so this !GetAtt failed as a live IAM read and
|
|
# broke every stack update. Nothing imported it (the Output had no
|
|
# ExportName, and no stack imports any export from this stack).
|
|
ExecAideDeployRoleArn:
|
|
Value: !GetAtt ExecAideDeployRole.Arn
|
|
SeahavenDoorUnlockApiDeployRoleArn:
|
|
Value: !GetAtt SeahavenDoorUnlockApiDeployRole.Arn
|
|
ProcurementIngestDeployRoleArn:
|
|
Value: !GetAtt ProcurementIngestDeployRole.Arn
|
|
ApmWoAnalysisDeployRoleArn:
|
|
Value: !GetAtt ApmWoAnalysisDeployRole.Arn
|
|
SeahavenAccountBaselineDeployRoleArn:
|
|
Value: !GetAtt SeahavenAccountBaselineDeployRole.Arn
|