From a6f22880db446434383bfbe6fb0e34800baf6034 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:07:04 -0400 Subject: [PATCH 01/17] feat(waf): add seahaven-prod shared CloudFront WebACL (PLAT-92) (#96) * feat(waf): add seahaven-prod shared CloudFront WebACL stack Stand up AppWebAcl in a thin prod stack and widen seahaven-site HCP roles to read the SSM ARN so CloudFront can associate the ACL in-account. * fix(deploy): add app-web-acl-prod to deploy.yaml --- .github/workflows/deploy.yaml | 2 +- README.md | 1 + bin/app.ts | 10 ++++++ lib/app-web-acl-stack.ts | 25 +++++++++++++++ .../terraform-substrate.template.yaml | 31 +++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 lib/app-web-acl-stack.ts diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index dca9c15..9da1991 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -56,7 +56,7 @@ jobs: uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 with: node-version: "24" - stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod terraform-substrate-prod" + stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod terraform-substrate-prod app-web-acl-prod" stack-name: "seahaven-prod-baseline" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }} diff --git a/README.md b/README.md index adb9d50..57c4fdc 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ the TypeScript source — no separate compile step needed for `cdk synth` / | `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` | +| `app-web-acl-prod` | `seahaven-app-web-acl` | 011934824531 | us-east-1 | `lib/app-web-acl-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 diff --git a/bin/app.ts b/bin/app.ts index af49987..b42a902 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -9,6 +9,7 @@ import { RegionalBaselineStack } from "../lib/regional-baseline-stack"; import { DeploySubstrateStack } from "../lib/deploy-substrate-stack"; import { TerraformSubstrateStack } from "../lib/terraform-substrate-stack"; import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack"; +import { AppWebAclStack } from "../lib/app-web-acl-stack"; import { MemberBaselineStack } from "../lib/member-baseline-stack"; import { OrgGovernanceStack } from "../lib/org-governance-stack"; @@ -207,6 +208,15 @@ const terraformSubstrateProd = new TerraformSubstrateStack( ); terraformSubstrateProd.addStackDependency(deploySubstrateProd); +// Shared CloudFront WAF for seahaven-prod (PLAT-92). Same AppWebAcl construct +// as mgmt account-baseline; thin stack so prod does not inherit the full +// mgmt baseline. Publishes /seahaven/waf/app-web-acl-arn for in-account +// CloudFront associations (same-account only). +new AppWebAclStack(app, "app-web-acl-prod", { + stackName: "seahaven-app-web-acl", + env: { account: PROD_ACCOUNT, region: "us-east-1" }, +}); + const terraformSubstrateDev = new TerraformSubstrateStack( app, "terraform-substrate-dev", diff --git a/lib/app-web-acl-stack.ts b/lib/app-web-acl-stack.ts new file mode 100644 index 0000000..30d7ae2 --- /dev/null +++ b/lib/app-web-acl-stack.ts @@ -0,0 +1,25 @@ +import * as cdk from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { AppWebAcl } from "./web-acl"; + +/** + * Thin per-account stack that owns the shared CloudFront WAFv2 WebACL (M-17) + * and publishes its ARN to SSM `/seahaven/waf/app-web-acl-arn`. + * + * Mgmt already has this ACL inside `AccountBaselineStack`. Workload accounts + * (starting with seahaven-prod / PLAT-92) get a dedicated stack so we do not + * pull the full mgmt baseline (trail, budgets, flow logs, …) into prod just + * to share a CloudFront WAF. App stacks associate by reading the SSM param + * in-account — WAFv2 CloudFront associations are same-account only. + */ +export class AppWebAclStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + new AppWebAcl(this, "AppWebAcl"); + + cdk.Tags.of(this).add("Project", "account-baseline"); + cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); + cdk.Tags.of(this).add("ManagedBy", "cdk"); + } +} diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index dbe482d..ab93017 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1482,6 +1482,19 @@ Resources: - acm:ListTagsForCertificate - acm:GetCertificate Resource: "*" + - Sid: RefreshAppWebAclSsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" + - Sid: RefreshWafWebAcl + Effect: Allow + Action: + - wafv2:GetWebACL + - wafv2:ListWebACLs + Resource: "*" HcptfSeahavenSiteApplyRole: Type: AWS::IAM::Role @@ -1554,3 +1567,21 @@ Resources: Condition: StringEquals: "aws:ResourceTag/Project": seahaven-site + # CloudFront web_acl_id is set via UpdateDistribution (cloudfront:* + # above). Read the shared ACL ARN from SSM (PLAT-92) and allow + # WAFv2 describe so plans/applies can validate the association. + - Sid: ReadAppWebAclSsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" + - Sid: ReadWafWebAcl + Effect: Allow + Action: + - wafv2:GetWebACL + - wafv2:GetWebACLForResource + - wafv2:ListWebACLs + - wafv2:ListResourcesForWebACL + Resource: "*" From 44b672ae9ad0f672d3b917b92ee8db1456a460e1 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 7 Aug 2026 19:36:20 -0400 Subject: [PATCH 02/17] feat(iam): add hcptf roles and boundary widen for meal-order-manager (PLAT-70) (#97) * feat(iam): add hcptf roles and boundary widen for meal-order-manager Append plan/apply OIDC roles for meal-order-manager-prod and widen the lambda execution boundary with exact prod secret ARN and data-plane statements. * fix(iam): make meal-order plan role Lambda refresh read-only Replace plan-role lambda:* with Get*/List* so plan-phase credentials cannot mutate functions or layers. --- .../deploy-substrate.template.yaml | 89 ++++- .../terraform-substrate.template.yaml | 314 ++++++++++++++++++ 2 files changed, 402 insertions(+), 1 deletion(-) diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index bc1f8dc..c2b37fd 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -692,9 +692,96 @@ Resources: - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/*" - !Ref AWS::NoValue + # ── meal-order-manager (PLAT-70) — prod-only exact secret ARN + data plane ─ + # Derived from meal-order-manager template.yaml + secrets created in + # seahaven-prod 2026-08-07. Exact ARN only (no secret:meal-order-* patterns). + - !If + - IsProdAccount + - Sid: MealOrderManagerSecrets + Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-ZGmSGw + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerDynamoDB + 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/meal-order-manager-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/index/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerS3 + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:PutObject* + - s3:DeleteObject* + - s3:AbortMultipartUpload + Resource: + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerSsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerLambdaInvoke + Effect: Allow + Action: + - lambda:InvokeFunction + Resource: + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:meal-order-manager-*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerSes + Effect: Allow + Action: + - ses:SendRawEmail + - ses:SendEmail + Resource: "*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerSns + Effect: Allow + Action: + - sns:Publish + Resource: + - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:site-alerts" + - !Ref AWS::NoValue + # ── FURTHER PER-WORKLOAD DATA-PLANE STATEMENTS ────────────────────── # Floor above + afi-backup-monitor (PLAT-56) + front-integrations - # (PLAT-72) + procurement-ingest (PLAT-86) + seahaven-site (PLAT-91). + # (PLAT-72) + procurement-ingest (PLAT-86) + seahaven-site (PLAT-91) + # + meal-order-manager (PLAT-70). # Additional stacks add their own statements here, derived from THEIR # OWN template, in their own PR, deployed to UPDATE_COMPLETE before # first workload deploy. WIDENING PATH in the header still governs. diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index ab93017..2beb176 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1585,3 +1585,317 @@ Resources: - wafv2:ListWebACLs - wafv2:ListResourcesForWebACL Resource: "*" + + # --------------------------------------------------------------------------- + # meal-order-manager-prod (PLAT-70) — HttpApi + 7 Lambdas + layer + DynamoDB + # + S3 form/reports/artifacts + CloudFront/ACM/WAF + EventBridge + alarms. + # Plan role: ViewOnly + plan-refresh sidecar. Apply role: HcptfIamManagement + # + prefix-scoped service wildcards (no enumerated Get* lists). + # --------------------------------------------------------------------------- + HcptfMealOrderManagerPlanRole: + Type: AWS::IAM::Role + Condition: IsProdAccount + Properties: + RoleName: hcptf-meal-order-manager-plan + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/app.terraform.io" + Action: sts:AssumeRoleWithWebIdentity + Condition: + StringEquals: + "app.terraform.io:aud": aws.workload.identity + "app.terraform.io:sub": organization:seahaven:project:seahaven-prod:workspace:meal-order-manager-prod:run_phase:plan + ManagedPolicyArns: + - arn:aws:iam::aws:policy/job-function/ViewOnlyAccess + Policies: + - PolicyName: meal-order-manager-plan-refresh + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: RefreshIamRoles + Effect: Allow + Action: + - iam:GetRole + - iam:GetRolePolicy + - iam:ListRolePolicies + - iam:ListAttachedRolePolicies + - iam:ListRoleTags + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/meal-order-manager-*" + - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/githubdeploy-meal-order-manager*" + - Sid: RefreshManagedPolicies + Effect: Allow + Action: + - iam:GetPolicy + - iam:GetPolicyVersion + Resource: "*" + - Sid: RefreshEventBridge + Effect: Allow + Action: + - events:DescribeRule + - events:ListTargetsByRule + - events:ListTagsForResource + Resource: + - !Sub "arn:aws:events:us-east-1:${AWS::AccountId}:rule/meal-order-manager-*" + - Sid: RefreshLambda + Effect: Allow + Action: + # Read-only refresh for plan; mutate APIs stay on the apply role. + - lambda:Get* + - lambda:List* + Resource: + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:meal-order-manager-*" + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:layer:meal-order-manager-*" + - Sid: RefreshBuckets + Effect: Allow + Action: + - s3:Get* + - s3:ListBucket + Resource: + - !Sub "arn:aws:s3:::meal-order-manager-artifacts-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-artifacts-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" + - Sid: RefreshDynamoDB + Effect: Allow + Action: + - dynamodb:DescribeTable + - dynamodb:DescribeTimeToLive + - dynamodb:DescribeContinuousBackups + - dynamodb:ListTagsOfResource + Resource: + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders" + - Sid: RefreshLogs + Effect: Allow + Action: + - logs:DescribeLogGroups + - logs:ListTagsForResource + Resource: "*" + - Sid: RefreshCloudFront + Effect: Allow + Action: + - cloudfront:Get* + - cloudfront:List* + Resource: "*" + - Sid: RefreshAcm + Effect: Allow + Action: + - acm:DescribeCertificate + - acm:ListCertificates + - acm:ListTagsForCertificate + - acm:GetCertificate + Resource: "*" + - Sid: RefreshAppWebAclSsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + - Sid: RefreshWafWebAcl + Effect: Allow + Action: + - wafv2:GetWebACL + - wafv2:ListWebACLs + Resource: "*" + - Sid: RefreshHttpApi + Effect: Allow + Action: + - apigateway:GET + Resource: + - !Sub "arn:aws:apigateway:us-east-1::/apis/*" + - !Sub "arn:aws:apigateway:us-east-1::/tags/*" + - Sid: RefreshAlarms + Effect: Allow + Action: + - cloudwatch:DescribeAlarms + - cloudwatch:ListTagsForResource + Resource: "*" + + HcptfMealOrderManagerApplyRole: + Type: AWS::IAM::Role + Condition: IsProdAccount + Properties: + RoleName: hcptf-meal-order-manager + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/app.terraform.io" + Action: sts:AssumeRoleWithWebIdentity + Condition: + StringEquals: + "app.terraform.io:aud": aws.workload.identity + "app.terraform.io:sub": organization:seahaven:project:seahaven-prod:workspace:meal-order-manager-prod:run_phase:apply + ManagedPolicyArns: + - !Ref HcptfIamManagementPolicy + Policies: + - PolicyName: meal-order-manager-services + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: LambdaAll + Effect: Allow + Action: + - lambda:* + Resource: + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:meal-order-manager-*" + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:layer:meal-order-manager-*" + - Sid: LambdaList + Effect: Allow + Action: + - lambda:ListFunctions + - lambda:ListLayers + - lambda:GetAccountSettings + Resource: "*" + - Sid: EventBridgeRules + Effect: Allow + Action: + - events:* + Resource: + - !Sub "arn:aws:events:us-east-1:${AWS::AccountId}:rule/meal-order-manager-*" + - Sid: CloudWatchLogs + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:DeleteLogGroup + - logs:PutRetentionPolicy + - logs:DeleteRetentionPolicy + - logs:TagResource + - logs:UntagResource + - logs:ListTagsForResource + - logs:PutMetricFilter + - logs:DeleteMetricFilter + - logs:DescribeMetricFilters + Resource: + - !Sub "arn:aws:logs:us-east-1:${AWS::AccountId}:log-group:/aws/lambda/meal-order-manager-*" + - !Sub "arn:aws:logs:us-east-1:${AWS::AccountId}:log-group:/aws/apigateway/meal-order-manager*" + - Sid: CloudWatchLogsDescribe + Effect: Allow + Action: + - logs:DescribeLogGroups + Resource: "*" + - Sid: StackBuckets + Effect: Allow + Action: + - s3:* + Resource: + - !Sub "arn:aws:s3:::meal-order-manager-artifacts-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-artifacts-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" + - Sid: DynamoDBTable + Effect: Allow + Action: + - dynamodb:* + Resource: + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/*" + - Sid: DynamoDBList + Effect: Allow + Action: + - dynamodb:ListTables + Resource: "*" + - Sid: HttpApiManage + Effect: Allow + Action: + - apigateway:* + Resource: + - !Sub "arn:aws:apigateway:us-east-1::/apis" + - !Sub "arn:aws:apigateway:us-east-1::/apis/*" + - !Sub "arn:aws:apigateway:us-east-1::/tags/*" + - !Sub "arn:aws:apigateway:us-east-1::/vpclinks" + - !Sub "arn:aws:apigateway:us-east-1::/vpclinks/*" + - Sid: CloudFrontManage + Effect: Allow + Action: + - cloudfront:* + Resource: "*" + - Sid: AcmCreate + Effect: Allow + Action: + - acm:RequestCertificate + Resource: "*" + Condition: + StringEquals: + "aws:RequestTag/Project": meal-order-manager + - Sid: AcmList + Effect: Allow + Action: + - acm:ListCertificates + - acm:ListTagsForCertificate + Resource: "*" + - Sid: AcmManageTagged + Effect: Allow + Action: + - acm:DescribeCertificate + - acm:GetCertificate + - acm:DeleteCertificate + - acm:AddTagsToCertificate + - acm:RemoveTagsFromCertificate + - acm:RenewCertificate + Resource: "*" + Condition: + StringEquals: + "aws:ResourceTag/Project": meal-order-manager + - Sid: ReadAppWebAclSsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" + - Sid: MealOrderSsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + - ssm:PutParameter + - ssm:DeleteParameter + - ssm:AddTagsToResource + - ssm:RemoveTagsFromResource + - ssm:ListTagsForResource + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + - Sid: ReadWafWebAcl + Effect: Allow + Action: + - wafv2:GetWebACL + - wafv2:GetWebACLForResource + - wafv2:ListWebACLs + - wafv2:ListResourcesForWebACL + Resource: "*" + - Sid: CloudWatchAlarms + Effect: Allow + Action: + - cloudwatch:PutMetricAlarm + - cloudwatch:DeleteAlarms + - cloudwatch:DescribeAlarms + - cloudwatch:TagResource + - cloudwatch:UntagResource + - cloudwatch:ListTagsForResource + Resource: + - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:meal-order-manager-*" + - Sid: SnsPublishSiteAlerts + Effect: Allow + Action: + - sns:Publish + - sns:GetTopicAttributes + - sns:ListTagsForResource + Resource: + - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:site-alerts" + - Sid: SesIdentityRead + Effect: Allow + Action: + - ses:GetIdentityVerificationAttributes + - ses:GetSendQuota + Resource: "*" From 3605215a28d1abf4fcd5489978dffc017d415c32 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 7 Aug 2026 19:54:28 -0400 Subject: [PATCH 03/17] refactor(iam): consolidate boundary statements under PolicySize cap (#98) Merge workload secret/DDB/S3 SIDs and trim meal-order extras so the shared lambda execution boundary fits under the 6144-character limit. --- .../deploy-substrate.template.yaml | 189 ++++++------------ 1 file changed, 56 insertions(+), 133 deletions(-) diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index c2b37fd..fa9712e 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -121,26 +121,23 @@ Description: >- # correctly left untouched. # # SIZE BUDGET: an attached managed policy document is capped at 6,144 characters -# (whitespace excluded). LambdaExecutionBoundary measures 691 characters across -# 4 statements as of 2026-07-31 — the fleet-wide floor only. Measure before -# widening — len(json.dumps(doc,separators=(',',':'))) on the synthesized -# PolicyDocument with ${AWS::AccountId} resolved, and UPDATE THESE TWO NUMBERS in -# the same edit (they went stale twice inside this branch alone). +# (whitespace excluded). LambdaExecutionBoundary measures 5903 characters across +# 16 statements as of 2026-08-07 (PLAT-93 consolidation after the meal-order +# PolicySize rollback). Measure before widening — len(json.dumps(doc, +# separators=(',',':'))) on the synthesized PolicyDocument with +# ${AWS::AccountId} resolved, and UPDATE THESE TWO NUMBERS in the same edit. # -# Headroom is 5,453 characters, roughly TWELVE workloads at ~450 each. That is a -# deliberate outcome, not luck: an earlier revision of this branch pre-loaded -# per-workload prefixes for all five mgmt SAM stacks and reached 5,457 characters -# with 687 left — about one workload of room — before any stack had actually -# migrated. Deferring per-workload scope to each migration PR (see the note on -# the boundary itself) removed that pressure entirely. If the budget tightens -# again as workloads land, the end-state fix is per-workload boundaries +# Headroom is 241 characters. Statement consolidation (shared WorkloadSecrets / +# WorkloadDynamoDB / extended WorkloadS3 Resource lists; no new action wildcards) +# is the only remaining lever short of per-workload boundaries. If the budget +# tightens again, the end-state fix is per-workload boundaries # (seahaven-lambda-execution-boundary-), which also resolves the -# shared-ceiling residual — tracked as INFRA-187, do not improvise it. +# shared-ceiling residual — tracked as PLAT-52 / INFRA-187, do not improvise it. # CRITICAL: unlike the 2026-07-27 inline-limit incident, # there is NO restructure available when this cap is reached — a role has exactly # ONE permissions boundary, so statements cannot be spilled into a second attached # managed policy. At the cap the only levers are prefix consolidation and dropping -# unused actions. +# unused actions. Do not introduce dynamodb:* / s3:* / ses:Send* to reclaim space. # # This template is deployed via lib/deploy-substrate-stack.ts # (cloudformation-include) as stack seahaven-deploy-substrate, once per member @@ -492,71 +489,38 @@ Resources: - ec2:DescribeVpcs Resource: "*" - # ── afi-backup-monitor (PLAT-56) — prod-only exact secret ARNs ─────── - # Derived from live stack parameters + secrets created in seahaven-prod - # 2026-08-05. No secret:afi-* patterns. Omitted in seahaven-dev via - # IsProdAccount (same template deploys to both accounts). + # ── Shared workload data-plane (PLAT-93 PolicySize consolidation) ─── + # Per-workload secret/DDB/S3 SIDs were merged so meal-order-manager + # (PLAT-70) can fit under the 6,144-character managed-policy cap + # without introducing new action wildcards. Exact secret ARNs only. + # End-state isolation remains PLAT-52 / INFRA-187. + # + # WorkloadSecrets covers: afi-backup-monitor (PLAT-56), + # front-integrations (PLAT-72), procurement-ingest (PLAT-86), + # meal-order-manager (PLAT-70). - !If - IsProdAccount - - Sid: AfiBackupMonitorSecrets + - Sid: WorkloadSecrets Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - arn:aws:secretsmanager:us-east-1:011934824531:secret:afi-api-key-w0E02a - arn:aws:secretsmanager:us-east-1:011934824531:secret:afi-slack-webhook-T4oR3G - - !Ref AWS::NoValue - - # ── front-integrations (PLAT-72) — prod-only exact secret ARNs + DDB ─ - # Derived from live SAM template + secrets created in seahaven-prod - # 2026-08-05. No secret:front-integrations/* patterns. - - !If - - IsProdAccount - - Sid: FrontIntegrationsSecrets - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/front-api-token-UXKv0U - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/slack-bot-token-giGfA7 - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/google-service-account-dqv3Bo - - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: FrontIntegrationsDynamoDB - 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/front-sla-alerts" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts/index/*" - - !Ref AWS::NoValue - - # ── procurement-ingest (PLAT-86) — prod-only exact secrets + data plane ─ - # Derived from live CDK stacks in seahaven-prod 2026-08-06. Exact secret - # ARNs only (no secret:procurement-* / workorder-ingest/* wildcards). - - !If - - IsProdAccount - - Sid: ProcurementIngestSecrets - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - arn:aws:secretsmanager:us-east-1:011934824531:secret:procurement-ingest/web-ui-auth-token-ApAMmr - arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB + - arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-ZGmSGw - !Ref AWS::NoValue + + # WorkloadDynamoDB: enumerated union of front-integrations CRUD + + # procurement-ingest CRUD/stream actions. Meal-order table ARNs appended. + # Stream actions on non-stream tables are inert at the ceiling. - !If - IsProdAccount - - Sid: ProcurementIngestDynamoDB + - Sid: WorkloadDynamoDB Effect: Allow Action: - dynamodb:GetItem @@ -576,6 +540,8 @@ Resources: # it is a silent no-op. Runtime stream consumers use the # stream ARN via DescribeStream/GetRecords above. Resource: + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts/index/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites" @@ -590,10 +556,16 @@ Resources: - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/index/*" - !Ref AWS::NoValue + + # ── procurement-ingest remaining data plane + meal-order S3 (PLAT-86/70) ─ + # WorkloadS3 keeps the prior ProcurementIngestS3 action list and appends + # meal-order form/reports bucket ARNs (same object CRUD shape). - !If - IsProdAccount - - Sid: ProcurementIngestS3 + - Sid: WorkloadS3 Effect: Allow Action: - s3:GetObject* @@ -607,6 +579,10 @@ Resources: - !Sub "arn:aws:s3:::po-ingest-emails-${AWS::AccountId}/*" - !Sub "arn:aws:s3:::workorder-ingest-emails-${AWS::AccountId}" - !Sub "arn:aws:s3:::workorder-ingest-emails-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" - !Ref AWS::NoValue - !If - IsProdAccount @@ -651,6 +627,8 @@ Resources: - arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 - arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 - !Ref AWS::NoValue + # site-alerts publish shared by procurement-ingest alarms and + # meal-order-manager (PLAT-70); no separate MealOrder SNS statement. - !If - IsProdAccount - Sid: ProcurementIngestSns @@ -692,61 +670,15 @@ Resources: - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/*" - !Ref AWS::NoValue - # ── meal-order-manager (PLAT-70) — prod-only exact secret ARN + data plane ─ - # Derived from meal-order-manager template.yaml + secrets created in - # seahaven-prod 2026-08-07. Exact ARN only (no secret:meal-order-* patterns). - - !If - - IsProdAccount - - Sid: MealOrderManagerSecrets - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: - - arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-ZGmSGw - - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerDynamoDB - 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/meal-order-manager-orders" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/index/*" - - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerS3 - Effect: Allow - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - - s3:PutObject* - - s3:DeleteObject* - - s3:AbortMultipartUpload - Resource: - - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" - - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" - - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" - - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" - - !Ref AWS::NoValue + # ── meal-order-manager (PLAT-70) — statements not covered above ───── + # Secrets → WorkloadSecrets; DynamoDB → WorkloadDynamoDB; S3 → WorkloadS3; + # SNS → ProcurementIngestSns. Trimmed to identity-policy needs. - !If - IsProdAccount - Sid: MealOrderManagerSsm Effect: Allow Action: - ssm:GetParameter - - ssm:GetParameters Resource: - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" - !Ref AWS::NoValue @@ -765,26 +697,17 @@ Resources: Effect: Allow Action: - ses:SendRawEmail - - ses:SendEmail Resource: "*" - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerSns - Effect: Allow - Action: - - sns:Publish - Resource: - - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:site-alerts" - - !Ref AWS::NoValue # ── FURTHER PER-WORKLOAD DATA-PLANE STATEMENTS ────────────────────── - # Floor above + afi-backup-monitor (PLAT-56) + front-integrations - # (PLAT-72) + procurement-ingest (PLAT-86) + seahaven-site (PLAT-91) - # + meal-order-manager (PLAT-70). - # Additional stacks add their own statements here, derived from THEIR - # OWN template, in their own PR, deployed to UPDATE_COMPLETE before - # first workload deploy. WIDENING PATH in the header still governs. + # Floor above + consolidated WorkloadSecrets/DynamoDB/S3 (PLAT-93) + + # procurement remainder + seahaven-site (PLAT-91) + meal-order extras + # (PLAT-70). Prefer extending existing Resource lists over new SIDs. + # Additional stacks add statements here, derived from THEIR OWN + # template, in their own PR, deployed to UPDATE_COMPLETE before first + # workload deploy. Measure SIZE BUDGET before merging. WIDENING PATH + # in the header still governs. # # WHY the floor stayed empty of data-plane (decided 2026-07-30, Adam): # The security win of INFRA-186 comes from DELETION, not enumeration. @@ -795,10 +718,10 @@ Resources: # # The end-state fix for the shared-ceiling residual (one boundary = # every SAM/Terraform workload reaches every other's data plane once - # they land) is per-workload boundaries — tracked as INFRA-187. Do not - # improvise it: both guardrail policies pin ONE literal boundary ARN - # inside StringEquals conditions, and loosening that to a wildcard - # weakens the gate. + # they land) is per-workload boundaries — tracked as PLAT-52 / + # INFRA-187. Do not improvise it: both guardrail policies pin ONE + # literal boundary ARN inside StringEquals conditions, and loosening + # that to a wildcard weakens the gate. # --------------------------------------------------------------------------- # Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped # From f44b88732e9e78995b7ad5d2a23ad4553df8e65a Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:11:48 -0400 Subject: [PATCH 04/17] fix(iam): allow ssm:DescribeParameters for meal-order hcptf roles (#99) --- .../terraform-substrate.template.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 2beb176..0fa6684 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1698,6 +1698,13 @@ Resources: Resource: - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + # aws_ssm_parameter refresh uses DescribeParameters (collection API; + # resource-level parameter ARNs are a silent no-op for this action). + - Sid: RefreshSsmDescribeParameters + Effect: Allow + Action: + - ssm:DescribeParameters + Resource: "*" - Sid: RefreshWafWebAcl Effect: Allow Action: @@ -1866,6 +1873,12 @@ Resources: - ssm:ListTagsForResource Resource: - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + # Collection API required for aws_ssm_parameter refresh/import. + - Sid: MealOrderSsmDescribeParameters + Effect: Allow + Action: + - ssm:DescribeParameters + Resource: "*" - Sid: ReadWafWebAcl Effect: Allow Action: From 1bb5e79ea0fe0bb8516d5ae2d60ddce60ba85a36 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 10 Aug 2026 13:23:56 -0400 Subject: [PATCH 05/17] fix(iam): allow ssm:ListTagsForResource on meal-order plan role --- lib/terraform-substrate/terraform-substrate.template.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 0fa6684..2c11a7d 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1695,6 +1695,8 @@ Resources: Action: - ssm:GetParameter - ssm:GetParameters + # aws_ssm_parameter refresh lists tags on managed parameters. + - ssm:ListTagsForResource Resource: - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" From 6bb2d54814fddc981e7a9e5e3d9a8bf7d3da006f Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 10 Aug 2026 13:46:12 -0400 Subject: [PATCH 06/17] fix(iam): allow PassRole to apigateway for meal-order authorizer --- .../terraform-substrate.template.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 2c11a7d..5199016 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1881,6 +1881,18 @@ Resources: Action: - ssm:DescribeParameters Resource: "*" + # API Gateway Lambda authorizer requires PassRole to + # apigateway.amazonaws.com. Shared HcptfIamManagementPolicy only + # grants PassRole to lambda.amazonaws.com (see IAMPassRole comment). + - Sid: MealOrderPassRoleApiGateway + Effect: Allow + Action: + - iam:PassRole + Resource: + - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/meal-order-manager-*" + Condition: + StringEquals: + "iam:PassedToService": "apigateway.amazonaws.com" - Sid: ReadWafWebAcl Effect: Allow Action: From e10462d25e8e68464049f826f49278898c102efd Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 10 Aug 2026 14:47:30 -0400 Subject: [PATCH 07/17] fix(iam): allow API GW Log Delivery on meal-order apply role --- .../terraform-substrate.template.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 5199016..331d86f 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1791,6 +1791,21 @@ Resources: Action: - logs:DescribeLogGroups Resource: "*" + # HTTP API stage access_log_settings uses the Log Delivery APIs. + # These are account-level (Resource "*"); scoped log-group ARNs + # above are insufficient. Mirrors deploy-substrate + # cloudwatch-logs-management LogDelivery actions. + - Sid: MealOrderApiGwAccessLogDelivery + Effect: Allow + Action: + - logs:CreateLogDelivery + - logs:GetLogDelivery + - logs:UpdateLogDelivery + - logs:DeleteLogDelivery + - logs:ListLogDeliveries + - logs:PutResourcePolicy + - logs:DescribeResourcePolicies + Resource: "*" - Sid: StackBuckets Effect: Allow Action: From b76d0578e070a07c6b529bc39cf8e57c361a0e90 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 10 Aug 2026 14:57:17 -0400 Subject: [PATCH 08/17] fix(iam): drop PutResourcePolicy from meal-order apply role Pre-grant delivery.logs write via MealOrderApiAccessLogResourcePolicy on substrate so the HCP apply role cannot mutate account-wide log resource policies. --- .../terraform-substrate.template.yaml | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 331d86f..c73efcf 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1791,10 +1791,11 @@ Resources: Action: - logs:DescribeLogGroups Resource: "*" - # HTTP API stage access_log_settings uses the Log Delivery APIs. - # These are account-level (Resource "*"); scoped log-group ARNs - # above are insufficient. Mirrors deploy-substrate - # cloudwatch-logs-management LogDelivery actions. + # HTTP API stage access_log_settings uses Log Delivery APIs + # (account-level Resource "*"). PutResourcePolicy is intentionally + # omitted: MealOrderApiAccessLogResourcePolicy below pre-grants + # delivery.logs.amazonaws.com on the meal-order API log group so + # the apply role cannot mutate account-wide log resource policies. - Sid: MealOrderApiGwAccessLogDelivery Effect: Allow Action: @@ -1803,7 +1804,6 @@ Resources: - logs:UpdateLogDelivery - logs:DeleteLogDelivery - logs:ListLogDeliveries - - logs:PutResourcePolicy - logs:DescribeResourcePolicies Resource: "*" - Sid: StackBuckets @@ -1941,3 +1941,36 @@ Resources: - ses:GetIdentityVerificationAttributes - ses:GetSendQuota Resource: "*" + + # Pre-grant delivery.logs write to the meal-order API access log group so + # hcptf-meal-order-manager does not need logs:PutResourcePolicy (account-wide). + # Deployed by CDK CFN exec on substrate update (PLAT-99). + MealOrderApiAccessLogResourcePolicy: + Type: AWS::Logs::ResourcePolicy + Condition: IsProdAccount + Properties: + PolicyName: MealOrderManagerApiAccessLogDelivery + PolicyDocument: !Sub | + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSLogDeliveryWrite", + "Effect": "Allow", + "Principal": { "Service": "delivery.logs.amazonaws.com" }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "arn:aws:logs:us-east-1:${AWS::AccountId}:log-group:/aws/apigateway/meal-order-manager", + "arn:aws:logs:us-east-1:${AWS::AccountId}:log-group:/aws/apigateway/meal-order-manager:*" + ], + "Condition": { + "StringEquals": { + "aws:SourceAccount": "${AWS::AccountId}" + } + } + } + ] + } From 7c43c867e3b794c013a9f863b959face9901c5c6 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 10 Aug 2026 15:42:12 -0400 Subject: [PATCH 09/17] fix(iam): allow execute-api Invoke for meal-order weekly-menu boundary --- lib/deploy-substrate/deploy-substrate.template.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index fa9712e..fb02518 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -682,6 +682,19 @@ Resources: Resource: - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" - !Ref AWS::NoValue + # Weekly-menu OIDC role (githubdeploy-meal-order-manager-weekly-menu) + # invokes IAM-authenticated HttpApi publish routes. Identity policy + # pins the API id; boundary uses method/path only (PLAT-100). + - !If + - IsProdAccount + - Sid: MealOrderManagerExecuteApi + Effect: Allow + Action: + - execute-api:Invoke + Resource: + - !Sub "arn:aws:execute-api:us-east-1:${AWS::AccountId}:*/*/GET/api/publish/settings" + - !Sub "arn:aws:execute-api:us-east-1:${AWS::AccountId}:*/*/POST/api/publish/menu" + - !Ref AWS::NoValue - !If - IsProdAccount - Sid: MealOrderManagerLambdaInvoke From 81cc8eb4f925a28acfa8e104104359b3b2377757 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 10 Aug 2026 15:57:08 -0400 Subject: [PATCH 10/17] fix(iam): consolidate meal-order boundary Sid under PolicySize cap --- .../deploy-substrate.template.yaml | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index fb02518..fc97b8e 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -121,13 +121,15 @@ Description: >- # correctly left untouched. # # SIZE BUDGET: an attached managed policy document is capped at 6,144 characters -# (whitespace excluded). LambdaExecutionBoundary measures 5903 characters across -# 16 statements as of 2026-08-07 (PLAT-93 consolidation after the meal-order -# PolicySize rollback). Measure before widening — len(json.dumps(doc, -# separators=(',',':'))) on the synthesized PolicyDocument with -# ${AWS::AccountId} resolved, and UPDATE THESE TWO NUMBERS in the same edit. +# (whitespace excluded). LambdaExecutionBoundary measures 5986 characters across +# 15 statements as of 2026-08-10 (PLAT-100: MealOrderManagerSsm+LambdaInvoke +# consolidated with execute-api:Invoke; SES kept separate because Resource:"*" +# must not share a statement with execute-api:Invoke. Was 5903/16 after PLAT-93). +# Measure before widening — len(json.dumps(doc, separators=(',',':'))) on the +# synthesized PolicyDocument with ${AWS::AccountId} resolved, and UPDATE THESE +# TWO NUMBERS in the same edit. # -# Headroom is 241 characters. Statement consolidation (shared WorkloadSecrets / +# Headroom is 158 characters. Statement consolidation (shared WorkloadSecrets / # WorkloadDynamoDB / extended WorkloadS3 Resource lists; no new action wildcards) # is the only remaining lever short of per-workload boundaries. If the budget # tightens again, the end-state fix is per-workload boundaries @@ -670,40 +672,30 @@ Resources: - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/*" - !Ref AWS::NoValue - # ── meal-order-manager (PLAT-70) — statements not covered above ───── + # ── meal-order-manager (PLAT-70 / PLAT-100) — not covered above ───── # Secrets → WorkloadSecrets; DynamoDB → WorkloadDynamoDB; S3 → WorkloadS3; - # SNS → ProcurementIngestSns. Trimmed to identity-policy needs. + # SNS → ProcurementIngestSns. SSM + Lambda Invoke + execute-api share + # one Sid (scoped Resources only) so PolicySize stays under 6,144 — + # a standalone execute-api Sid is ~243 chars against 241 headroom and + # would fail UPDATE with LimitExceeded. SES stays in its own Sid: + # Resource:"*" must not share a statement with execute-api:Invoke + # (that would allow Invoke on every API in the account). + # Weekly-menu OIDC identity policy pins the API id; boundary pins + # method/path only. - !If - IsProdAccount - - Sid: MealOrderManagerSsm + - Sid: MealOrderManager Effect: Allow Action: - ssm:GetParameter - Resource: - - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" - - !Ref AWS::NoValue - # Weekly-menu OIDC role (githubdeploy-meal-order-manager-weekly-menu) - # invokes IAM-authenticated HttpApi publish routes. Identity policy - # pins the API id; boundary uses method/path only (PLAT-100). - - !If - - IsProdAccount - - Sid: MealOrderManagerExecuteApi - Effect: Allow - Action: + - lambda:InvokeFunction - execute-api:Invoke Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:meal-order-manager-*" - !Sub "arn:aws:execute-api:us-east-1:${AWS::AccountId}:*/*/GET/api/publish/settings" - !Sub "arn:aws:execute-api:us-east-1:${AWS::AccountId}:*/*/POST/api/publish/menu" - !Ref AWS::NoValue - - !If - - IsProdAccount - - Sid: MealOrderManagerLambdaInvoke - Effect: Allow - Action: - - lambda:InvokeFunction - Resource: - - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:meal-order-manager-*" - - !Ref AWS::NoValue - !If - IsProdAccount - Sid: MealOrderManagerSes From 0f84d7808b69ed5de8d1f46cdc4f34d239d50e84 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Thu, 13 Aug 2026 16:47:53 -0400 Subject: [PATCH 11/17] feat(iam): add per-workload lambda execution boundaries Shared seahaven-lambda-execution-boundary stays unchanged for live roles. New named policies plus an enumerated StringEquals allow-list unblock the next PLAT-71 widen without growing the 6144-character shared document. --- README.md | 68 +- .../deploy-substrate.template.yaml | 644 ++++++++++++++---- .../terraform-substrate.template.yaml | 58 +- 3 files changed, 593 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 57c4fdc..4113aa9 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,9 @@ 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 `seahaven-lambda-execution-boundary` permissions boundary (legacy + shared ceiling for roles not yet retargeted) plus per-workload policies + `seahaven-lambda-execution-boundary-` (PLAT-52), - 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`, @@ -140,17 +141,22 @@ of truth for mgmt (328440206208) until its stacks migrate out. **The two copies are no longer at parity, and the old "edit both files" rule no longer applies uniformly.** Under INFRA-186, `seahaven-lambda-execution-boundary` -in *this* copy was reduced to a fleet-wide floor for prod and dev (where -boundary usage was 0, so no live Lambda could break): CloudWatch Logs write on -`/aws/lambda*`, log-group describe, X-Ray, and ENI lifecycle — nothing else. -Each migrating stack adds its own data-plane statements, derived from its own -template, in its own PR (per-workload boundaries are the INFRA-187 end state). -mgmt's copy keeps the account-wide wildcards pending its own separately -validated rollout across 26 live boundary-carrying roles. So: **the boundary -resource is deliberately divergent**; every *other* substrate resource -(`github-cfn-execution-role`, `seahaven-cfn-exec-iam-management`) is still -expected to change in both files together. The template's provenance header -records which is which — read it before assuming either parity or divergence. +in *this* copy was reduced to a fleet-wide floor for prod and dev; later +migrations packed per-workload data plane back into that shared document until +it hit the 6,144-character cap (PLAT-93 / PLAT-100). PLAT-52 adds per-workload +policies `seahaven-lambda-execution-boundary-` (floor plus that +stack's data plane) and switches both prod/dev guardrails to a StringEquals +allow-list of the shared ARN plus each per-workload ARN. The shared document +is left unchanged until live roles retarget. Mgmt's copy keeps the +account-wide wildcards and a **single-ARN** pin pending its own separately +validated rollout across 26 live boundary-carrying roles (PLAT-51). So: **the +boundary resource is deliberately divergent**, and the guardrail +`iam:PermissionsBoundary` condition **cardinality** is also divergent +(enumerated list here, scalar on mgmt). Do not weaken mgmt to ArnLike. Every +*other* substrate resource (`github-cfn-execution-role`, +`seahaven-cfn-exec-iam-management` Sid/Action/Resource sets) is still expected +to change in both files together. The template's provenance header records +which is which — read it before assuming either parity or divergence. Per-repo `githubdeploy-*` deploy roles are deliberately NOT part of the substrate — they are provisioned per repo at migration/onboarding time @@ -159,7 +165,9 @@ to it. **Escalation controls on `github-cfn-execution-role`.** Every `iam:CreateRole`, `AttachRolePolicy` and `PutRolePolicy` is conditioned on the target carrying -`seahaven-lambda-execution-boundary`. That condition alone is not sufficient, +one of the enumerated `seahaven-lambda-execution-boundary` ARNs (the shared +policy plus each `seahaven-lambda-execution-boundary-`). That +condition alone is not sufficient, so the attached `seahaven-cfn-exec-iam-management` managed policy also carries three explicit Deny statements: @@ -214,9 +222,10 @@ shared account-level plumbing: `aws.workload.identity`; Retain — it is the federation anchor for every future `hcptf-*` role), - the `seahaven-hcptf-iam-management` guardrail policy: the boundary-gated - IAM role lifecycle (conditioned on `seahaven-lambda-execution-boundary`, - owned by the deploy-substrate stack — hence the explicit stack dependency - in `bin/app.ts`) plus the `DenyBoundaryTampering` / `DenyBoundaryPolicyEdit` + IAM role lifecycle (conditioned on the enumerated + `seahaven-lambda-execution-boundary` allow-list owned by the + deploy-substrate stack — hence the explicit stack dependency in + `bin/app.ts`) plus the `DenyBoundaryTampering` / `DenyBoundaryPolicyEdit` / `DenySelfMutation` backstops. **This policy derives from `seahaven-cfn-exec-iam-management` but is @@ -281,14 +290,16 @@ this template rather than inventing new IAM shapes. (or `:apply`). Exact `StringEquals` only — never `StringLike`, never a wildcarded `run_phase` (a speculative PR plan must never hold write credentials). **If the stack creates Lambda execution roles, this same PR - must also widen `seahaven-lambda-execution-boundary`** per the WIDENING - PATH in `lib/deploy-substrate/deploy-substrate.template.yaml` with the - **exact** secret ARNs from step 1 (no `secret:afi-*` patterns): the - guardrail forces every Terraform-created role to carry that boundary, and - it is a fleet-wide floor with zero data-plane permissions until widened — - an unwidened migration deploys green, then every data-plane call is denied + must also add `seahaven-lambda-execution-boundary-`** per the + WIDENING PATH in `lib/deploy-substrate/deploy-substrate.template.yaml` + (floor plus that stack's data plane, **exact** secret ARNs from step 1, + no `secret:afi-*` patterns) **and** append that policy's ARN to both + guardrail StringEquals allow-lists. Do not add data-plane to the shared + `seahaven-lambda-execution-boundary` document. The guardrail forces every + Terraform-created role to carry a listed boundary; an unlisted or + floor-only boundary deploys green, then every data-plane call is denied at first invoke and async/DLQ writes are discarded silently. IAM roles and - boundary widenings = mandatory cross-family review + + boundary policies = mandatory cross-family review + `/sh-security-review` on the diff. 3a. **Plan role (required for every stack):** attach @@ -321,8 +332,9 @@ this template rather than inventing new IAM shapes. `list-attached-role-policies`; trust subs match the live org/project/workspace names byte-for-byte; simulate the apply role against a `hcptf-*` ARN (expect `explicitDeny` from `DenySelfMutation`) and against - a normal stack role name (expect `allowed`); and if step 3 widened the - boundary, confirm the deployed default version carries the stack's + a normal stack role name (expect `allowed`); and if step 3 added a + per-workload boundary, confirm the deployed default version of + `seahaven-lambda-execution-boundary-` carries the stack's data-plane statements (`aws iam get-policy-version`) — role verification alone never checks boundary content. Mechanical template↔deployed policy reconcile as for other substrate policies. @@ -332,7 +344,9 @@ this template rather than inventing new IAM shapes. trust is pinned per workspace, so a shared set breaks every other workspace. Auto-apply stays OFF until the stack is sealed. 6. **App Terraform PR:** every `aws_iam_role` sets `path = "/tf-managed/"` and - the boundary; package Lambda/layer zips via an account artifact S3 bucket + `permissions_boundary` to that stack's + `seahaven-lambda-execution-boundary-` ARN (not the shared name, + once the per-workload policy exists); package Lambda/layer zips via an account artifact S3 bucket and `aws_s3_object` `content_base64` (HCP plan and apply run on separate workers and do not share local `archive_file` paths — see `afi-backup-monitor/terraform/artifacts.tf`); functions `depends_on` their diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index fc97b8e..f5723ed 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -41,13 +41,19 @@ Description: >- # DenyBoundaryPolicyEdit scope were then ported back here, so the two # copies' GUARDRAIL statement sets were reconciled as of that date. # SamCfnIamManagementPolicy and SamCfnExecutionRole remain at parity on -# their IAM STATEMENT SETS across the two files and MUST still be changed -# together. Parity covers statements, not surrounding comments — a comment -# may diverge where it describes boundary content, which now differs -# between the files. The only functional delta -# between them is the DependsOn line above, which is ordering, not -# permission. LambdaExecutionBoundary is NO LONGER byte-identical — see -# DELIBERATE DIVERGENCE below. +# their IAM STATEMENT SETS across the two files EXCEPT the +# iam:PermissionsBoundary StringEquals VALUE. Prod/dev (this file) now +# enumerates the shared ARN plus each seahaven-lambda-execution-boundary- +# ARN (PLAT-52). Mgmt's .github copy keeps the unsuffixed single ARN — +# those per-workload policies do not exist in mgmt, and adding them to +# mgmt's allow-list would be a no-op that reads as false parity. Do not +# weaken mgmt to ArnLike. Sid names, Deny statements, and every other +# Action/Resource still change in both files together. Parity covers +# statements, not surrounding comments — a comment may diverge where it +# describes boundary content, which now differs between the files. The +# only other functional delta is the DependsOn line above, which is +# ordering, not permission. LambdaExecutionBoundary is NO LONGER +# byte-identical — see DELIBERATE DIVERGENCE below. # # DELIBERATE DIVERGENCE — LambdaExecutionBoundary (INFRA-186, 2026-07-30) # The parity rule above is SCOPED, not global. LambdaExecutionBoundary in THIS @@ -108,38 +114,51 @@ Description: >- # ready to roll back — and is explicitly OUT OF SCOPE of INFRA-186. Until that # lands, the two copies stay divergent and that is the intended state. # -# COUPLING: the boundary's ManagedPolicyName and ARN are UNCHANGED and must stay -# unchanged. Four Conditions in SamCfnIamManagementPolicy below, and four more in -# HcptfIamManagementPolicy in lib/terraform-substrate/terraform-substrate.template.yaml, -# pin arn:aws:iam:::policy/seahaven-lambda-execution-boundary by literal -# string inside StringEquals iam:PermissionsBoundary. A rename fails SILENTLY — an -# IAM condition naming a non-existent policy simply never matches, so the -# escalation control would evaporate rather than error — and would additionally -# force a CloudFormation REPLACEMENT that any role carrying the boundary would -# block. INFRA-186 is a CONTENT-ONLY change for exactly this reason; -# terraform-substrate.template.yaml already records that expectation and is -# correctly left untouched. +# COUPLING (PLAT-52): the SHARED policy's ManagedPolicyName and ARN +# (seahaven-lambda-execution-boundary) stay unchanged until +# PermissionsBoundaryUsageCount is 0 in the account. Four Conditions in +# SamCfnIamManagementPolicy below, and four more in HcptfIamManagementPolicy +# in lib/terraform-substrate/terraform-substrate.template.yaml, pin an +# enumerated StringEquals list of acceptable boundary ARNs: the shared ARN +# plus each seahaven-lambda-execution-boundary- ARN. The two files' +# lists MUST match (mechanical sorted-JSON compare). A rename of the SHARED +# policy still fails SILENTLY — an IAM condition naming a non-existent policy +# simply never matches, so the escalation control evaporates rather than +# error — and would additionally force a CloudFormation REPLACEMENT that any +# role carrying the boundary would block. Adding a workload is a NEW named +# ManagedPolicy in this file AND one ARN appended to both allow-lists. Do +# not use ArnLike on seahaven-lambda-execution-boundary-*: githubdeploy-seahaven-org-baseline +# can CreatePolicy via CFN, so a conforming-name policy would become an +# acceptable ceiling without touching the eight pin sites. # # SIZE BUDGET: an attached managed policy document is capped at 6,144 characters -# (whitespace excluded). LambdaExecutionBoundary measures 5986 characters across -# 15 statements as of 2026-08-10 (PLAT-100: MealOrderManagerSsm+LambdaInvoke -# consolidated with execute-api:Invoke; SES kept separate because Resource:"*" -# must not share a statement with execute-api:Invoke. Was 5903/16 after PLAT-93). -# Measure before widening — len(json.dumps(doc, separators=(',',':'))) on the -# synthesized PolicyDocument with ${AWS::AccountId} resolved, and UPDATE THESE -# TWO NUMBERS in the same edit. +# (whitespace excluded). Measure with len(json.dumps(doc, separators=(',',':'))) +# on the synthesized PolicyDocument with ${AWS::AccountId} resolved, and UPDATE +# THE NUMBERS in the same edit. # -# Headroom is 158 characters. Statement consolidation (shared WorkloadSecrets / -# WorkloadDynamoDB / extended WorkloadS3 Resource lists; no new action wildcards) -# is the only remaining lever short of per-workload boundaries. If the budget -# tightens again, the end-state fix is per-workload boundaries -# (seahaven-lambda-execution-boundary-), which also resolves the -# shared-ceiling residual — tracked as PLAT-52 / INFRA-187, do not improvise it. -# CRITICAL: unlike the 2026-07-27 inline-limit incident, -# there is NO restructure available when this cap is reached — a role has exactly -# ONE permissions boundary, so statements cannot be spilled into a second attached -# managed policy. At the cap the only levers are prefix consolidation and dropping -# unused actions. Do not introduce dynamodb:* / s3:* / ses:Send* to reclaim space. +# Shared LambdaExecutionBoundary (legacy ceiling; do not widen): 5986 +# characters / 15 statements as of 2026-08-10 (PLAT-100). Headroom 158. +# Leave it unchanged until live roles retarget (PLAT-52 phase 2). +# +# Per-workload policies (floor + own data plane). Compact sizes recorded +# after synth (prod, ${AWS::AccountId}=011934824531): +# seahaven-lambda-execution-boundary-afi-backup-monitor: 952 / 5 statements +# seahaven-lambda-execution-boundary-front-integrations: 1532 / 6 statements +# seahaven-lambda-execution-boundary-procurement-ingest: 3977 / 11 statements +# seahaven-lambda-execution-boundary-meal-order-manager: 2380 / 10 statements +# seahaven-lambda-execution-boundary-seahaven-site: 1159 / 6 statements +# Dev copies are floor-only (691 / 4) via IsProdAccount. +# +# Guardrail PolicyDocuments also cap at 6,144. Each extra allow-list ARN +# is copied into four Sids in EACH of SamCfnIamManagementPolicy and +# HcptfIamManagementPolicy. Measured after this list (6 ARNs): +# seahaven-cfn-exec-iam-management: 4571 / 10 statements (1573 headroom) +# seahaven-hcptf-iam-management: 4693 / 10 statements (1451 headroom) +# +# CRITICAL: a role has exactly ONE permissions boundary, so statements cannot +# be spilled into a second attached managed policy. Do not introduce +# dynamodb:* / s3:* / ses:Send* to reclaim space. Do not add new data-plane +# to the shared document; create seahaven-lambda-execution-boundary-. # # This template is deployed via lib/deploy-substrate-stack.ts # (cloudformation-include) as stack seahaven-deploy-substrate, once per member @@ -192,32 +211,22 @@ Resources: # intersection of the role's own policies and this boundary, so a misconfigured # SAM role can never exceed what is listed here. # - # SCOPING RULE (INFRA-186, 2026-07-30). This boundary is the FLEET-WIDE FLOOR - # ONLY: what every Lambda execution role needs regardless of workload. It - # carries NO per-workload data-plane statements — each migrating stack adds its - # own, from its own template, in its own PR (see the note on the boundary - # resource and the WIDENING PATH below). It was previously a deliberate SUPERSET - # with account-wide wildcards (table/*, secret:*, sqs :*, function:*, - # parameter/*, and an s3:::*- pattern that was a name-suffix filter, - # not an ownership check). That trade-off was made when the only account - # carrying this policy hosted nothing but the five SAM stacks. It does not - # survive multi-tenancy: seahaven-prod now hosts CDK-owned tenants - # (proposal-system, procurement-ingest, workorder-ingest) whose tables, buckets, - # secrets and queues those wildcards reached, and whose Lambdas carry NO - # permissions boundary at all — making function:* a boundary-escape primitive. - # - # Tightening here carries ZERO runtime risk and was sequenced deliberately: - # PermissionsBoundaryUsageCount is 0 in BOTH accounts this template deploys to - # (seahaven-prod 011934824531 and seahaven-dev 710827005802, verified - # 2026-07-30), so no live Lambda can break. A boundary that is slightly TOO - # TIGHT is recoverable here — the migrating stack widens it in its own PR before - # its first deploy — whereas leaving it loose perpetuates the exposure. Prefer - # tighter; the widening path is below. + # SCOPING RULE (PLAT-52 phase 1, 2026-08-13). New workloads get their own + # ManagedPolicy seahaven-lambda-execution-boundary-: the fleet-wide + # floor (CloudWatchLogsWrite / CloudWatchLogsDescribe / XRay / Ec2Eni) plus + # that workload's data plane, derived from ITS OWN template. Do not add new + # data-plane statements to the shared seahaven-lambda-execution-boundary + # document — it is the legacy ceiling for roles not yet retargeted and stays + # unchanged until PermissionsBoundaryUsageCount is 0. INFRA-186 reduced this + # copy to a floor and later migrations packed data plane back into it under + # the 6,144-character cap (PLAT-93 / PLAT-100). Per-workload policies are + # the escape hatch from that cap and from the shared-ceiling residual. # # A resource pattern that genuinely CANNOT be scoped keeps its wildcard WITH a # written justification on the statement: CloudWatchLogsDescribe, XRay and # Ec2Eni name runtime-created resources or use actions AWS authorises against - # "*" regardless of the ARN supplied. Do not "tighten" those. + # "*" regardless of the ARN supplied. Do not "tighten" those. Copy them into + # every per-workload policy (a role takes exactly one boundary). # # Every "verified " annotation in this file is a POINT-IN-TIME # observation, not live state. Re-validate (usage counts, log-group CMK state, @@ -225,20 +234,22 @@ Resources: # future change. # # WIDENING PATH — read this before migrating a stack into prod or dev. - # The boundary is never widened by the person who hits the AccessDenied. It is - # widened by the migrating stack's owner, in THIS repo, BEFORE the workload's + # The ceiling is never widened by the person who hits the AccessDenied. It is + # created by the migrating stack's owner, in THIS repo, BEFORE the workload's # first deploy into the target account: - # 0. PRECONDITION — check the managed-policy VERSION budget BEFORE merging: - # max 5 versions, both - # accounts are on v1 today. Every widening (PolicyDocument edit) burns - # one. Description, ManagedPolicyName and Path are REPLACEMENT - # properties per the CFN resource reference — CloudFormation cannot - # replace a custom-named policy, so a Description-only edit FAILS the - # stack update, and the error's suggested remedy (rename) is exactly - # the forbidden rename in the COUPLING note above. Never edit those - # three properties. Delete the oldest non-default version if at 5: + # 0. Create AWS::IAM::ManagedPolicy seahaven-lambda-execution-boundary- + # in this file (floor via the YAML anchors on AfiBackupMonitorBoundary, + # plus that stack's data plane). Do NOT edit the shared + # LambdaExecutionBoundary PolicyDocument. Description, ManagedPolicyName + # and Path on an existing named policy are REPLACEMENT properties — + # CloudFormation cannot replace a custom-named policy, so a + # Description-only edit FAILS the stack update, and the error's suggested + # remedy (rename) is the forbidden rename in the COUPLING note. Never + # edit those three properties on a policy that already exists. New + # policies start at v1. Per-policy version budget (max 5) applies when + # later editing that workload's document: # aws iam list-policy-versions --policy-arn \ - # arn:aws:iam:::policy/seahaven-lambda-execution-boundary + # arn:aws:iam:::policy/seahaven-lambda-execution-boundary- # aws iam delete-policy-version --version-id v ... # 1. Derive the workload's needs from ITS OWN TEMPLATE — open the stack's # template.yaml and read the actual IAM policy statements. The @@ -246,29 +257,34 @@ Resources: # the /sh-security-review pass on 2026-07-30 found SIX places where it was # incomplete or simply invented a resource name, three of which would have # failed silently. Update that block in the same edit with what you find. - # 2. Add the workload's statements. Group by service so a second workload can - # extend a Resource list rather than duplicate an action list (~250 - # characters for zero new actions; an extra ARN costs ~60). Check for the - # SILENT classes specifically: a denied SQS destination/DLQ write discards - # the async event with no error and no alarm; a denied scheduler call may - # sit behind a bare except; a denied KMS decrypt for env-var encryption - # fails at cold-start INIT; any CMK-encrypted resource needs the - # matching kms:ViaService principal, not just the kms action; and a - # function using LoggingConfig with a custom log-group name outside - # /aws/lambda* silently loses ALL logs — add a scoped logs statement - # for the custom group or keep the default group name. - # 3. Measure. See SIZE BUDGET in the header. There is NO escape hatch. - # 4. Both review gates run and neither discharges the other: the GPT-4.1 - # cross-family review against the real diff, and /sh-security-review + # 2. Add the workload's statements to ITS policy. Do not pack them into + # another workload's Resource lists. Check for the SILENT classes: + # a denied SQS destination/DLQ write discards the async event with no + # error and no alarm; a denied scheduler call may sit behind a bare + # except; a denied KMS decrypt for env-var encryption fails at cold-start + # INIT; any CMK-encrypted resource needs the matching kms:ViaService + # principal, not just the kms action; and a function using LoggingConfig + # with a custom log-group name outside /aws/lambda* silently loses ALL + # logs — add a scoped logs statement for the custom group or keep the + # default group name. + # 3. Measure THAT policy against 6144 (SIZE BUDGET). Also measure both + # guardrail PolicyDocuments after step 4 — each new ARN is copied into + # four Sids in each guardrail. + # 4. Append the new ARN to BOTH allow-lists (SamCfnIamManagementPolicy in + # this file AND HcptfIamManagementPolicy in + # terraform-substrate.template.yaml). The lists must match. Do not use + # ArnLike. Both review gates run and neither discharges the other: the + # GPT-4.1 cross-family review against the real diff, and /sh-security-review # (IaC/IAM is on the mandatory surface). CLI down = review outstanding. - # 5. Merge and let CI deploy deploy-substrate-prod / deploy-substrate-dev to - # UPDATE_COMPLETE, THEN deploy the workload stack. + # 5. Merge and let CI deploy deploy-substrate-prod / deploy-substrate-dev + # (and terraform-substrate) to UPDATE_COMPLETE, THEN deploy the workload + # with PermissionsBoundary set to THIS stack's ARN (not the shared name). # ORDERING IS NOT ENFORCED BY CLOUDFORMATION AND THIS IS THE MOST IMPORTANT - # SENTENCE HERE: the workload's deploy SUCCEEDS even against a stale boundary, - # because seahaven-cfn-exec-iam-management's gate checks that the boundary ARN - # is attached, never its contents. The failure surfaces later, at first invoke, - # as AccessDenied. A stale boundary is a silent deploy-time pass and a loud - # production failure. + # SENTENCE HERE: the workload's deploy SUCCEEDS even against a stale or + # missing-content boundary, because the guardrail gates check that a listed + # boundary ARN is attached, never its contents. The failure surfaces later, + # at first invoke, as AccessDenied. A stale boundary is a silent deploy-time + # pass and a loud production failure. # # CONSIDERED AND REJECTED: a Deny statement reserving the seahaven-* namespace. # With the Allow set reduced to the fleet-wide floor it is fully redundant (verified @@ -705,28 +721,397 @@ Resources: Resource: "*" - !Ref AWS::NoValue - # ── FURTHER PER-WORKLOAD DATA-PLANE STATEMENTS ────────────────────── - # Floor above + consolidated WorkloadSecrets/DynamoDB/S3 (PLAT-93) + - # procurement remainder + seahaven-site (PLAT-91) + meal-order extras - # (PLAT-70). Prefer extending existing Resource lists over new SIDs. - # Additional stacks add statements here, derived from THEIR OWN - # template, in their own PR, deployed to UPDATE_COMPLETE before first - # workload deploy. Measure SIZE BUDGET before merging. WIDENING PATH - # in the header still governs. - # - # WHY the floor stayed empty of data-plane (decided 2026-07-30, Adam): - # The security win of INFRA-186 comes from DELETION, not enumeration. - # Removing the account-wide secret:*, table/*, function:* and sqs:* - # wildcards is what closes the amplifier. Per-workload prefixes add no - # security; they exist only to keep a workload FUNCTIONAL once it - # arrives. WIDENING IS THE SAFE DIRECTION. - # - # The end-state fix for the shared-ceiling residual (one boundary = - # every SAM/Terraform workload reaches every other's data plane once - # they land) is per-workload boundaries — tracked as PLAT-52 / - # INFRA-187. Do not improvise it: both guardrail policies pin ONE - # literal boundary ARN inside StringEquals conditions, and loosening - # that to a wildcard weakens the gate. + # ── FURTHER PER-WORKLOAD DATA-PLANE ──────────────────────────────── + # Do not add statements here. Create seahaven-lambda-execution-boundary- + # below and append its ARN to both guardrail allow-lists (WIDENING PATH). + # This shared document stays unchanged until live roles retarget + # (PLAT-52 phase 2) and PermissionsBoundaryUsageCount reaches 0. + # --------------------------------------------------------------------------- + # Per-workload Lambda execution boundaries (PLAT-52 phase 1) + # + # Each policy is the fleet floor plus that workload's data plane, split from + # the shared document above without editing it. Live roles keep the shared + # ARN until app-repo retargets. Guardrail StringEquals lists include both. + # Floor statements are YAML-anchored on AfiBackupMonitorBoundary; later + # policies alias them. Floor rationale lives on LambdaExecutionBoundary. + # Prod-only data plane stays behind IsProdAccount (same as the shared copy). + # --------------------------------------------------------------------------- + AfiBackupMonitorBoundary: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: seahaven-lambda-execution-boundary-afi-backup-monitor + Description: >- + Per-workload permissions boundary for afi-backup-monitor (PLAT-52). + Floor plus exact prod secret ARNs. Shared seahaven-lambda-execution-boundary + remains the live-role ceiling until app retarget. + PolicyDocument: + Version: "2012-10-17" + Statement: + - &lambdaBoundaryFloorLogsWrite + Sid: CloudWatchLogsWrite + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + - logs:DescribeLogStreams + Resource: + - !Sub "arn:aws:logs:us-east-1:${AWS::AccountId}:log-group:/aws/lambda*" + - &lambdaBoundaryFloorLogsDescribe + Sid: CloudWatchLogsDescribe + Effect: Allow + Action: + - logs:DescribeLogGroups + Resource: "*" + - &lambdaBoundaryFloorXRay + Sid: XRay + Effect: Allow + Action: + - xray:PutTraceSegments + - xray:PutTelemetryRecords + Resource: "*" + - &lambdaBoundaryFloorEc2Eni + Sid: Ec2Eni + Effect: Allow + Action: + - ec2:CreateNetworkInterface + - ec2:DescribeNetworkInterfaces + - ec2:DeleteNetworkInterface + - ec2:DescribeSubnets + - ec2:DescribeSecurityGroups + - ec2:DescribeVpcs + Resource: "*" + - !If + - IsProdAccount + - Sid: AfiBackupMonitorSecrets + Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - arn:aws:secretsmanager:us-east-1:011934824531:secret:afi-api-key-w0E02a + - arn:aws:secretsmanager:us-east-1:011934824531:secret:afi-slack-webhook-T4oR3G + - !Ref AWS::NoValue + + FrontIntegrationsBoundary: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: seahaven-lambda-execution-boundary-front-integrations + Description: >- + Per-workload permissions boundary for front-integrations (PLAT-52). + Floor plus exact prod secrets and front-sla-alerts. Shared policy + remains the live-role ceiling until app retarget. + PolicyDocument: + Version: "2012-10-17" + Statement: + # Floor aliases — edit the &lambdaBoundaryFloor* anchors on + # AfiBackupMonitorBoundary only; do not inline a divergent copy. + - *lambdaBoundaryFloorLogsWrite + - *lambdaBoundaryFloorLogsDescribe + - *lambdaBoundaryFloorXRay + - *lambdaBoundaryFloorEc2Eni + - !If + - IsProdAccount + - Sid: FrontIntegrationsSecrets + Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/front-api-token-UXKv0U + - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/slack-bot-token-giGfA7 + - arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/google-service-account-dqv3Bo + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: FrontIntegrationsDynamoDB + 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/front-sla-alerts" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/front-sla-alerts/index/*" + - !Ref AWS::NoValue + + ProcurementIngestBoundary: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: seahaven-lambda-execution-boundary-procurement-ingest + Description: >- + Per-workload permissions boundary for procurement-ingest including + bundled workorder-ingest data plane (PLAT-86 grouping, PLAT-52 split). + Shared policy remains the live-role ceiling until app retarget. + PolicyDocument: + Version: "2012-10-17" + Statement: + # Floor aliases — edit the &lambdaBoundaryFloor* anchors on + # AfiBackupMonitorBoundary only; do not inline a divergent copy. + - *lambdaBoundaryFloorLogsWrite + - *lambdaBoundaryFloorLogsDescribe + - *lambdaBoundaryFloorXRay + - *lambdaBoundaryFloorEc2Eni + - !If + - IsProdAccount + - Sid: ProcurementIngestSecrets + Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - arn:aws:secretsmanager:us-east-1:011934824531:secret:procurement-ingest/web-ui-auth-token-ApAMmr + - arn:aws:secretsmanager:us-east-1:011934824531:secret:workorder-ingest/shoc-webhook-hmac-puYTcB + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: ProcurementIngestDynamoDB + Effect: Allow + Action: + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:UpdateItem + - dynamodb:DeleteItem + - dynamodb:Query + - dynamodb:Scan + - dynamodb:BatchGetItem + - dynamodb:BatchWriteItem + - dynamodb:DescribeTable + - dynamodb:ConditionCheckItem + - dynamodb:GetRecords + - dynamodb:GetShardIterator + - dynamodb:DescribeStream + Resource: + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: ProcurementIngestS3 + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:PutObject* + - s3:DeleteObject* + - s3:AbortMultipartUpload + Resource: + - !Sub "arn:aws:s3:::po-ingest-emails-${AWS::AccountId}" + - !Sub "arn:aws:s3:::po-ingest-emails-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::workorder-ingest-emails-${AWS::AccountId}" + - !Sub "arn:aws:s3:::workorder-ingest-emails-${AWS::AccountId}/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: ProcurementIngestSqs + 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}:po-ingest-*" + - !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:WorkorderIngestStack-*" + - !Sub "arn:aws:sqs:us-east-1:${AWS::AccountId}:workorder-shoc-emitter-*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: ProcurementIngestKms + Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:GenerateDataKey* + - kms:ReEncrypt* + Resource: + - arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12 + - arn:aws:kms:us-east-1:011934824531:key/d10fd1f0-a61a-4405-8568-85e9fd11ba18 + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: ProcurementIngestBedrock + Effect: Allow + Action: + - bedrock:InvokeModel + - bedrock:InvokeModelWithResponseStream + Resource: + - !Sub "arn:aws:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0" + - arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 + - arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 + - arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: ProcurementIngestSns + Effect: Allow + Action: + - sns:Publish + Resource: + - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:site-alerts" + - !Ref AWS::NoValue + + MealOrderManagerBoundary: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: seahaven-lambda-execution-boundary-meal-order-manager + Description: >- + Per-workload permissions boundary for meal-order-manager (PLAT-52). + Floor plus secrets, orders table, form/reports buckets, site-alerts, + SSM/invoke/execute-api, and SES. Shared policy remains the live-role + ceiling until app retarget. + PolicyDocument: + Version: "2012-10-17" + Statement: + # Floor aliases — edit the &lambdaBoundaryFloor* anchors on + # AfiBackupMonitorBoundary only; do not inline a divergent copy. + - *lambdaBoundaryFloorLogsWrite + - *lambdaBoundaryFloorLogsDescribe + - *lambdaBoundaryFloorXRay + - *lambdaBoundaryFloorEc2Eni + - !If + - IsProdAccount + - Sid: MealOrderManagerSecrets + Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: + - arn:aws:secretsmanager:us-east-1:011934824531:secret:meal-order-manager/slack-bot-token-ZGmSGw + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerDynamoDB + 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/meal-order-manager-orders" + - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/meal-order-manager-orders/index/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerS3 + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:PutObject* + - s3:DeleteObject* + - s3:AbortMultipartUpload + Resource: + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-form-${AWS::AccountId}/*" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}" + - !Sub "arn:aws:s3:::meal-order-manager-reports-${AWS::AccountId}/*" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerSns + Effect: Allow + Action: + - sns:Publish + Resource: + - !Sub "arn:aws:sns:us-east-1:${AWS::AccountId}:site-alerts" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManager + Effect: Allow + Action: + - ssm:GetParameter + - lambda:InvokeFunction + - execute-api:Invoke + Resource: + - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/meal-order-manager/*" + - !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:meal-order-manager-*" + - !Sub "arn:aws:execute-api:us-east-1:${AWS::AccountId}:*/*/GET/api/publish/settings" + - !Sub "arn:aws:execute-api:us-east-1:${AWS::AccountId}:*/*/POST/api/publish/menu" + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: MealOrderManagerSes + Effect: Allow + Action: + - ses:SendRawEmail + Resource: "*" + - !Ref AWS::NoValue + + SeahavenSiteBoundary: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: seahaven-lambda-execution-boundary-seahaven-site + Description: >- + Per-workload permissions boundary for githubdeploy-seahaven-site + (PLAT-91 / PLAT-52). Floor plus origin S3 and CloudFront invalidate. + Not a Lambda; hcptf guardrail still requires a listed boundary on + /tf-managed/ roles. Shared policy remains the live-role ceiling + until app retarget. + PolicyDocument: + Version: "2012-10-17" + Statement: + # Floor aliases — edit the &lambdaBoundaryFloor* anchors on + # AfiBackupMonitorBoundary only; do not inline a divergent copy. + - *lambdaBoundaryFloorLogsWrite + - *lambdaBoundaryFloorLogsDescribe + - *lambdaBoundaryFloorXRay + - *lambdaBoundaryFloorEc2Eni + - !If + - IsProdAccount + - Sid: SeahavenSiteOriginS3 + Effect: Allow + Action: + - s3:GetObject + - s3:PutObject + - s3:DeleteObject + - s3:GetObjectTagging + - s3:PutObjectTagging + - s3:ListBucket + - s3:GetBucketLocation + Resource: + - arn:aws:s3:::seahaven-site-prod + - arn:aws:s3:::seahaven-site-prod/* + - !Ref AWS::NoValue + - !If + - IsProdAccount + - Sid: SeahavenSiteCloudFrontInvalidate + Effect: Allow + Action: + - cloudfront:CreateInvalidation + - cloudfront:GetInvalidation + Resource: + - !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/*" + - !Ref AWS::NoValue + # --------------------------------------------------------------------------- # Shared CloudFormation execution role (SAM stacks) — INFRA-97 scoped # @@ -736,9 +1121,10 @@ Resources: # # 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 + # conditioned on iam:PermissionsBoundary StringEquals an enumerated list + # of acceptable boundary ARNs (shared seahaven-lambda-execution-boundary + # plus each seahaven-lambda-execution-boundary-, PLAT-52). + # That condition is what prevents the CFN execution role from minting an # unconstrained admin role. # # SAM RolePath deviation note @@ -1221,12 +1607,12 @@ Resources: # 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 (in - # THIS file the fleet-wide floor plus per-migration widenings; - # in mgmt's copy the five SAM stacks' service wildcards). + # conditioned on iam:PermissionsBoundary StringEquals an enumerated + # list: the shared seahaven-lambda-execution-boundary ARN plus each + # seahaven-lambda-execution-boundary- ARN (PLAT-52). That + # condition means any role this execution role creates must have a + # listed boundary applied, so it can never exceed what that boundary + # allows. Mgmt's .github copy still pins the unsuffixed ARN only. # # iam:PassRole is also included here so CloudFormation can pass # the auto-generated Lambda execution role to the Lambda service. @@ -1263,7 +1649,13 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": &acceptableLambdaBoundaries + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-afi-backup-monitor" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-front-integrations" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-procurement-ingest" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-meal-order-manager" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-seahaven-site" # Attach managed policies — MUST have boundary already on role - Sid: IAMAttachPolicyWithBoundary @@ -1274,7 +1666,7 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": *acceptableLambdaBoundaries # Put inline policy — MUST have boundary already on role - Sid: IAMPutRolePolicyWithBoundary @@ -1285,7 +1677,7 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": *acceptableLambdaBoundaries # Boundary management — SET the boundary only. DELETE is NOT # granted: for a delete, the iam:PermissionsBoundary condition key @@ -1307,7 +1699,7 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": *acceptableLambdaBoundaries # Explicit Deny backstop (AWS's documented NoBoundaryPolicyEdit / # NoBoundaryDelete delegation pattern). A Deny is required, not diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index c73efcf..73333d6 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -57,28 +57,32 @@ Description: >- # protection in (4) but is attached ONLY to the security OU — extending it to # prod/nonprod is the durable org-level fix and is tracked separately. # -# COUPLING: the boundary ARN referenced in the Conditions below is -# seahaven-lambda-execution-boundary, created by the seahaven-deploy-substrate -# stack in the same account. The reference is a literal !Sub string inside -# Condition values, so CloudFormation infers NO ordering edge from it — -# bin/app.ts carries an explicit addStackDependency on the same-account -# deploy-substrate stack instead. The coupling is by NAME: if the boundary -# policy is ever renamed or replaced, every Condition below (and the -# deploy-substrate copy) must change in the same piece of work. INFRA-186 -# (boundary reduced to a fleet-wide floor; per-workload boundaries are -# INFRA-187) changed the boundary's CONTENT, not its ARN, so this file is -# textually untouched — but the Terraform path IS affected: the Conditions -# below FORCE every role a Terraform apply creates onto that boundary, and -# the floor carries zero data-plane permissions. A migrating stack that -# creates Lambda execution roles must widen the boundary per the WIDENING -# PATH in lib/deploy-substrate/deploy-substrate.template.yaml, deployed -# before its first apply (README migration checklist step 2). +# COUPLING: acceptable boundary ARNs are an enumerated StringEquals list +# (PLAT-52): seahaven-lambda-execution-boundary plus each +# seahaven-lambda-execution-boundary-. The policies live in +# seahaven-deploy-substrate in the same account. The reference is a literal +# !Sub string inside Condition values, so CloudFormation infers NO ordering +# edge from it — bin/app.ts carries an explicit addStackDependency on the +# same-account deploy-substrate stack instead. The two files' allow-lists +# MUST match (mechanical sorted-JSON compare). Renaming the SHARED policy +# still fails silently. Adding a workload appends one ARN here AND creates +# the named policy in deploy-substrate. Do not use ArnLike on +# seahaven-lambda-execution-boundary-*: the org-baseline deploy role can +# CreatePolicy via CFN. INFRA-186 changed the shared boundary's CONTENT, not +# its ARN. A migrating stack that creates Lambda execution roles must add +# seahaven-lambda-execution-boundary- per the WIDENING PATH in +# lib/deploy-substrate/deploy-substrate.template.yaml, deployed before its +# first apply (README migration checklist step 3). Do not widen the shared +# document. # # SIZE BUDGET: an attached managed policy document is capped at 6,144 -# characters (whitespace excluded). The statement set below is ~2.5 KB. -# Measure before adding statements — len(json.dumps(doc,separators=(',',':'))) -# on the synthesized PolicyDocument — the same wall the role INLINE limit -# (10,240 bytes) put the first deploy-substrate deploy into on 2026-07-27. +# characters (whitespace excluded). The statement set below was ~2.5 KB +# before the PLAT-52 allow-list. Each extra boundary ARN is copied into +# four Sids. Measure before merging — +# len(json.dumps(doc,separators=(',',':'))) on the synthesized PolicyDocument +# — the same wall the role INLINE limit (10,240 bytes) put the first +# deploy-substrate deploy into on 2026-07-27. Compact size recorded after +# synth in this PR: 4693 characters / 10 statements (1451 headroom). # # PER-WORKSPACE ROLE ACCUMULATOR # At each stack's migration, a PR appends to this template: @@ -200,7 +204,13 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": &acceptableLambdaBoundaries + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-afi-backup-monitor" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-front-integrations" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-procurement-ingest" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-meal-order-manager" + - !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary-seahaven-site" # Attach managed policies — MUST have boundary already on role - Sid: IAMAttachPolicyWithBoundary @@ -211,7 +221,7 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": *acceptableLambdaBoundaries # Put inline policy — MUST have boundary already on role - Sid: IAMPutRolePolicyWithBoundary @@ -222,7 +232,7 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": *acceptableLambdaBoundaries # Boundary management — SET only, never DELETE. For a delete, the # iam:PermissionsBoundary condition key resolves to the boundary @@ -245,7 +255,7 @@ Resources: - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/*" Condition: StringEquals: - "iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary" + "iam:PermissionsBoundary": *acceptableLambdaBoundaries # Explicit Deny backstop (AWS's NoBoundaryPolicyEdit/NoBoundaryDelete # delegation pattern). A Deny is required, not merely omitting the From 5fa42677983b0bb1baccb0561f2c1742974586c0 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Fri, 14 Aug 2026 11:58:41 -0400 Subject: [PATCH 12/17] chore(iam): drop PascalCase WO Dynamo and alarm ARNs --- lib/deploy-substrate/deploy-substrate.template.yaml | 8 -------- .../terraform-substrate.template.yaml | 12 ------------ 2 files changed, 20 deletions(-) diff --git a/lib/deploy-substrate/deploy-substrate.template.yaml b/lib/deploy-substrate/deploy-substrate.template.yaml index f5723ed..cafa75d 100644 --- a/lib/deploy-substrate/deploy-substrate.template.yaml +++ b/lib/deploy-substrate/deploy-substrate.template.yaml @@ -566,10 +566,6 @@ Resources: - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" @@ -892,10 +888,6 @@ Resources: - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 73333d6..d1d97c6 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1057,10 +1057,6 @@ Resources: - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/purchase-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" @@ -1093,8 +1089,6 @@ Resources: - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:purchase-orders-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:verified-sites-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:pending-site-review-*" - - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:WorkOrders-*" - - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:WorkOrderComments-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:work-orders-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:work-order-comments-*" - Sid: RefreshApiGateway @@ -1278,10 +1272,6 @@ Resources: - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/verified-sites/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/pending-site-review/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrders/*" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments" - - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/WorkOrderComments/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-orders/*" - !Sub "arn:aws:dynamodb:us-east-1:${AWS::AccountId}:table/work-order-comments" @@ -1311,8 +1301,6 @@ Resources: - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:purchase-orders-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:verified-sites-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:pending-site-review-*" - - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:WorkOrders-*" - - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:WorkOrderComments-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:work-orders-*" - !Sub "arn:aws:cloudwatch:us-east-1:${AWS::AccountId}:alarm:work-order-comments-*" - Sid: SiteAlertsSns From c52aa5bf99077a1e5b14aaed35ee09173d217480 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:14:35 +0000 Subject: [PATCH 13/17] chore(deps): bump callable-dependency-review.yaml from 1.0.6 to 1.0.7 Bumps [Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml](https://github.com/sea-haven-industries/.github) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/sea-haven-industries/.github/releases) - [Commits](https://github.com/sea-haven-industries/.github/compare/7ac3528750b346f181347bb09f6af927a1c0aa14...e5691d8a7f96ac4d5a841a82975ff0a4354d53ac) --- updated-dependencies: - dependency-name: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml dependency-version: 1.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index a9901f6..09793af 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -7,4 +7,4 @@ permissions: jobs: review: - uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 From 9f7b7d522eaea38fb9850e63372521e688190982 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:14:40 +0000 Subject: [PATCH 14/17] chore(deps): bump ci-typescript-cdk.yaml from 1.0.6 to 1.0.7 Bumps [Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml](https://github.com/sea-haven-industries/.github) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/sea-haven-industries/.github/releases) - [Commits](https://github.com/sea-haven-industries/.github/compare/7ac3528750b346f181347bb09f6af927a1c0aa14...e5691d8a7f96ac4d5a841a82975ff0a4354d53ac) --- updated-dependencies: - dependency-name: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml dependency-version: 1.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1476343..de2c600 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,6 @@ permissions: jobs: ci: - uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" From e8e74073c715eaf8f32e545db1af2eca95245620 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:14:39 +0000 Subject: [PATCH 15/17] chore(deps): bump callable-pr-policy.yaml from 1.0.6 to 1.0.7 Bumps [Sea-Haven-Industries/.github/.github/workflows/callable-pr-policy.yaml](https://github.com/sea-haven-industries/.github) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/sea-haven-industries/.github/releases) - [Commits](https://github.com/sea-haven-industries/.github/compare/7ac3528750b346f181347bb09f6af927a1c0aa14...e5691d8a7f96ac4d5a841a82975ff0a4354d53ac) --- updated-dependencies: - dependency-name: Sea-Haven-Industries/.github/.github/workflows/callable-pr-policy.yaml dependency-version: 1.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/policy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/policy.yaml b/.github/workflows/policy.yaml index b6fdc0f..c65c4f6 100644 --- a/.github/workflows/policy.yaml +++ b/.github/workflows/policy.yaml @@ -15,7 +15,7 @@ permissions: jobs: policy: - uses: Sea-Haven-Industries/.github/.github/workflows/callable-pr-policy.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/callable-pr-policy.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 secrets: JIRA_CLOUD_ID: ${{ secrets.JIRA_CLOUD_ID }} JIRA_SERVICE_ACCOUNT_EMAIL: ${{ secrets.JIRA_SERVICE_ACCOUNT_EMAIL }} From 9b1cf9c9e7b90e6da7ec5684919e2b1575162a27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:14:38 +0000 Subject: [PATCH 16/17] chore(deps): bump cd-cdk.yaml from 1.0.6 to 1.0.7 Bumps [Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml](https://github.com/sea-haven-industries/.github) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/sea-haven-industries/.github/releases) - [Commits](https://github.com/sea-haven-industries/.github/compare/7ac3528750b346f181347bb09f6af927a1c0aa14...e5691d8a7f96ac4d5a841a82975ff0a4354d53ac) --- updated-dependencies: - dependency-name: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml dependency-version: 1.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/deploy.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9da1991..c62fc6f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -19,7 +19,7 @@ concurrency: jobs: deploy-management: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" stacks: "account-baseline dynamodb-cmk regional-baseline-us-west-2 regional-baseline-us-east-2 backup-offsite backup org-governance" @@ -27,7 +27,7 @@ jobs: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} deploy-external-dev: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" stacks: "external-dev-baseline" @@ -35,7 +35,7 @@ jobs: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_EXTDEV }} deploy-security: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" stacks: "security-baseline" @@ -44,7 +44,7 @@ jobs: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_SECURITY }} deploy-dev: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" stacks: "dev-baseline deploy-substrate-dev terraform-substrate-dev" @@ -53,7 +53,7 @@ jobs: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_DEV }} deploy-prod: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod terraform-substrate-prod app-web-acl-prod" From c61f444e8f4e1fcb61cd939c890988e79f150bba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:14:36 +0000 Subject: [PATCH 17/17] chore(deps): bump callable-labeler.yaml from 1.0.6 to 1.0.7 Bumps [Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml](https://github.com/sea-haven-industries/.github) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/sea-haven-industries/.github/releases) - [Commits](https://github.com/sea-haven-industries/.github/compare/7ac3528750b346f181347bb09f6af927a1c0aa14...e5691d8a7f96ac4d5a841a82975ff0a4354d53ac) --- updated-dependencies: - dependency-name: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml dependency-version: 1.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 653530d..90b2c46 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,4 +10,4 @@ permissions: jobs: label: - uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@7ac3528750b346f181347bb09f6af927a1c0aa14 # v1.0.6 + uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7