mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
New stack seahaven-terraform-substrate (instances terraform-substrate-prod + terraform-substrate-dev): app.terraform.io OIDC provider and the shared boundary-gated guardrail policy seahaven-hcptf-iam-management that per-workspace Terraform apply roles attach at migration time. No roles are pre-provisioned (accumulator pattern, parallel to githubdeploy-*). Guardrail statements mirror seahaven-cfn-exec-iam-management byte-identically except DenySelfMutation, whose scope extends to hcptf-* alongside the GitHub-substrate principals. Explicit stack dependency on the same-account deploy-substrate stack (boundary ARN appears only in Condition strings, so CFN infers no edge).
266 lines
13 KiB
YAML
266 lines
13 KiB
YAML
AWSTemplateFormatVersion: "2010-09-09"
|
|
Description: >-
|
|
Per-account HCP Terraform deploy substrate for Sea Haven Industries:
|
|
the app.terraform.io OIDC identity provider and the shared boundary-gated
|
|
IAM guardrail policy that every per-workspace Terraform APPLY role attaches.
|
|
Per-workspace hcptf-* roles are NOT pre-provisioned — they are appended to
|
|
this template at each stack's migration time.
|
|
|
|
# PROVENANCE / DESIGN SOURCE
|
|
# Authored fresh 2026-07-30 (the mgmt Terraform POC's CLI-created provider and
|
|
# hcptf-* roles were rolled back the same day, so there is no deployed source
|
|
# to vendor). The IAM statement set in HcptfIamManagementPolicy MIRRORS the
|
|
# reviewed seahaven-cfn-exec-iam-management pattern in
|
|
# lib/deploy-substrate/deploy-substrate.template.yaml (boundary-gated
|
|
# CreateRole/AttachRolePolicy/PutRolePolicy/PutRolePermissionsBoundary +
|
|
# DenyBoundaryTampering / DenyBoundaryPolicyEdit / DenySelfMutation). If that
|
|
# pattern changes in either file, reconcile BOTH in the same piece of work and
|
|
# verify mechanically (tag-preserving YAML load + sorted JSON compare per
|
|
# statement), never by reading headers.
|
|
#
|
|
# 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
|
|
# (per-workload boundary scoping) changes the boundary's CONTENT, not its ARN,
|
|
# and does not touch this file.
|
|
#
|
|
# 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.
|
|
#
|
|
# PER-WORKSPACE ROLE ACCUMULATOR
|
|
# At each stack's migration, a PR appends to this template:
|
|
# - hcptf-<stack>-plan: read-only (ViewOnlyAccess-class), trust sub
|
|
# organization:seahaven:project:seahaven-<env>:workspace:<workspace>:run_phase:plan
|
|
# - hcptf-<stack>: apply role attaching HcptfIamManagementPolicy plus
|
|
# stack-scoped service statements, trust sub ...run_phase:apply
|
|
# All subs are exact StringEquals (never StringLike, never a wildcarded
|
|
# run_phase — a speculative PR plan must never hold write credentials);
|
|
# audience is aws.workload.identity. IAM role additions here are a mandatory
|
|
# GPT-4.1 cross-review + /sh-security-review trigger. See the README
|
|
# "Terraform substrate" section for the full migration checklist and the
|
|
# rollback runbook.
|
|
#
|
|
# This template is deployed via lib/terraform-substrate-stack.ts
|
|
# (cloudformation-include) as stack seahaven-terraform-substrate, once per
|
|
# member account that hosts Terraform-managed workloads (currently
|
|
# seahaven-prod 011934824531 and seahaven-dev 710827005802; NEVER mgmt —
|
|
# mgmt stays SAM until its stacks migrate out).
|
|
|
|
Resources:
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HCP Terraform OIDC provider
|
|
#
|
|
# Always created: Phase-0 checks (2026-07-30) confirmed neither prod nor dev
|
|
# has an app.terraform.io provider (the mgmt POC's copy was deleted in the
|
|
# same-day rollback and never existed in the member accounts). An account
|
|
# holds exactly ONE provider per URL.
|
|
# ---------------------------------------------------------------------------
|
|
TerraformCloudOIDCProvider:
|
|
Type: AWS::IAM::OIDCProvider
|
|
Properties:
|
|
Url: https://app.terraform.io
|
|
ClientIdList:
|
|
# Default audience of HCP Terraform dynamic provider credentials
|
|
# (TFC_AWS_WORKLOAD_IDENTITY_AUDIENCE). Trust policies pin this via
|
|
# StringEquals on app.terraform.io:aud.
|
|
- aws.workload.identity
|
|
ThumbprintList:
|
|
# AWS ignores thumbprints for issuers signed by a trusted root CA
|
|
# (app.terraform.io qualifies) and secures trust via the CA bundle;
|
|
# the property is populated because CloudFormation requires a value.
|
|
# This is the thumbprint HashiCorp's own AWS setup documentation uses.
|
|
- 9e99a48a9960b14926bb7f3b02e22da2b0ab7280
|
|
# Every future hcptf-* role trusts this provider. Retain so deleting the
|
|
# stack can never delete the account's Terraform federation anchor out
|
|
# from under live workspaces.
|
|
DeletionPolicy: Retain
|
|
UpdateReplacePolicy: Retain
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared boundary-gated IAM guardrail policy (attached managed policy)
|
|
#
|
|
# Attached by every per-workspace Terraform APPLY role (hcptf-<stack>);
|
|
# NEVER by plan roles (hcptf-<stack>-plan are read-only and hold no IAM
|
|
# writes at all). Defined once here so all apply roles carry the identical
|
|
# reviewed escalation control instead of per-role copies that can drift.
|
|
#
|
|
# PRIMARY ESCALATION CONTROL (same design as INFRA-97 on the SAM side):
|
|
# every iam:CreateRole / AttachRolePolicy / PutRolePolicy is conditioned on
|
|
# the target role carrying seahaven-lambda-execution-boundary, so a role
|
|
# created by a Terraform apply can never exceed the boundary ceiling. The
|
|
# POC security review confirmed the unconditioned alternative is critical:
|
|
# iam:PutRolePolicy on Lambda exec roles + lambda:UpdateFunctionCode reads
|
|
# every secret in the account.
|
|
# ---------------------------------------------------------------------------
|
|
HcptfIamManagementPolicy:
|
|
Type: AWS::IAM::ManagedPolicy
|
|
Properties:
|
|
# Fixed name: future hcptf-* roles reference it by ARN, and a rename
|
|
# would detach-and-replace mid-update. Treat a rename as a coordinated
|
|
# migration, not an edit.
|
|
ManagedPolicyName: seahaven-hcptf-iam-management
|
|
Description: >-
|
|
Boundary-gated IAM role lifecycle for per-workspace Terraform apply
|
|
roles (hcptf-*), plus the explicit Deny backstops that keep the
|
|
permissions boundary from being detached or rewritten and the deploy
|
|
substrates' own principals from being mutated. Mirrors
|
|
seahaven-cfn-exec-iam-management; reconcile changes across both.
|
|
PolicyDocument:
|
|
Version: "2012-10-17"
|
|
Statement:
|
|
# Create role — MUST attach boundary
|
|
- Sid: IAMCreateRoleWithBoundary
|
|
Effect: Allow
|
|
Action:
|
|
- iam:CreateRole
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
|
Condition:
|
|
StringEquals:
|
|
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
|
|
|
# Attach managed policies — MUST have boundary already on role
|
|
- Sid: IAMAttachPolicyWithBoundary
|
|
Effect: Allow
|
|
Action:
|
|
- iam:AttachRolePolicy
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
|
Condition:
|
|
StringEquals:
|
|
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
|
|
|
# Put inline policy — MUST have boundary already on role
|
|
- Sid: IAMPutRolePolicyWithBoundary
|
|
Effect: Allow
|
|
Action:
|
|
- iam:PutRolePolicy
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
|
Condition:
|
|
StringEquals:
|
|
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
|
|
|
# Boundary management — SET only, never DELETE. For a delete, the
|
|
# iam:PermissionsBoundary condition key resolves to the boundary
|
|
# CURRENTLY on the target role, so a StringEquals grant would match
|
|
# exactly the roles the gate protects and self-defeat it (verified
|
|
# live against the mgmt SAM copy 2026-07-27). Terraform never needs
|
|
# the delete: it SETS the boundary on roles it creates, and destroy
|
|
# calls DeleteRole.
|
|
- Sid: IAMPutPermissionsBoundary
|
|
Effect: Allow
|
|
Action:
|
|
- iam:PutRolePermissionsBoundary
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
|
Condition:
|
|
StringEquals:
|
|
"iam:PermissionsBoundary": !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-lambda-execution-boundary"
|
|
|
|
# Explicit Deny backstop (AWS's NoBoundaryPolicyEdit/NoBoundaryDelete
|
|
# delegation pattern). A Deny is required, not merely omitting the
|
|
# Allow — any future Allow added to an apply role silently reopens
|
|
# the escalation otherwise.
|
|
- Sid: DenyBoundaryTampering
|
|
Effect: Deny
|
|
Action:
|
|
- iam:DeleteRolePermissionsBoundary
|
|
- iam:DeleteUserPermissionsBoundary
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:user/*"
|
|
|
|
# Whole seahaven-* policy family: this policy carries the Denies, so
|
|
# it is a higher-value target than the boundary it protects. Safe to
|
|
# scope broadly — no Terraform stack manages a seahaven-* managed
|
|
# policy, and apply roles hold no iam:CreatePolicy.
|
|
- Sid: DenyBoundaryPolicyEdit
|
|
Effect: Deny
|
|
Action:
|
|
- iam:CreatePolicyVersion
|
|
- iam:SetDefaultPolicyVersion
|
|
- iam:DeletePolicyVersion
|
|
- iam:DeletePolicy
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:policy/seahaven-*"
|
|
|
|
# Self-protection for BOTH deploy substrates' principals. Without
|
|
# this the control is one API call from being undone —
|
|
# IAMRoleReadAndDelete below grants iam:DetachRolePolicy on
|
|
# Resource "*" unconditioned, so an apply role could detach this
|
|
# very policy from itself. Scope covers the Terraform substrate's
|
|
# own roles (hcptf-*) AND the GitHub Actions substrate's
|
|
# (github-cfn-execution-role, githubdeploy-*): a Terraform apply
|
|
# never legitimately manages any of them — hcptf-* roles are
|
|
# managed by THIS stack via the CDK bootstrap execution role, the
|
|
# GitHub-side roles by their own substrate/onboarding — so the Deny
|
|
# costs nothing operationally and closes the same
|
|
# UpdateAssumeRolePolicy-on-* repoint risk the SAM-side review
|
|
# flagged, for every substrate principal reachable from this path.
|
|
- Sid: DenySelfMutation
|
|
Effect: Deny
|
|
Action:
|
|
- iam:AttachRolePolicy
|
|
- iam:DeleteRole
|
|
- iam:DeleteRolePolicy
|
|
- iam:DeleteRolePermissionsBoundary
|
|
- iam:DetachRolePolicy
|
|
- iam:PutRolePolicy
|
|
- iam:PutRolePermissionsBoundary
|
|
- iam:UpdateAssumeRolePolicy
|
|
- iam:UpdateRole
|
|
- iam:UpdateRoleDescription
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/hcptf-*"
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/github-cfn-execution-role"
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/githubdeploy-*"
|
|
|
|
# Read / tag / delete role and policy — no boundary condition needed
|
|
# (delete cannot be boundary-conditioned, see IAMPutPermissionsBoundary;
|
|
# DenySelfMutation above is the backstop). Parity with the SAM copy.
|
|
- Sid: IAMRoleReadAndDelete
|
|
Effect: Allow
|
|
Action:
|
|
- iam:DeleteRole
|
|
- iam:DeleteRolePolicy
|
|
- iam:DetachRolePolicy
|
|
- iam:GetRole
|
|
- iam:GetRolePolicy
|
|
- iam:ListAttachedRolePolicies
|
|
- iam:ListRolePolicies
|
|
- iam:ListRoles
|
|
- iam:TagRole
|
|
- iam:UntagRole
|
|
- iam:UpdateRole
|
|
- iam:UpdateRoleDescription
|
|
- iam:UpdateAssumeRolePolicy
|
|
- iam:GetPolicy
|
|
- iam:GetPolicyVersion
|
|
- iam:ListPolicies
|
|
- iam:ListPolicyVersions
|
|
Resource: "*"
|
|
|
|
# PassRole — Terraform passes stack-created execution roles to the
|
|
# Lambda service. Other target services (e.g. scheduler.amazonaws.com,
|
|
# apigateway.amazonaws.com) are NOT granted here: a stack that needs
|
|
# one adds a scoped PassRole statement on its own apply role at
|
|
# migration time.
|
|
- Sid: IAMPassRole
|
|
Effect: Allow
|
|
Action:
|
|
- iam:PassRole
|
|
Resource:
|
|
- !Sub "arn:aws:iam::${AWS::AccountId}:role/*"
|
|
Condition:
|
|
StringEquals:
|
|
"iam:PassedToService": "lambda.amazonaws.com"
|