mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-09-30 05:53:12 +00:00
1490 lines
64 KiB
YAML
1490 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
|
|
# No glob metacharacters: this value is interpolated into StringLike trust
|
|
# conditions, where a '*' override would silently open every role's trust
|
|
# to any GitHub org with a same-named repo.
|
|
AllowedPattern: "^[A-Za-z0-9-]+$"
|
|
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).
|
|
#
|
|
# THIS IS THE ONLY COPY. It was introduced alongside an inline
|
|
# iam-role-management-boundary-gated policy that carried the older,
|
|
# vulnerable version of these statements; that inline copy was removed once
|
|
# this one was deployed and verified. Consolidating here also freed the
|
|
# role's inline budget from 10,006 to 8,261 of the 10,240-byte limit —
|
|
# prefer adding future statements here rather than inline.
|
|
#
|
|
# OPERATIONAL NOTES
|
|
# - Detaching or deleting this policy does not just drop the Deny backstops,
|
|
# it drops every IAM permission the role has, so SAM deploys stop working
|
|
# immediately and loudly rather than silently becoming less safe. The role
|
|
# cannot do it to itself (DenySelfMutation below), but an administrator
|
|
# can — treat detach/delete as a break-glass action, not a cleanup step.
|
|
# - A managed policy keeps at most 5 versions. CloudFormation creates a new
|
|
# version on every change to this document, so if an update ever fails with
|
|
# LimitExceeded, prune old versions (list-policy-versions /
|
|
# delete-policy-version) rather than assuming the template is wrong.
|
|
# ---------------------------------------------------------------------------
|
|
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: "*"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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-*
|
|
|
|
# Scoped runtime role for the meal-order-manager weekly-menu workflow
|
|
# (Monday scrape + order-form publish). Deliberately narrower than the
|
|
# repo's deploy role: the scheduled job reads stack outputs and app config,
|
|
# writes menu items and the published form, and invalidates the form's
|
|
# CloudFront path. It deploys nothing, so it gets no CloudFormation write
|
|
# actions, no PassRole, and no access outside the form bucket.
|
|
MealOrderManagerWeeklyMenuRole:
|
|
Type: AWS::IAM::Role
|
|
Properties:
|
|
RoleName: github-meal-order-manager-weekly-menu
|
|
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 (not the sibling roles' StringLike): no wildcard is
|
|
# intended, and job_workflow_ref pins this runtime role to the ONE
|
|
# workflow it serves — unlike the deploy roles, any main-branch
|
|
# workflow must NOT be able to mint these credentials.
|
|
StringEquals:
|
|
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/meal-order-manager:ref:refs/heads/main
|
|
token.actions.githubusercontent.com:job_workflow_ref: !Sub ${GitHubOrg}/meal-order-manager/.github/workflows/weekly-menu.yml@refs/heads/main
|
|
Policies:
|
|
- PolicyName: weekly-menu-publish
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudformation:DescribeStacks
|
|
Resource:
|
|
- !Sub arn:aws:cloudformation:us-east-1:${AWS::AccountId}:stack/meal-order-manager/*
|
|
- Effect: Allow
|
|
Action:
|
|
- secretsmanager:GetSecretValue
|
|
# Secrets Manager appends a random 6-char suffix to every secret
|
|
# ARN, so a name-based match needs a glob — but exactly six '?'
|
|
# (one char each), NOT '-*', which would also match any future
|
|
# secret extending the name (e.g. form-api-key-backup).
|
|
Resource:
|
|
- !Sub arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:meal-order-manager/form-api-key-??????
|
|
- !Sub arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:meal-order-manager/slack-bot-token-??????
|
|
- Effect: Allow
|
|
Action:
|
|
- ssm:GetParameter
|
|
Resource:
|
|
- !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/google-client-id
|
|
- !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/slack-channel-id
|
|
- Effect: Allow
|
|
Action:
|
|
- dynamodb:GetItem
|
|
- dynamodb:PutItem
|
|
Resource:
|
|
- !Sub arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders
|
|
- Effect: Allow
|
|
Action:
|
|
- s3:PutObject
|
|
Resource:
|
|
- !Sub arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/index.html
|
|
- !Sub arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/archive/*.html
|
|
- Effect: Allow
|
|
Action:
|
|
- cloudfront:CreateInvalidation
|
|
# Distribution ID = the meal-order-manager stack's DistributionId
|
|
# output (stable for the life of the distribution).
|
|
Resource:
|
|
- !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/E314J1CJJ9ZTRA
|
|
|
|
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
|
|
MealOrderManagerWeeklyMenuRoleArn:
|
|
Value: !GetAtt MealOrderManagerWeeklyMenuRole.Arn
|