mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 05:43:17 +00:00
Merge pull request #104 from Sea-Haven-Industries/feature/per-workload-lambda-boundaries
feat(iam): add per-workload lambda execution boundaries (PLAT-52)
This commit is contained in:
commit
a2e9449ef1
3 changed files with 593 additions and 177 deletions
68
README.md
68
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-<workload>` (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-<workload>` (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-<workload>`). 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-<stack>`** 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-<stack>` 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-<stack>` 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
|
||||
|
|
|
|||
|
|
@ -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-<workload>
|
||||
# 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::<account>: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-<workload> 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-<workload>), 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-<stack>.
|
||||
#
|
||||
# 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:::*-<accountid> 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-<workload>: 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 <date>" 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-<stack>
|
||||
# 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::<acct>:policy/seahaven-lambda-execution-boundary
|
||||
# arn:aws:iam::<acct>:policy/seahaven-lambda-execution-boundary-<stack>
|
||||
# aws iam delete-policy-version --version-id v<oldest-non-default> ...
|
||||
# 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-<stack>
|
||||
# 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-<workload>, 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-<workload> 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
|
||||
|
|
|
|||
|
|
@ -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-<workload>. 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-<stack> 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue