refactor(iam): fold seahaven-site roles into seahaven-hcptf (#175)
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
Deploy / deploy-security (push) Waiting to run
Deploy / deploy-dev (push) Waiting to run
Deploy / deploy-prod (push) Waiting to run

Prod HCP exec roles for seahaven-site now live in the same stack as payments-dashboard so role ownership is one stack.
This commit is contained in:
Adam Moussa 2026-10-02 17:30:52 +00:00 • committed by GitHub
parent c6de4e0c7f
commit 031e1d1a3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 97 additions and 50 deletions

View file

@ -58,10 +58,10 @@ jobs:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7
with:
node-version: "24"
# seahaven-site-hcptf was imported after the roles left terraform-substrate.
# seahaven-hcptf was imported. The deploy creates the three managed
# policies and removes the inline policies. Do not create the roles.
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod app-web-acl-prod seahaven-site-hcptf seahaven-hcptf"
# seahaven-hcptf was imported. It owns payments-dashboard and the
# seahaven-site exec roles. Do not create the roles. Deleting the
# retired seahaven-site-hcptf stack is a separate prod step.
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod app-web-acl-prod seahaven-hcptf"
stack-name: "seahaven-prod-baseline"
secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }}

View file

@ -33,8 +33,7 @@ are noted):
| `seahaven-engineering-access` | 328440206208 | us-east-1 | Identity Center group `engineering`. `EngineeringProd` reads payments-dashboard configuration and seahaven-site in 011934824531. `EngineeringDev` has no allow (PLAT-235, PLAT-236). |
| `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget |
| `seahaven-terraform-substrate` | 011934824531, 710827005802 | us-east-1 | Removed from the CDK app and from CD (PLAT-147). Live stacks remain until `scripts/delete-terraform-substrate-prod-dev.sh`. The six imported prod pairs are already forgotten. |
| `seahaven-site-hcptf` | 011934824531 | us-east-1 | Imported `hcptf-seahaven-site` apply and plan roles (PLAT-225). Policies are managed at `/tf-managed/`. On the prod deploy job. Do not create the roles. |
| `seahaven-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary. Trust is pinned to `payments-dashboard-prod` in project `seahaven-prod`, and to `payments-dashboard-dev` in project `seahaven-dev`. Roles and boundary are imported. On the dev and prod deploy jobs. |
| `seahaven-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary in prod and dev. Prod also owns the imported `hcptf-seahaven-site` apply and plan roles (PLAT-225), with policies at `/tf-managed/`. Trust is pinned per workspace. On the dev and prod deploy jobs. Do not create the roles. |
| `seahaven-terraform-substrate` | 396287094661 | us-east-1 | Staged manually for SHOC backend/frontend adoption; exact HCP roles and deploy boundaries referencing the existing OIDC provider. Stays (PLAT-148). |
| `seahaven-security-baseline` | 001520130573 | us-east-1 | Member-account baseline for the delegated security-admin account (same construct set) |
| `seahaven-dev-baseline` | 710827005802 | us-east-1 | Member-account baseline for internal dev/staging (org-managed detection — no local GuardDuty/SecurityHub) |
@ -78,7 +77,6 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
| `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` |
| `seahaven-site-hcptf` | `seahaven-site-hcptf` | 011934824531 | us-east-1 | `lib/seahaven-site-hcptf-stack.ts` |
| `seahaven-hcptf` | `seahaven-hcptf` | 011934824531 | us-east-1 | `lib/seahaven-hcptf-stack.ts` |
| `seahaven-hcptf-dev` | `seahaven-hcptf` | 710827005802 | us-east-1 | `lib/seahaven-hcptf-stack.ts` |
@ -240,12 +238,13 @@ Console / one-shot CLI owns only:
Do not manage prod/dev workload IAM (`hcptf-<stack>` pairs, Lambda exec
roles, `policy/tf-managed/<stack>` ceilings) in the console. Do not append
new prod/dev `hcptf-<stack>` pairs to this template. New prod/dev HCP stacks
do not need an org-baseline IAM PR. payments-dashboard HCP roles and
the Lambda boundary are stack `seahaven-hcptf` in prod and dev. Both copies
are imported and are on the deploy jobs. The deploy creates
do not need an org-baseline IAM PR. Prod HCP exec roles for
payments-dashboard and seahaven-site both live in `seahaven-hcptf`.
Dev `seahaven-hcptf` is payments-dashboard only. Both account copies
are imported and are on the deploy jobs. The payments deploy creates
`payments-dashboard-hcptf-iam`, `payments-dashboard-hcptf-services`, and
`payments-dashboard-hcptf-plan`, and removes the inline policies. Do not
create the roles or the boundary.
create the roles, the boundary, or the seahaven-site roles.
**External-dev IAM stays in this repo (PLAT-148).** SHOC backend/frontend HCP
roles, SHOC deploy/runtime boundaries, `shoc-frontend-resources.ts`, and
@ -267,7 +266,7 @@ Until the delete script runs, the live stacks still hold:
`seahaven-lambda-execution-boundary-*` allow-list; frozen, do not append).
The six imported prod pairs are Retain-removed. `seahaven-site` is
`seahaven-site-hcptf`. `sh-openswe-traces` is gone.
`seahaven-hcptf`. `sh-openswe-traces` is gone.
External-dev still carries:
@ -314,7 +313,7 @@ and nonprod (PLAT-145), covering `hcptf-bootstrap*` plus the break-glass /
CDK / `githubdeploy-*` set already on the security OU.
The six live prod pairs are imported into the owning app, not recreated, then
Retain-removed from this template. `seahaven-site` is `seahaven-site-hcptf`.
Retain-removed from this template. `seahaven-site` is `seahaven-hcptf`.
`sh-openswe-traces` is not imported. The prod/dev stacks are deleted after
that forget. See the first-apply and import runbooks below.
@ -611,7 +610,7 @@ wildcards. Do not enumerate provider Get* APIs.
state: `afi-backup-monitor`, `front-integrations`, `paychex-integrations`,
`procurement-ingest`, `meal-order-manager`, `seahaven-door-unlock-api`.
`sh-openswe-traces` was decommissioned (PLAT-196) and is not imported.
`seahaven-site` is stack `seahaven-site-hcptf` (PLAT-225), not this template.
`seahaven-site` is stack `seahaven-hcptf` (PLAT-225), not this template.
Repos that do not change: SHOC (`shoc-backend`, `shoc-frontend-new`),
remaining SAM / unmigrated stacks.

View file

@ -11,7 +11,6 @@ import { TerraformSubstrateStack } from "../lib/terraform-substrate-stack";
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
import { AppWebAclStack } from "../lib/app-web-acl-stack";
import { SeahavenHcptfStack } from "../lib/seahaven-hcptf-stack";
import { SeahavenSiteHcptfStack } from "../lib/seahaven-site-hcptf-stack";
import { MemberBaselineStack } from "../lib/member-baseline-stack";
import { OrgGovernanceStack } from "../lib/org-governance-stack";
import { PlatformAccessStack } from "../lib/platform-access-stack";
@ -231,19 +230,12 @@ new AppWebAclStack(app, "app-web-acl-prod", {
env: { account: PROD_ACCOUNT, region: "us-east-1" },
});
// seahaven-site exec roles (PLAT-225). Not part of terraform-substrate.
// Imported. On the prod deploy job. A create fails because the roles already exist.
// Inline policies are managed policies at /tf-managed/. Do not recreate the roles.
new SeahavenSiteHcptfStack(app, "seahaven-site-hcptf", {
stackName: "seahaven-site-hcptf",
env: { account: PROD_ACCOUNT, region: "us-east-1" },
});
// payments-dashboard HCP roles, scoped policies, and the Lambda boundary.
// The same three names already exist in prod and dev. Import them with
// `-c hcptfPaymentsImport=true`. A create fails. Neither stack is on a
// deploy job until that import succeeds. Trust is pinned per workspace.
// Prod also includes the imported seahaven-site apply and plan roles
// (PLAT-225). A create fails. Import site with `-c hcptfSiteImport=true`.
// Trust is pinned per workspace.
const hcptfPaymentsImport = contextBoolean("hcptfPaymentsImport");
const hcptfSiteImport = contextBoolean("hcptfSiteImport");
const paymentsSecrets = (
account: string,
suffixes: readonly string[],
@ -269,6 +261,8 @@ new SeahavenHcptfStack(app, "seahaven-hcptf", {
"expense-slack-signing-secret-lbb78J",
]),
importExisting: hcptfPaymentsImport,
includeSeahavenSite: true,
siteImportExisting: hcptfSiteImport,
});
new SeahavenHcptfStack(app, "seahaven-hcptf-dev", {

View file

@ -0,0 +1,17 @@
{
"SeahavenSiteApplyRoleE31E1F61": {
"RoleName": "hcptf-seahaven-site"
},
"SeahavenSitePlanRoleF0DFA964": {
"RoleName": "hcptf-seahaven-site-plan"
},
"SeahavenSiteIamPolicy1032B47C": {
"PolicyArn": "arn:aws:iam::011934824531:policy/tf-managed/seahaven-site-hcptf-iam"
},
"SeahavenSiteServicesPolicy1E92BC24": {
"PolicyArn": "arn:aws:iam::011934824531:policy/tf-managed/seahaven-site-hcptf-services"
},
"SeahavenSitePlanPolicyC4736E1F": {
"PolicyArn": "arn:aws:iam::011934824531:policy/tf-managed/seahaven-site-hcptf-plan"
}
}

View file

@ -8,7 +8,8 @@ const MAX_POLICY_CHARS = 6144;
/**
* Fails synth when an HCP stack's managed policy document reaches the IAM
* size quota, or when a role in that stack carries an inline policy.
* Register it on seahaven-hcptf and seahaven-site-hcptf only.
* Register it on seahaven-hcptf only. That stack owns payments-dashboard
* in prod and dev, and seahaven-site in prod.
*
* `allowInlinePolicies` is only for the one-time `cdk import` template.
* That template has to name the live inline policies so the following

View file

@ -2,11 +2,15 @@ import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";
import { HcptfPolicyAspect } from "./hcptf-policy-aspect";
import { SeahavenSiteRoles } from "./seahaven-site-hcptf-stack";
/**
* payments-dashboard HCP exec roles. One stack per account. Prod is
* 011934824531, workspace payments-dashboard-prod, project seahaven-prod.
* Dev is 710827005802, workspace payments-dashboard-dev, project seahaven-dev.
* HCP exec roles. One stack per account. Prod is 011934824531 and also
* hosts the seahaven-site apply and plan roles. Dev is 710827005802 and
* stays payments-dashboard only.
*
* payments-dashboard: workspace payments-dashboard-prod, project
* seahaven-prod, or workspace payments-dashboard-dev, project seahaven-dev.
*
* hcptf-payments-dashboard, hcptf-payments-dashboard-plan, and
* payments-dashboard-lambda-boundary already existed in both accounts and
@ -37,12 +41,27 @@ export interface SeahavenHcptfStackProps extends cdk.StackProps {
* Default is the managed-policy template.
*/
importExisting?: boolean;
/** Prod only. Fold seahaven-site exec roles into this stack. */
includeSeahavenSite?: boolean;
/**
* Synthesize the seahaven-site import template. Set from
* `-c hcptfSiteImport=true`. Omits site role tags and site outputs.
*/
siteImportExisting?: boolean;
}
export class SeahavenHcptfStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: SeahavenHcptfStackProps) {
super(scope, id, props);
if (props.importExisting && props.siteImportExisting) {
throw new Error("hcptfPaymentsImport and hcptfSiteImport cannot both be set");
}
paymentsDashboard(this, props);
if (props.includeSeahavenSite) {
new SeahavenSiteRoles(this, "SeahavenSite", {
importExisting: props.siteImportExisting === true,
});
}
cdk.Aspects.of(this).add(new HcptfPolicyAspect(props.importExisting === true));
}
}

View file

@ -1,25 +1,43 @@
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";
import { HcptfPolicyAspect } from "./hcptf-policy-aspect";
/**
* Prod exec roles for the seahaven-site HCP workspace (PLAT-225).
*
* These roles already exist and were imported into this stack. Do not
* create them. A plain create fails because the roles already exist.
* The stack is on the prod deploy job.
* Nested in the prod seahaven-hcptf stack. These roles already exist.
* Do not create them. A plain create fails because the roles already exist.
*
* Import identifiers were the role names `hcptf-seahaven-site` and
* `hcptf-seahaven-site-plan`. Construct ids stay ApplyRole and PlanRole.
* Import identifiers are the role names `hcptf-seahaven-site` and
* `hcptf-seahaven-site-plan`, plus the three /tf-managed/ policy ARNs.
* Construct ids stay ApplyRole and PlanRole under SeahavenSite.
* Inline policies are managed policies at /tf-managed/. Do not rename
* the roles.
*
* `importExisting` is the `-c hcptfSiteImport=true` template. It omits
* role tags and the role ARN outputs. CloudFormation rejects both on an
* IAM role import. The steady-state template adds them back.
*/
export class SeahavenSiteHcptfStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
export interface SeahavenSiteRolesProps {
/** Omit role tags and outputs. Set from `-c hcptfSiteImport=true`. */
importExisting?: boolean;
}
const account = this.account;
export class SeahavenSiteRoles extends Construct {
constructor(scope: Construct, id: string, props: SeahavenSiteRolesProps = {}) {
super(scope, id);
const importing = props.importExisting === true;
// Overrides the parent stack's Project=payments-dashboard tag.
cdk.Tags.of(this).add("Project", "seahaven-site", { priority: 200 });
if (importing) {
const roleOnly = { priority: 300, includeResourceTypes: ["AWS::IAM::Role"] };
cdk.Tags.of(this).remove("Project", roleOnly);
cdk.Tags.of(this).remove("Owner", roleOnly);
cdk.Tags.of(this).remove("ManagedBy", roleOnly);
}
const account = cdk.Stack.of(this).account;
const deployRole = `arn:aws:iam::${account}:role/tf-managed/githubdeploy-seahaven-site`;
const boundary = `arn:aws:iam::${account}:policy/tf-managed/seahaven-site-githubdeploy-boundary`;
const bucket = "arn:aws:s3:::seahaven-site-prod";
@ -62,7 +80,7 @@ export class SeahavenSiteHcptfStack extends cdk.Stack {
maxSessionDuration: 3600,
assumeRolePolicyDocument: trust(hcpOidc, "apply"),
managedPolicyArns: [iamPolicy.ref, services.ref],
tags: roleTags(),
...(importing ? {} : { tags: roleTags() }),
});
retain(apply);
@ -74,17 +92,16 @@ export class SeahavenSiteHcptfStack extends cdk.Stack {
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
planRefresh.ref,
],
tags: roleTags(),
...(importing ? {} : { tags: roleTags() }),
});
retain(plan);
new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn });
new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn });
cdk.Tags.of(this).add("Project", "seahaven-site");
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
cdk.Tags.of(this).add("ManagedBy", "cdk");
cdk.Aspects.of(this).add(new HcptfPolicyAspect());
if (!importing) {
const applyArn = new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn });
const planArn = new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn });
applyArn.overrideLogicalId("SeahavenSiteApplyRoleArn");
planArn.overrideLogicalId("SeahavenSitePlanRoleArn");
}
}
}

View file

@ -78,7 +78,7 @@ Description: >-
# those roles (PLAT-144/PLAT-146). The six imported prod pairs are removed
# here. Their previous DeletionPolicy is Retain, so CloudFormation forgets
# them (PLAT-147). hcptf-sh-openswe-traces was already forgotten.
# seahaven-site lives in seahaven-site-hcptf. External-dev
# seahaven-site lives in seahaven-hcptf. External-dev
# SHOC roles below remain in this template (PLAT-148). All remaining subs are
# exact StringEquals (never StringLike, never a wildcarded run_phase).
#