seahaven-account-baseline/lib/deploy-substrate-stack.ts
Adam Moussa d6bea33436
feat(deploy-substrate): per-account GitHub Actions deploy substrate for prod/dev
SAM repos migrating off the frozen management account need the shared
deploy plumbing (permissions boundary + github-cfn-execution-role) in
their target account; none of it existed outside mgmt, so there was no
OIDC SAM deploy path into seahaven-prod or seahaven-dev at all.

Adds a templated, per-account substrate stack so onboarding a future
account is one bin/app.ts instance plus one CD job, not a hand-rolled
copy. Per-repo githubdeploy-* roles stay out by design: they are
provisioned per repo at migration time so an account never accumulates
trust for repos that do not deploy to it.

The template is a verbatim extraction of the reviewed mgmt substrate,
with deliberate, documented divergences — notably the removal of
iam:DeleteRolePermissionsBoundary plus explicit Deny backstops, which
closes a confirmed privilege-escalation path (see PR body).
2026-07-27 16:24:09 -04:00

64 lines
2.8 KiB
TypeScript

import * as cdk from "aws-cdk-lib";
import * as cfninc from "aws-cdk-lib/cloudformation-include";
import * as path from "path";
import { Construct } from "constructs";
export interface DeploySubstrateStackProps extends cdk.StackProps {
/**
* Create the GitHub OIDC identity provider in this account. Leave false
* when the provider already exists (seahaven-prod and seahaven-dev both
* have it from their githubdeploy-* role provisioning) - an account can
* only hold ONE provider per URL, so creating a duplicate fails the deploy.
* Set true only for a brand-new account with no OIDC provider yet.
*/
createOidcProvider?: boolean;
}
/**
* Per-account GitHub Actions deploy substrate: the shared account-level
* resources every SAM deploy pipeline needs -
* - GitHub OIDC identity provider (conditional, see props),
* - `seahaven-lambda-execution-boundary` permissions boundary (the ceiling
* applied to every SAM-generated Lambda execution role),
* - `github-cfn-execution-role` (the shared CloudFormation execution role
* that cd-sam callers pass as cfn-role-arn).
*
* Deliberately NOT here: per-repo githubdeploy-* roles. Those are provisioned
* per repo at migration/onboarding time (deploy-role-first playbook) so an
* account never accumulates trust relationships for repos that do not deploy
* to it.
*
* The resources come verbatim from the management account's reviewed
* oidc-deploy-roles.yaml substrate section via cloudformation-include, so the
* policy JSON that passed cross-review and security review deploys unchanged.
* See lib/deploy-substrate/deploy-substrate.template.yaml for the provenance
* and drift warning (mgmt's copy stays source of truth for 328440206208 until
* its stacks finish migrating out).
*
* Deploy-order note: the boundary and the execution role live in the SAME
* stack, and the role carries an explicit DependsOn on the boundary (the
* role only names the boundary ARN inside Condition strings, so CFN would
* otherwise infer no creation edge). App stacks (payments-dashboard,
* front-integrations, sh-openswe-traces, ...) can only target this account
* AFTER this stack is deployed there.
*/
export class DeploySubstrateStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: DeploySubstrateStackProps) {
super(scope, id, props);
new cfninc.CfnInclude(this, "Substrate", {
templateFile: path.join(
__dirname,
"deploy-substrate",
"deploy-substrate.template.yaml",
),
parameters: {
CreateOIDCProvider: props?.createOidcProvider ? "true" : "false",
},
});
cdk.Tags.of(this).add("Project", "account-baseline");
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
cdk.Tags.of(this).add("ManagedBy", "cdk");
}
}