mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
feat(deploy-substrate): per-account GitHub Actions deploy substrate for prod/dev
SAM repos migrating off the frozen management account need the shared deploy plumbing (permissions boundary + github-cfn-execution-role) in their target account; none of it existed outside mgmt, so there was no OIDC SAM deploy path into seahaven-prod or seahaven-dev at all. Adds a templated, per-account substrate stack so onboarding a future account is one bin/app.ts instance plus one CD job, not a hand-rolled copy. Per-repo githubdeploy-* roles stay out by design: they are provisioned per repo at migration time so an account never accumulates trust for repos that do not deploy to it. The template is a verbatim extraction of the reviewed mgmt substrate, with deliberate, documented divergences — notably the removal of iam:DeleteRolePermissionsBoundary plus explicit Deny backstops, which closes a confirmed privilege-escalation path (see PR body).
This commit is contained in:
parent
dd552bff4d
commit
d6bea33436
5 changed files with 1029 additions and 3 deletions
4
.github/workflows/deploy.yaml
vendored
4
.github/workflows/deploy.yaml
vendored
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main
|
||||
with:
|
||||
node-version: "24"
|
||||
stacks: "dev-baseline"
|
||||
stacks: "dev-baseline deploy-substrate-dev"
|
||||
stack-name: "seahaven-dev-baseline"
|
||||
secrets:
|
||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_DEV }}
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main
|
||||
with:
|
||||
node-version: "24"
|
||||
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod"
|
||||
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod"
|
||||
stack-name: "seahaven-prod-baseline"
|
||||
secrets:
|
||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }}
|
||||
|
|
|
|||
45
README.md
45
README.md
|
|
@ -50,7 +50,7 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
|
|||
| `tsconfig.json` | TypeScript compiler options (`outDir: cdk.out`) |
|
||||
| `package.json` | Pinned `aws-cdk-lib`, CDK CLI, and the `build` / `synth` / `diff` / `deploy` npm scripts |
|
||||
|
||||
`bin/app.ts` synthesizes eleven stacks across three regions and five accounts:
|
||||
`bin/app.ts` synthesizes fifteen stacks across three regions and five accounts:
|
||||
|
||||
| Construct id | Stack name | Account | Region | Source |
|
||||
|---|---|---|---|---|
|
||||
|
|
@ -65,6 +65,10 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
|
|||
| `security-baseline` | `seahaven-security-baseline` | 001520130573 | us-east-1 | `lib/member-baseline-stack.ts` |
|
||||
| `dev-baseline` | `seahaven-dev-baseline` | 710827005802 | us-east-1 | `lib/member-baseline-stack.ts` (orgManagedDetection) |
|
||||
| `prod-baseline` | `seahaven-prod-baseline` | 011934824531 | us-east-1 | `lib/member-baseline-stack.ts` (orgManagedDetection) |
|
||||
| `deploy-substrate-prod` | `seahaven-deploy-substrate` | 011934824531 | us-east-1 | `lib/deploy-substrate-stack.ts` |
|
||||
| `deploy-substrate-dev` | `seahaven-deploy-substrate` | 710827005802 | us-east-1 | `lib/deploy-substrate-stack.ts` |
|
||||
| `dynamodb-cmk-prod` | `seahaven-dynamodb-cmk` | 011934824531 | us-east-1 | `lib/dynamodb-cmk-stack.ts` |
|
||||
| `alarm-topic-prod` | `seahaven-alarm-topic` | 011934824531 | us-east-1 | `lib/alarm-topic-stack.ts` |
|
||||
|
||||
Member-account stacks deploy with per-account credentials — the CD workflow
|
||||
runs one job per account, each assuming that account's OIDC deploy role. Local
|
||||
|
|
@ -114,6 +118,45 @@ The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This pr
|
|||
|
||||
## What it deploys
|
||||
|
||||
### GitHub Actions deploy substrate (per account)
|
||||
|
||||
`lib/deploy-substrate-stack.ts` + `lib/deploy-substrate/deploy-substrate.template.yaml`
|
||||
deploy `seahaven-deploy-substrate` into each member account that hosts SAM
|
||||
workloads (currently seahaven-prod and seahaven-dev). It contains the shared
|
||||
account-level deploy plumbing:
|
||||
|
||||
- the `seahaven-lambda-execution-boundary` permissions boundary (ceiling for
|
||||
every SAM-generated Lambda execution role),
|
||||
- the `github-cfn-execution-role` CloudFormation execution role that `cd-sam`
|
||||
callers pass as `cfn-role-arn`,
|
||||
- optionally the GitHub OIDC identity provider (`createOidcProvider: true`,
|
||||
only for an account that does not already have one — one provider per URL
|
||||
per account).
|
||||
|
||||
The template is a verbatim extraction of the substrate section of
|
||||
`Sea-Haven-Industries/.github/oidc-deploy-roles.yaml` (see the provenance
|
||||
header in the template — mgmt's copy remains source of truth for 328440206208
|
||||
until its stacks migrate out; substrate changes while both are live must edit
|
||||
both files). Per-repo `githubdeploy-*` deploy roles are deliberately NOT part
|
||||
of the substrate — they are provisioned per repo at migration/onboarding time
|
||||
so an account never carries trust relationships for repos that do not deploy
|
||||
to it.
|
||||
|
||||
**Onboarding a future account as a deploy target:**
|
||||
|
||||
1. CDK-bootstrap the account (`npx cdk bootstrap aws://<account>/us-east-1`
|
||||
via `OrganizationAccountAccessRole`).
|
||||
2. Create `githubdeploy-seahaven-org-baseline` in the account (same trust and
|
||||
policy as the dev/prod copies) and add the repo secret
|
||||
`AWS_DEPLOY_ROLE_ARN_<ACCT>`.
|
||||
3. Add a `DeploySubstrateStack` instance in `bin/app.ts`
|
||||
(`createOidcProvider: true` if the account has no GitHub OIDC provider)
|
||||
and append its construct id to a new per-account job in
|
||||
`.github/workflows/deploy.yaml` (explicit `stacks` selector, one job per
|
||||
account).
|
||||
4. Merge; the substrate deploys via CD. Per-repo deploy roles and app stacks
|
||||
follow the cross-account migration playbook from there.
|
||||
|
||||
### CloudTrail (audit finding C-1)
|
||||
|
||||
| Resource | Logical ID | Notes |
|
||||
|
|
|
|||
27
bin/app.ts
27
bin/app.ts
|
|
@ -6,6 +6,7 @@ import { AlarmTopicStack } from "../lib/alarm-topic-stack";
|
|||
import { BackupOffsiteStack } from "../lib/backup-offsite-stack";
|
||||
import { BackupStack } from "../lib/backup-stack";
|
||||
import { RegionalBaselineStack } from "../lib/regional-baseline-stack";
|
||||
import { DeploySubstrateStack } from "../lib/deploy-substrate-stack";
|
||||
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
|
||||
import { MemberBaselineStack } from "../lib/member-baseline-stack";
|
||||
import { OrgGovernanceStack } from "../lib/org-governance-stack";
|
||||
|
|
@ -156,6 +157,32 @@ new MemberBaselineStack(app, "prod-baseline", {
|
|||
orgManagedDetection: true,
|
||||
});
|
||||
|
||||
// ── Per-account GitHub Actions deploy substrate ──────────────────────────────
|
||||
// The shared account-level deploy plumbing for SAM pipelines: permissions
|
||||
// boundary + github-cfn-execution-role (+ optional OIDC provider). mgmt's
|
||||
// copy lives in Sea-Haven-Industries/.github/oidc-deploy-roles.yaml and stays
|
||||
// there until its stacks finish migrating out; these stacks are what let SAM
|
||||
// repos (payments-dashboard, front-integrations, sh-openswe-traces, ...)
|
||||
// target prod/dev at all. Per-repo githubdeploy-* roles are provisioned at
|
||||
// each repo's migration time, never here. createOidcProvider stays false for
|
||||
// both accounts (provider verified present in each, 2026-07-27); a FUTURE
|
||||
// member account without one sets it true on its own instance. First-create
|
||||
// precondition verified 2026-07-27: github-cfn-execution-role and the
|
||||
// seahaven-lambda-execution-boundary policy both returned NoSuchEntity in
|
||||
// 011934824531 AND 710827005802, so the named creates cannot collide with
|
||||
// out-of-band copies.
|
||||
new DeploySubstrateStack(app, "deploy-substrate-prod", {
|
||||
stackName: "seahaven-deploy-substrate",
|
||||
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
||||
createOidcProvider: false,
|
||||
});
|
||||
|
||||
new DeploySubstrateStack(app, "deploy-substrate-dev", {
|
||||
stackName: "seahaven-deploy-substrate",
|
||||
env: { account: DEV_ACCOUNT, region: "us-east-1" },
|
||||
createOidcProvider: false,
|
||||
});
|
||||
|
||||
// ── Shared DynamoDB CMK (INFRA-95 / M-3) ─────────────────────────────────────
|
||||
// Dedicated, standalone stack so the customer-managed key for sensitive
|
||||
// finance/PII DynamoDB tables is an independent shared dependency for the owning
|
||||
|
|
|
|||
64
lib/deploy-substrate-stack.ts
Normal file
64
lib/deploy-substrate-stack.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import * as cfninc from "aws-cdk-lib/cloudformation-include";
|
||||
import * as path from "path";
|
||||
import { Construct } from "constructs";
|
||||
|
||||
export interface DeploySubstrateStackProps extends cdk.StackProps {
|
||||
/**
|
||||
* Create the GitHub OIDC identity provider in this account. Leave false
|
||||
* when the provider already exists (seahaven-prod and seahaven-dev both
|
||||
* have it from their githubdeploy-* role provisioning) - an account can
|
||||
* only hold ONE provider per URL, so creating a duplicate fails the deploy.
|
||||
* Set true only for a brand-new account with no OIDC provider yet.
|
||||
*/
|
||||
createOidcProvider?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-account GitHub Actions deploy substrate: the shared account-level
|
||||
* resources every SAM deploy pipeline needs -
|
||||
* - GitHub OIDC identity provider (conditional, see props),
|
||||
* - `seahaven-lambda-execution-boundary` permissions boundary (the ceiling
|
||||
* applied to every SAM-generated Lambda execution role),
|
||||
* - `github-cfn-execution-role` (the shared CloudFormation execution role
|
||||
* that cd-sam callers pass as cfn-role-arn).
|
||||
*
|
||||
* Deliberately NOT here: per-repo githubdeploy-* roles. Those are provisioned
|
||||
* per repo at migration/onboarding time (deploy-role-first playbook) so an
|
||||
* account never accumulates trust relationships for repos that do not deploy
|
||||
* to it.
|
||||
*
|
||||
* The resources come verbatim from the management account's reviewed
|
||||
* oidc-deploy-roles.yaml substrate section via cloudformation-include, so the
|
||||
* policy JSON that passed cross-review and security review deploys unchanged.
|
||||
* See lib/deploy-substrate/deploy-substrate.template.yaml for the provenance
|
||||
* and drift warning (mgmt's copy stays source of truth for 328440206208 until
|
||||
* its stacks finish migrating out).
|
||||
*
|
||||
* Deploy-order note: the boundary and the execution role live in the SAME
|
||||
* stack, and the role carries an explicit DependsOn on the boundary (the
|
||||
* role only names the boundary ARN inside Condition strings, so CFN would
|
||||
* otherwise infer no creation edge). App stacks (payments-dashboard,
|
||||
* front-integrations, sh-openswe-traces, ...) can only target this account
|
||||
* AFTER this stack is deployed there.
|
||||
*/
|
||||
export class DeploySubstrateStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: DeploySubstrateStackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
new cfninc.CfnInclude(this, "Substrate", {
|
||||
templateFile: path.join(
|
||||
__dirname,
|
||||
"deploy-substrate",
|
||||
"deploy-substrate.template.yaml",
|
||||
),
|
||||
parameters: {
|
||||
CreateOIDCProvider: props?.createOidcProvider ? "true" : "false",
|
||||
},
|
||||
});
|
||||
|
||||
cdk.Tags.of(this).add("Project", "account-baseline");
|
||||
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
|
||||
cdk.Tags.of(this).add("ManagedBy", "cdk");
|
||||
}
|
||||
}
|
||||
892
lib/deploy-substrate/deploy-substrate.template.yaml
Normal file
892
lib/deploy-substrate/deploy-substrate.template.yaml
Normal file
|
|
@ -0,0 +1,892 @@
|
|||
AWSTemplateFormatVersion: "2010-09-09"
|
||||
Description: >-
|
||||
Per-account GitHub Actions deploy substrate for Sea Haven Industries:
|
||||
the shared account-level resources every SAM deploy pipeline needs
|
||||
(GitHub OIDC provider, Lambda execution permissions boundary, and the
|
||||
shared CloudFormation execution role). Per-repo githubdeploy-* roles
|
||||
are NOT here — they are provisioned per repo at migration/onboarding
|
||||
time in the target account.
|
||||
|
||||
# PROVENANCE / DRIFT WARNING
|
||||
# The Resources below are a VERBATIM extraction of the substrate section
|
||||
# (OIDC provider + LambdaExecutionBoundary + SamCfnExecutionRole) of
|
||||
# Sea-Haven-Industries/.github/oidc-deploy-roles.yaml at commit 786dcfe8,
|
||||
# which remains the deployed source of truth for the management account
|
||||
# (328440206208) until that account's stacks finish migrating out. If a
|
||||
# substrate resource must change while both copies are live, change BOTH
|
||||
# files in the same piece of work. Documented deltas from the source:
|
||||
# - unused GitHubOrg parameter dropped (only serves the per-repo roles
|
||||
# left behind),
|
||||
# - DependsOn: LambdaExecutionBoundary added to SamCfnExecutionRole (the
|
||||
# role only names the boundary ARN inside Condition strings, so CFN
|
||||
# infers no edge; first-create needs the boundary to exist first — moot
|
||||
# for mgmt where both resources already exist, so mgmt's copy is
|
||||
# deliberately unchanged),
|
||||
# - DeletionPolicy/UpdateReplacePolicy Retain on the OIDC provider,
|
||||
# - SECURITY FIX, deliberate divergence: iam:DeleteRolePermissionsBoundary
|
||||
# removed from Sid IAMPutPermissionsBoundary and explicit Deny statements
|
||||
# (DenyBoundaryTampering / DenyBoundaryPolicyEdit) added. The mgmt copy
|
||||
# still carries the hole — verified live 2026-07-27 via
|
||||
# simulate-principal-policy on the deployed mgmt role
|
||||
# (iam:DeleteRolePermissionsBoundary = ALLOWED). New accounts must not be
|
||||
# born with it. Remediating mgmt means changing a role that is actively
|
||||
# executing production deploys, so it is tracked as a separate change
|
||||
# with its own review gates rather than folded in here. Reconcile the two
|
||||
# copies when that lands.
|
||||
#
|
||||
# This template is deployed via lib/deploy-substrate-stack.ts
|
||||
# (cloudformation-include) as stack seahaven-deploy-substrate, once per
|
||||
# member account that hosts SAM workloads.
|
||||
|
||||
Parameters:
|
||||
CreateOIDCProvider:
|
||||
Type: String
|
||||
Default: "false"
|
||||
AllowedValues: ["true", "false"]
|
||||
Description: Set to true only if the GitHub OIDC provider does not already exist in this account
|
||||
|
||||
Conditions:
|
||||
ShouldCreateOIDCProvider: !Equals [!Ref CreateOIDCProvider, "true"]
|
||||
|
||||
Resources:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OIDC Provider (conditional — most accounts already have it; seahaven-prod
|
||||
# and seahaven-dev both do, from their githubdeploy-* role provisioning)
|
||||
# ---------------------------------------------------------------------------
|
||||
GitHubOIDCProvider:
|
||||
Type: AWS::IAM::OIDCProvider
|
||||
Condition: ShouldCreateOIDCProvider
|
||||
Properties:
|
||||
Url: https://token.actions.githubusercontent.com
|
||||
ClientIdList:
|
||||
- sts.amazonaws.com
|
||||
ThumbprintList:
|
||||
- 6938fd4d98bab03faadb97b34396831e3780aea1
|
||||
# An account has exactly ONE provider per URL and every githubdeploy-* role
|
||||
# trusts it. Retain so that flipping createOidcProvider back to false (or
|
||||
# deleting this stack) can never delete the account's federation anchor and
|
||||
# break every deploy into it.
|
||||
DeletionPolicy: Retain
|
||||
UpdateReplacePolicy: Retain
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lambda execution permissions boundary (INFRA-103)
|
||||
#
|
||||
# This managed policy is the CEILING for every Lambda execution role that the
|
||||
# five SAM stacks auto-generate via AWS::Serverless::Function. Applying it as
|
||||
# PermissionsBoundary on those roles means the effective permissions are the
|
||||
# intersection of the role's own policies and this boundary, so a misconfigured
|
||||
# SAM role can never exceed what is listed here.
|
||||
#
|
||||
# The boundary is intentionally a SUPERSET of the union of all runtime
|
||||
# permissions currently granted across the five stacks. Being slightly broad
|
||||
# is the correct trade-off at this stage — a boundary that is too tight will
|
||||
# break Lambda functions at runtime after deploy, which is worse than a slightly
|
||||
# loose boundary that is tightened in a follow-up.
|
||||
#
|
||||
# Permission sources per stack:
|
||||
#
|
||||
# afterhours-shift-manager
|
||||
# - DynamoDB CRUD (afterhours-shifts table)
|
||||
# - secretsmanager:GetSecretValue (afterhours-shift-manager/*)
|
||||
# - ses:SendEmail (SES identity)
|
||||
# - CloudWatch Logs (all functions)
|
||||
#
|
||||
# payments-dashboard
|
||||
# - DynamoDB CRUD / Read (PaymentsDashboard table)
|
||||
# - S3 GetObject (payroll-emails, payments-csv buckets)
|
||||
# - secretsmanager:GetSecretValue (payments-dashboard/*)
|
||||
# - sqs:SendMessage + sqs:ReceiveMessage + sqs:DeleteMessage etc.
|
||||
# (PayrollBatchQueue + DLQs)
|
||||
# - lambda:InvokeFunction (ExpenseReceiver → ExpenseProcessor)
|
||||
# - ec2:CreateNetworkInterface / DescribeNetworkInterfaces /
|
||||
# DeleteNetworkInterface (VPC-attached functions)
|
||||
# - CloudWatch Logs
|
||||
#
|
||||
# meal-order-manager
|
||||
# - DynamoDB CRUD / Read (meal-order-manager-orders table)
|
||||
# - S3 CRUD (ReportsBucket) + s3:GetObject (ReportsBucket presigned URLs)
|
||||
# - secretsmanager:GetSecretValue (meal-order-manager/*)
|
||||
# - ssm:GetParameter (/meal-order-manager/*)
|
||||
# - lambda:InvokeFunction (submit-order → slack-notifier,
|
||||
# close-form → aggregate-orders)
|
||||
# - ses:SendRawEmail
|
||||
# - CloudWatch Logs
|
||||
#
|
||||
# front-integrations
|
||||
# - DynamoDB CRUD (front-sla-alerts table)
|
||||
# - secretsmanager:GetSecretValue (by ARN, various)
|
||||
# - CloudWatch Logs
|
||||
#
|
||||
# afi-backup-monitor
|
||||
# - secretsmanager:GetSecretValue (by ARN)
|
||||
# - CloudWatch Logs
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
LambdaExecutionBoundary:
|
||||
Type: AWS::IAM::ManagedPolicy
|
||||
Properties:
|
||||
ManagedPolicyName: seahaven-lambda-execution-boundary
|
||||
Description: >-
|
||||
Permissions boundary ceiling for all SAM-managed Lambda execution roles.
|
||||
Applied via PermissionsBoundary on every Globals.Function in the five
|
||||
SAM stacks (INFRA-103). Effective permissions are the intersection of
|
||||
this policy and the role's own inline policies.
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
|
||||
# ── CloudWatch Logs (every Lambda) ──────────────────────────────────
|
||||
- Sid: CloudWatchLogs
|
||||
Effect: Allow
|
||||
Action:
|
||||
- logs:CreateLogGroup
|
||||
- logs:CreateLogStream
|
||||
- logs:PutLogEvents
|
||||
- logs:DescribeLogGroups
|
||||
- logs:DescribeLogStreams
|
||||
Resource: "*"
|
||||
|
||||
# ── X-Ray tracing (standard Lambda execution) ────────────────────
|
||||
- Sid: XRay
|
||||
Effect: Allow
|
||||
Action:
|
||||
- xray:PutTraceSegments
|
||||
- xray:PutTelemetryRecords
|
||||
Resource: "*"
|
||||
|
||||
# ── VPC / ENI management (payments-dashboard VPC functions) ────────
|
||||
# Matches AWSLambdaVPCAccessExecutionRole exactly.
|
||||
# AssignPrivateIpAddresses / UnassignPrivateIpAddresses are for EFA
|
||||
# and secondary IPs — not part of the Lambda ENI lifecycle — omitted.
|
||||
- Sid: Ec2Eni
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ec2:CreateNetworkInterface
|
||||
- ec2:DescribeNetworkInterfaces
|
||||
- ec2:DeleteNetworkInterface
|
||||
- ec2:DescribeSubnets
|
||||
- ec2:DescribeSecurityGroups
|
||||
- ec2:DescribeVpcs
|
||||
Resource: "*"
|
||||
|
||||
# ── DynamoDB (afterhours, payments, meal-order, front-integrations) ─
|
||||
# Table/* covers base-table operations; table/*/index/* is required for
|
||||
# Query/Scan on Global Secondary Indexes.
|
||||
- Sid: DynamoDB
|
||||
Effect: Allow
|
||||
Action:
|
||||
- dynamodb:GetItem
|
||||
- dynamodb:PutItem
|
||||
- dynamodb:UpdateItem
|
||||
- dynamodb:DeleteItem
|
||||
- dynamodb:Query
|
||||
- dynamodb:Scan
|
||||
- dynamodb:BatchGetItem
|
||||
- dynamodb:BatchWriteItem
|
||||
- dynamodb:DescribeTable
|
||||
- dynamodb:ConditionCheckItem
|
||||
Resource:
|
||||
- !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*"
|
||||
- !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*/index/*"
|
||||
|
||||
# ── S3 (payments-dashboard read, meal-order-manager CRUD) ──────────
|
||||
- Sid: S3
|
||||
Effect: Allow
|
||||
Action:
|
||||
- s3:GetObject
|
||||
- s3:PutObject
|
||||
- s3:DeleteObject
|
||||
- s3:ListBucket
|
||||
- s3:GetBucketLocation
|
||||
- s3:GetObjectVersion
|
||||
- s3:GetObjectTagging
|
||||
- s3:PutObjectTagging
|
||||
Resource:
|
||||
- !Sub "arn:aws:s3:::*-${AWS::AccountId}"
|
||||
- !Sub "arn:aws:s3:::*-${AWS::AccountId}/*"
|
||||
# meal-order-manager ReportsBucket (non-AccountId suffix pattern)
|
||||
- !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}"
|
||||
- !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*"
|
||||
- !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}"
|
||||
- !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*"
|
||||
|
||||
# ── Secrets Manager (all stacks) ──────────────────────────────────
|
||||
- Sid: SecretsManager
|
||||
Effect: Allow
|
||||
Action:
|
||||
- secretsmanager:GetSecretValue
|
||||
- secretsmanager:DescribeSecret
|
||||
Resource:
|
||||
- !Sub "arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:*"
|
||||
|
||||
# ── SSM Parameter Store (meal-order-manager, afterhours) ──────────
|
||||
- Sid: SSMParameterRead
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ssm:GetParameter
|
||||
- ssm:GetParameters
|
||||
- ssm:GetParametersByPath
|
||||
Resource:
|
||||
- !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*"
|
||||
|
||||
# ── SQS (payments-dashboard batch queues) ─────────────────────────
|
||||
- Sid: SQS
|
||||
Effect: Allow
|
||||
Action:
|
||||
- sqs:SendMessage
|
||||
- sqs:ReceiveMessage
|
||||
- sqs:DeleteMessage
|
||||
- sqs:GetQueueAttributes
|
||||
- sqs:GetQueueUrl
|
||||
- sqs:ChangeMessageVisibility
|
||||
Resource:
|
||||
- !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*"
|
||||
|
||||
# ── Lambda invocation (payments, meal-order inter-function calls) ──
|
||||
- Sid: LambdaInvoke
|
||||
Effect: Allow
|
||||
Action:
|
||||
- lambda:InvokeFunction
|
||||
Resource:
|
||||
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:*"
|
||||
|
||||
# ── SES (afterhours weekly-post, meal-order email-report) ──────────
|
||||
- Sid: SES
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ses:SendEmail
|
||||
- ses:SendRawEmail
|
||||
Resource:
|
||||
- !Sub "arn:aws:ses:us-east-1:${AWS::AccountId}:identity/*"
|
||||
- !Sub "arn:aws:ses:us-east-1:${AWS::AccountId}:configuration-set/*"
|
||||
|
||||
# ── KMS (CMK-encrypted resources) ─────────────────────────────────
|
||||
# Required for Lambda functions that read/write CMK-encrypted AWS
|
||||
# resources. Verified live state:
|
||||
# - PaymentsDashboard DynamoDB table: CMK key/0b660af3 (KMS:ENABLED)
|
||||
# - payments-dashboard CloudWatch log groups: CMK key/b748750c
|
||||
# Secrets Manager + SQS queues in these stacks use AWS-managed keys
|
||||
# (aws/secretsmanager, aws/sqs) which do not require explicit kms:*
|
||||
# actions in the execution role policy. The CMK keys are scoped to
|
||||
# this account to prevent cross-account KMS calls.
|
||||
- Sid: KMS
|
||||
Effect: Allow
|
||||
Action:
|
||||
- kms:Decrypt
|
||||
- kms:GenerateDataKey
|
||||
- kms:DescribeKey
|
||||
Resource:
|
||||
- !Sub "arn:aws:kms:us-east-1:${AWS::AccountId}:key/*"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped
|
||||
#
|
||||
# Replaces the previous blanket managed-policy set (IAMFullAccess +
|
||||
# *FullAccess) with per-service inline statements that cover exactly
|
||||
# what the five SAM stacks need during a CloudFormation deploy/update.
|
||||
#
|
||||
# PRIMARY ESCALATION CONTROL
|
||||
# iam:CreateRole and iam:AttachRolePolicy / iam:PutRolePolicy are
|
||||
# conditioned on iam:PermissionsBoundary StringEquals the boundary ARN
|
||||
# (seahaven-lambda-execution-boundary, created in INFRA-103). That
|
||||
# condition is what prevents the CFN execution role from minting an
|
||||
# unconstrained admin role.
|
||||
#
|
||||
# SAM RolePath deviation note
|
||||
# The original cross-review suggestion mentioned scoping IAM role
|
||||
# creation to a specific path (/cfn-managed/). AWS::Serverless::Function
|
||||
# does NOT support a custom RolePath on auto-generated execution roles —
|
||||
# the PermissionsBoundary property is supported, but the role always lands
|
||||
# at path /. Relying on a path condition (iam:ResourceTag or path-prefix)
|
||||
# would therefore exclude the SAM auto-roles and break every deploy.
|
||||
# The iam:PermissionsBoundary condition achieves the same security goal
|
||||
# without requiring a path. For any explicit AWS::IAM::Role resources
|
||||
# in SAM templates (e.g. AdminAuthorizerInvokeRole in meal-order-manager)
|
||||
# where we can control the path, path scoping can be added in a follow-up.
|
||||
#
|
||||
# DEPLOY ORDER DEPENDENCY
|
||||
# This role references the boundary ARN only as literal !Sub strings inside
|
||||
# Condition values, so CloudFormation infers NO creation edge from the
|
||||
# references alone. The explicit DependsOn below is what guarantees the
|
||||
# boundary exists before the role on first create (IAM would otherwise
|
||||
# accept the role, leaving a window where the role exists unbounded-gated
|
||||
# against a not-yet-existing boundary policy).
|
||||
# ---------------------------------------------------------------------------
|
||||
SamCfnExecutionRole:
|
||||
Type: AWS::IAM::Role
|
||||
DependsOn: LambdaExecutionBoundary
|
||||
Properties:
|
||||
RoleName: github-cfn-execution-role
|
||||
AssumeRolePolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
Principal:
|
||||
Service: cloudformation.amazonaws.com
|
||||
Action: sts:AssumeRole
|
||||
Policies:
|
||||
|
||||
# ── CloudFormation transforms (SAM macro) ─────────────────────────
|
||||
- PolicyName: cloudformation-transforms
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: AllowSAMTransform
|
||||
Effect: Allow
|
||||
Action:
|
||||
- cloudformation:CreateChangeSet
|
||||
Resource:
|
||||
- arn:aws:cloudformation:us-east-1:aws:transform/*
|
||||
|
||||
# ── Lambda management ─────────────────────────────────────────────
|
||||
# Covers function create/update/delete, aliases, event source
|
||||
# mappings, and Lambda layers — all needed for SAM deploys.
|
||||
- PolicyName: lambda-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: LambdaFunctions
|
||||
Effect: Allow
|
||||
Action:
|
||||
- lambda:AddPermission
|
||||
- lambda:CreateFunction
|
||||
- lambda:DeleteFunction
|
||||
- lambda:GetFunction
|
||||
- lambda:GetFunctionConfiguration
|
||||
- lambda:ListFunctions
|
||||
- lambda:RemovePermission
|
||||
- lambda:UpdateFunctionCode
|
||||
- lambda:UpdateFunctionConfiguration
|
||||
- lambda:UpdateFunctionEventInvokeConfig
|
||||
- lambda:PutFunctionEventInvokeConfig
|
||||
- lambda:DeleteFunctionEventInvokeConfig
|
||||
- lambda:GetFunctionEventInvokeConfig
|
||||
- lambda:ListTags
|
||||
- lambda:TagResource
|
||||
- lambda:UntagResource
|
||||
- lambda:GetPolicy
|
||||
- lambda:ListVersionsByFunction
|
||||
- lambda:PublishVersion
|
||||
- lambda:CreateAlias
|
||||
- lambda:DeleteAlias
|
||||
- lambda:UpdateAlias
|
||||
- lambda:GetAlias
|
||||
Resource:
|
||||
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:*"
|
||||
- Sid: LambdaLayers
|
||||
Effect: Allow
|
||||
Action:
|
||||
- lambda:PublishLayerVersion
|
||||
- lambda:DeleteLayerVersion
|
||||
- lambda:GetLayerVersion
|
||||
- lambda:ListLayerVersions
|
||||
- lambda:ListLayers
|
||||
- lambda:AddLayerVersionPermission
|
||||
- lambda:RemoveLayerVersionPermission
|
||||
Resource:
|
||||
- !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:layer:*"
|
||||
- Sid: LambdaEventSourceMappings
|
||||
Effect: Allow
|
||||
Action:
|
||||
- lambda:CreateEventSourceMapping
|
||||
- lambda:DeleteEventSourceMapping
|
||||
- lambda:GetEventSourceMapping
|
||||
- lambda:ListEventSourceMappings
|
||||
- lambda:UpdateEventSourceMapping
|
||||
Resource: "*"
|
||||
|
||||
# ── API Gateway (HTTP APIs + REST APIs) ───────────────────────────
|
||||
- PolicyName: apigateway-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: ApiGateway
|
||||
Effect: Allow
|
||||
Action:
|
||||
- apigateway:GET
|
||||
- apigateway:POST
|
||||
- apigateway:PUT
|
||||
- apigateway:PATCH
|
||||
- apigateway:DELETE
|
||||
Resource:
|
||||
- "arn:aws:apigateway:us-east-1::*"
|
||||
|
||||
# ── DynamoDB ──────────────────────────────────────────────────────
|
||||
- PolicyName: dynamodb-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: DynamoDBTables
|
||||
Effect: Allow
|
||||
Action:
|
||||
- dynamodb:CreateTable
|
||||
- dynamodb:DeleteTable
|
||||
- dynamodb:DescribeTable
|
||||
- dynamodb:UpdateTable
|
||||
- dynamodb:ListTables
|
||||
- dynamodb:TagResource
|
||||
- dynamodb:UntagResource
|
||||
- dynamodb:DescribeTimeToLive
|
||||
- dynamodb:UpdateTimeToLive
|
||||
- dynamodb:DescribeContinuousBackups
|
||||
- dynamodb:UpdateContinuousBackups
|
||||
Resource:
|
||||
- !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/*"
|
||||
|
||||
# ── S3 ────────────────────────────────────────────────────────────
|
||||
# Covers bucket create/configure + object operations for SAM
|
||||
# artifact buckets and application buckets.
|
||||
- PolicyName: s3-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: S3BucketOps
|
||||
Effect: Allow
|
||||
Action:
|
||||
- s3:CreateBucket
|
||||
- s3:DeleteBucket
|
||||
- s3:GetBucketLocation
|
||||
- s3:GetBucketPolicy
|
||||
- s3:PutBucketPolicy
|
||||
- s3:DeleteBucketPolicy
|
||||
- s3:GetBucketTagging
|
||||
- s3:PutBucketTagging
|
||||
- s3:GetBucketVersioning
|
||||
- s3:PutBucketVersioning
|
||||
- s3:GetLifecycleConfiguration
|
||||
- s3:PutLifecycleConfiguration
|
||||
- s3:GetBucketPublicAccessBlock
|
||||
- s3:PutBucketPublicAccessBlock
|
||||
# Explicit BucketEncryption blocks (first: payments-dashboard
|
||||
# BoaRawBucket, 2026-07-22) need the encryption config pair.
|
||||
- s3:GetEncryptionConfiguration
|
||||
- s3:PutEncryptionConfiguration
|
||||
- s3:GetBucketNotification
|
||||
- s3:PutBucketNotification
|
||||
- s3:GetBucketWebsite
|
||||
- s3:PutBucketWebsite
|
||||
- s3:DeleteBucketWebsite
|
||||
- s3:GetBucketAcl
|
||||
- s3:PutBucketAcl
|
||||
Resource:
|
||||
- "arn:aws:s3:::*"
|
||||
- Sid: S3ObjectOps
|
||||
Effect: Allow
|
||||
Action:
|
||||
- s3:GetObject
|
||||
- s3:PutObject
|
||||
- s3:DeleteObject
|
||||
- s3:ListBucket
|
||||
- s3:ListBucketVersions
|
||||
- s3:GetObjectVersion
|
||||
Resource:
|
||||
- "arn:aws:s3:::*"
|
||||
- "arn:aws:s3:::*/*"
|
||||
|
||||
# ── CloudWatch Logs ───────────────────────────────────────────────
|
||||
- PolicyName: cloudwatch-logs-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: CWLogs
|
||||
Effect: Allow
|
||||
Action:
|
||||
- logs:CreateLogGroup
|
||||
- logs:DeleteLogGroup
|
||||
- logs:DescribeLogGroups
|
||||
- logs:PutRetentionPolicy
|
||||
- logs:DeleteRetentionPolicy
|
||||
- logs:ListTagsLogGroup
|
||||
- logs:TagLogGroup
|
||||
- logs:UntagLogGroup
|
||||
- logs:ListTagsForResource
|
||||
- logs:TagResource
|
||||
- logs:UntagResource
|
||||
- logs:CreateLogDelivery
|
||||
- logs:GetLogDelivery
|
||||
- logs:UpdateLogDelivery
|
||||
- logs:DeleteLogDelivery
|
||||
- logs:ListLogDeliveries
|
||||
- logs:PutResourcePolicy
|
||||
- logs:DescribeResourcePolicies
|
||||
- logs:PutDestination
|
||||
- logs:DeleteDestination
|
||||
- logs:DescribeDestinations
|
||||
- logs:AssociateKmsKey
|
||||
- logs:DisassociateKmsKey
|
||||
Resource: "*"
|
||||
|
||||
# ── EventBridge / CloudWatch Events (scheduled Lambdas) ───────────
|
||||
- PolicyName: eventbridge-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: EventBridge
|
||||
Effect: Allow
|
||||
Action:
|
||||
- events:DeleteRule
|
||||
- events:DescribeRule
|
||||
- events:EnableRule
|
||||
- events:DisableRule
|
||||
- events:ListRules
|
||||
- events:ListTargetsByRule
|
||||
- events:PutRule
|
||||
- events:PutTargets
|
||||
- events:RemoveTargets
|
||||
- events:TagResource
|
||||
- events:UntagResource
|
||||
- events:ListTagsForResource
|
||||
- events:PutPermission
|
||||
- events:RemovePermission
|
||||
Resource: "*"
|
||||
|
||||
# ── SES (afterhours weekly-post, meal-order email-report) ─────────
|
||||
- PolicyName: ses-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: SESRules
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ses:CreateReceiptRule
|
||||
- ses:DeleteReceiptRule
|
||||
- ses:DescribeReceiptRule
|
||||
- ses:UpdateReceiptRule
|
||||
- ses:CreateReceiptRuleSet
|
||||
- ses:DescribeActiveReceiptRuleSet
|
||||
- ses:DescribeReceiptRuleSet
|
||||
- ses:SetActiveReceiptRuleSet
|
||||
- ses:ReorderReceiptRuleSet
|
||||
- ses:GetIdentityVerificationAttributes
|
||||
- ses:ListIdentities
|
||||
Resource: "*"
|
||||
|
||||
# ── SQS (payments-dashboard queues + DLQs) ────────────────────────
|
||||
- PolicyName: sqs-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: SQSQueues
|
||||
Effect: Allow
|
||||
Action:
|
||||
- sqs:CreateQueue
|
||||
- sqs:DeleteQueue
|
||||
- sqs:GetQueueAttributes
|
||||
- sqs:SetQueueAttributes
|
||||
- sqs:GetQueueUrl
|
||||
- sqs:ListQueues
|
||||
- sqs:TagQueue
|
||||
- sqs:UntagQueue
|
||||
- sqs:ListQueueTags
|
||||
- sqs:AddPermission
|
||||
- sqs:RemovePermission
|
||||
Resource:
|
||||
- !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:*"
|
||||
|
||||
# ── SNS (validation / alarm notifications) ────────────────────────
|
||||
- PolicyName: sns-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: SNS
|
||||
Effect: Allow
|
||||
Action:
|
||||
- sns:CreateTopic
|
||||
- sns:DeleteTopic
|
||||
- sns:GetTopicAttributes
|
||||
- sns:SetTopicAttributes
|
||||
- sns:Subscribe
|
||||
- sns:Unsubscribe
|
||||
- sns:ListSubscriptionsByTopic
|
||||
- sns:ListTopics
|
||||
- sns:TagResource
|
||||
- sns:UntagResource
|
||||
Resource:
|
||||
- !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:*"
|
||||
|
||||
# ── CloudWatch Alarms ─────────────────────────────────────────────
|
||||
- PolicyName: cloudwatch-alarms-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: CWAlarms
|
||||
Effect: Allow
|
||||
Action:
|
||||
- cloudwatch:PutMetricAlarm
|
||||
- cloudwatch:DeleteAlarms
|
||||
- cloudwatch:DescribeAlarms
|
||||
- cloudwatch:EnableAlarmActions
|
||||
- cloudwatch:DisableAlarmActions
|
||||
- cloudwatch:ListTagsForResource
|
||||
- cloudwatch:TagResource
|
||||
- cloudwatch:UntagResource
|
||||
Resource: "*"
|
||||
|
||||
# ── EC2 / VPC / NAT / EIP / Security Groups ───────────────────────
|
||||
# payments-dashboard deploys a VPC, NAT gateway, EIP, route tables,
|
||||
# subnets, security groups, and gateway VPC endpoints.
|
||||
- PolicyName: ec2-vpc-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: EC2VPC
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ec2:AllocateAddress
|
||||
- ec2:AssociateRouteTable
|
||||
- ec2:AttachInternetGateway
|
||||
- ec2:AuthorizeSecurityGroupEgress
|
||||
- ec2:AuthorizeSecurityGroupIngress
|
||||
- ec2:CreateInternetGateway
|
||||
- ec2:CreateNatGateway
|
||||
- ec2:CreateRoute
|
||||
- ec2:CreateRouteTable
|
||||
- ec2:CreateSecurityGroup
|
||||
- ec2:CreateSubnet
|
||||
- ec2:CreateVpc
|
||||
- ec2:CreateVpcEndpoint
|
||||
- ec2:CreateTags
|
||||
- ec2:DeleteInternetGateway
|
||||
- ec2:DeleteNatGateway
|
||||
- ec2:DeleteRoute
|
||||
- ec2:DeleteRouteTable
|
||||
- ec2:DeleteSecurityGroup
|
||||
- ec2:DeleteSubnet
|
||||
- ec2:DeleteVpc
|
||||
- ec2:DeleteVpcEndpoints
|
||||
- ec2:DescribeAddresses
|
||||
- ec2:DescribeAvailabilityZones
|
||||
- ec2:DescribeInternetGateways
|
||||
- ec2:DescribeNatGateways
|
||||
- ec2:DescribeRouteTables
|
||||
- ec2:DescribeSecurityGroups
|
||||
- ec2:DescribeSubnets
|
||||
- ec2:DescribeVpcEndpoints
|
||||
- ec2:DescribeVpcs
|
||||
- ec2:DescribePrefixLists
|
||||
- ec2:DetachInternetGateway
|
||||
- ec2:DisassociateAddress
|
||||
- ec2:DisassociateRouteTable
|
||||
- ec2:ModifySubnetAttribute
|
||||
- ec2:ModifyVpcAttribute
|
||||
- ec2:ModifyVpcEndpoint
|
||||
- ec2:ReleaseAddress
|
||||
- ec2:RevokeSecurityGroupEgress
|
||||
- ec2:RevokeSecurityGroupIngress
|
||||
- ec2:UpdateSecurityGroupRuleDescriptionsEgress
|
||||
- ec2:UpdateSecurityGroupRuleDescriptionsIngress
|
||||
Resource: "*"
|
||||
|
||||
# ── CloudFront + OAC (meal-order-manager form distribution) ───────
|
||||
- PolicyName: cloudfront-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: CloudFront
|
||||
Effect: Allow
|
||||
Action:
|
||||
- cloudfront:CreateDistribution
|
||||
- cloudfront:DeleteDistribution
|
||||
- cloudfront:GetDistribution
|
||||
- cloudfront:GetDistributionConfig
|
||||
- cloudfront:UpdateDistribution
|
||||
- cloudfront:TagResource
|
||||
- cloudfront:UntagResource
|
||||
- cloudfront:ListTagsForResource
|
||||
- cloudfront:CreateOriginAccessControl
|
||||
- cloudfront:DeleteOriginAccessControl
|
||||
- cloudfront:GetOriginAccessControl
|
||||
- cloudfront:GetOriginAccessControlConfig
|
||||
- cloudfront:UpdateOriginAccessControl
|
||||
- cloudfront:ListOriginAccessControls
|
||||
- cloudfront:CreateInvalidation
|
||||
- cloudfront:GetInvalidation
|
||||
Resource: "*"
|
||||
|
||||
# ── SSM Parameter Store (meal-order-manager, afterhours) ──────────
|
||||
# Write is needed because meal-order-manager creates
|
||||
# /meal-order-manager/slack-channel-id via AWS::SSM::Parameter.
|
||||
- PolicyName: ssm-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: SSMParameters
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ssm:GetParameter
|
||||
- ssm:GetParameters
|
||||
- ssm:GetParametersByPath
|
||||
- ssm:PutParameter
|
||||
- ssm:DeleteParameter
|
||||
- ssm:DeleteParameters
|
||||
- ssm:DescribeParameters
|
||||
- ssm:AddTagsToResource
|
||||
- ssm:RemoveTagsFromResource
|
||||
- ssm:ListTagsForResource
|
||||
Resource:
|
||||
- !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/*"
|
||||
# WAF association needs SSM parameter read at deploy time
|
||||
# (/seahaven/waf/app-web-acl-arn value lookup)
|
||||
- Sid: SSMParameterDescribe
|
||||
Effect: Allow
|
||||
Action:
|
||||
- ssm:DescribeParameters
|
||||
Resource: "*"
|
||||
|
||||
# ── WAF (meal-order-manager CloudFront WebACL association) ────────
|
||||
- PolicyName: waf-management
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: WAF
|
||||
Effect: Allow
|
||||
Action:
|
||||
- wafv2:GetWebACL
|
||||
- wafv2:GetWebACLForResource
|
||||
- wafv2:ListWebACLs
|
||||
- wafv2:AssociateWebACL
|
||||
- wafv2:DisassociateWebACL
|
||||
- wafv2:ListResourcesForWebACL
|
||||
Resource: "*"
|
||||
|
||||
# ── IAM role lifecycle — BOUNDARY-GATED ──────────────────────────
|
||||
# This is the PRIMARY escalation control for INFRA-97.
|
||||
#
|
||||
# iam:CreateRole / iam:AttachRolePolicy / iam:PutRolePolicy are
|
||||
# conditioned on iam:PermissionsBoundary StringEquals the
|
||||
# seahaven-lambda-execution-boundary ARN. That condition means
|
||||
# any role this execution role creates must have the boundary
|
||||
# applied, so it can never exceed what the boundary allows
|
||||
# (which is scoped to the services the five stacks actually use).
|
||||
#
|
||||
# iam:PassRole is also included here so CloudFormation can pass
|
||||
# the auto-generated Lambda execution role to the Lambda service.
|
||||
#
|
||||
# Why not path-scoped (e.g. iam:ResourceTag / path /cfn-managed/)?
|
||||
# SAM's AWS::Serverless::Function auto-generates execution roles at
|
||||
# path / — there is no supported way to set a custom RolePath on
|
||||
# SAM auto-roles. A path condition would therefore exclude the
|
||||
# SAM auto-roles and break every deploy. The PermissionsBoundary
|
||||
# condition achieves the same security goal without a path requirement.
|
||||
- PolicyName: iam-role-management-boundary-gated
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
# Create role — MUST attach boundary
|
||||
- Sid: IAMCreateRoleWithBoundary
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:CreateRole
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
|
||||
# Attach managed policies — MUST have boundary already on role
|
||||
- Sid: IAMAttachPolicyWithBoundary
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:AttachRolePolicy
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
|
||||
# Put inline policy — MUST have boundary already on role
|
||||
- Sid: IAMPutRolePolicyWithBoundary
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:PutRolePolicy
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
|
||||
# Boundary management — SET the boundary only. DELETE is NOT
|
||||
# granted: for a delete, the iam:PermissionsBoundary condition key
|
||||
# reflects the boundary CURRENTLY attached to the target role, so
|
||||
# a StringEquals condition on the boundary ARN MATCHES exactly the
|
||||
# roles the gate protects. Granting delete under that condition
|
||||
# lets this role create a boundary-gated role with an inline *:*
|
||||
# policy, strip the boundary, and pass the now-unbounded role to
|
||||
# Lambda — defeating the primary escalation control. Verified live
|
||||
# against the mgmt copy 2026-07-27 (simulate-principal-policy:
|
||||
# iam:DeleteRolePermissionsBoundary = allowed). SAM never needs
|
||||
# the delete: it only SETS the boundary on roles it creates, and
|
||||
# stack teardown calls DeleteRole, not DeleteRolePermissionsBoundary.
|
||||
- Sid: IAMPutPermissionsBoundary
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:PutRolePermissionsBoundary
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
|
||||
# Explicit Deny backstop (AWS's documented NoBoundaryPolicyEdit /
|
||||
# NoBoundaryDelete delegation pattern). A Deny is required, not
|
||||
# merely omitting the Allow: without it, any future Allow added to
|
||||
# this role — or a broader managed policy attached to it — silently
|
||||
# reopens the escalation. Covers both removing a boundary from a
|
||||
# role and rewriting the boundary POLICY DOCUMENT itself (the
|
||||
# latter is only implicitly denied today).
|
||||
- Sid: DenyBoundaryTampering
|
||||
Effect: Deny
|
||||
Action:
|
||||
- iam:DeleteRolePermissionsBoundary
|
||||
- iam:DeleteUserPermissionsBoundary
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:user/*"
|
||||
|
||||
- Sid: DenyBoundaryPolicyEdit
|
||||
Effect: Deny
|
||||
Action:
|
||||
- iam:CreatePolicyVersion
|
||||
- iam:SetDefaultPolicyVersion
|
||||
- iam:DeletePolicyVersion
|
||||
- iam:DeletePolicy
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
||||
|
||||
# Read / tag / delete role and policy — no boundary condition needed
|
||||
- Sid: IAMRoleReadAndDelete
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:DeleteRole
|
||||
- iam:DeleteRolePolicy
|
||||
- iam:DetachRolePolicy
|
||||
- iam:GetRole
|
||||
- iam:GetRolePolicy
|
||||
- iam:ListAttachedRolePolicies
|
||||
- iam:ListRolePolicies
|
||||
- iam:ListRoles
|
||||
- iam:TagRole
|
||||
- iam:UntagRole
|
||||
- iam:UpdateRole
|
||||
- iam:UpdateRoleDescription
|
||||
- iam:UpdateAssumeRolePolicy
|
||||
- iam:GetPolicy
|
||||
- iam:GetPolicyVersion
|
||||
- iam:ListPolicies
|
||||
- iam:ListPolicyVersions
|
||||
Resource: "*"
|
||||
|
||||
# PassRole — CloudFormation passes the Lambda execution role
|
||||
# to the Lambda service. Scoped to SAM-generated role pattern.
|
||||
- Sid: IAMPassRole
|
||||
Effect: Allow
|
||||
Action:
|
||||
- iam:PassRole
|
||||
Resource:
|
||||
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
"iam:PassedToService": "lambda.amazonaws.com"
|
||||
|
||||
Loading…
Add table
Reference in a new issue