.github/oidc-deploy-roles.yaml

1468 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 is SET-only. Granting delete would let this
# role create a boundary-gated role, strip the boundary, then pass an
# unconstrained role to Lambda. SAM creation and teardown need only
# PutRolePermissionsBoundary and DeleteRole.
#
# Removing a boundary therefore fails by design. A failed rollback
# reaches UPDATE_ROLLBACK_FAILED and needs admin recovery by skipping
# or replacing the role. A successful rollback reaches the stable
# UPDATE_ROLLBACK_COMPLETE state and can accept a corrective update.
# Removing a SAM function boundary is a security regression, so
# failing loudly is intentional.
- 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"
# API Gateway assumes SAM authorizer invocation roles. Keep this
# separate from Lambda PassRole so each target service and role
# pattern remains independently constrained.
- Sid: IAMPassAuthorizerRole
Effect: Allow
Action:
- iam:PassRole
Resource:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/cfn-managed/*AuthorizerInvokeRole-*"
Condition:
StringEquals:
"iam:PassedToService": "apigateway.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-*
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 Terraform-written deploy
# parameters and app config, invokes the IAM-authenticated publication API,
# writes the published form, and invalidates the form's CloudFront path. It
# deploys nothing, so it gets no CloudFormation write actions, no PassRole,
# and no DynamoDB access.
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:
- 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
# Deploy targets are written by Terraform (meal-order-manager
# terraform/ssm.tf). App config params remain named grants only.
Resource:
- !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/deploy/api-url
- !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/deploy/form-bucket
- !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/deploy/distribution-id
- !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/deploy/form-url
- !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
# Publication routes on the meal-order-manager HttpApi (API id
# b5mli7qgp3 is stable for the life of the stack). Menu/settings
# writes go through these IAM-authenticated routes, not DynamoDB.
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub arn:aws:execute-api:us-east-1:${AWS::AccountId}:b5mli7qgp3/*/GET/api/publish/settings
- !Sub arn:aws:execute-api:us-east-1:${AWS::AccountId}:b5mli7qgp3/*/POST/api/publish/menu
- 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 removed 2026-08-07 (PLAT-88): HCP sole
# mutate path; prod githubdeploy role deleted; mgmt twin already gone.
ApmWoAnalysisDeployRoleArn:
Value: !GetAtt ApmWoAnalysisDeployRole.Arn
SeahavenAccountBaselineDeployRoleArn:
Value: !GetAtt SeahavenAccountBaselineDeployRole.Arn
MealOrderManagerWeeklyMenuRoleArn:
Value: !GetAtt MealOrderManagerWeeklyMenuRole.Arn