feat(ci): add the seahaven-ci baseline and runner exec roles (PLAT-253) (#180)

* feat(ci): add the seahaven-ci baseline and runner exec roles

* fix(ci): allow collection reads on the runner apply role
This commit is contained in:
Adam Moussa 2026-10-05 19:36:55 -04:00 • committed by GitHub
parent 227a5d91a2
commit 98c11e09ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 515 additions and 0 deletions

View file

@ -65,3 +65,12 @@ jobs:
stack-name: "seahaven-prod-baseline"
secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }}
deploy-ci:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7
with:
node-version: "24"
stacks: "ci-baseline seahaven-hcptf-ci"
stack-name: "seahaven-ci-baseline"
secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_CI }}

View file

@ -81,6 +81,8 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
| `app-web-acl-prod` | `seahaven-app-web-acl` | 011934824531 | us-east-1 | `lib/app-web-acl-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` |
| `ci-baseline` | `seahaven-ci-baseline` | 188424654861 | us-east-1 | `lib/member-baseline-stack.ts` (orgManagedDetection) |
| `seahaven-hcptf-ci` | `seahaven-hcptf` | 188424654861 | us-east-1 | `lib/actions-runner-hcptf-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
@ -92,6 +94,7 @@ deploys/diffs assume `OrganizationAccountAccessRole` in the target account.
| 001520130573 (security) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_SECURITY` |
| 710827005802 (dev) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_DEV` |
| 011934824531 (prod) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_PROD` |
| 188424654861 (ci) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_CI` |
Shared constructs (`DetectiveControls`, `FlowLogs`, `GovernanceToggles`) are
prefix-parameterized — construct ids and physical names must stay

View file

@ -16,12 +16,14 @@ import { OrgGovernanceStack } from "../lib/org-governance-stack";
import { PlatformAccessStack } from "../lib/platform-access-stack";
import { EngineeringAccessStack } from "../lib/engineering-access-stack";
import { ViewAccessStack } from "../lib/view-access-stack";
import { ActionsRunnerHcptfStack } from "../lib/actions-runner-hcptf-stack";
const ACCOUNT = "328440206208";
const EXTERNAL_DEV_ACCOUNT = "396287094661";
const SECURITY_ACCOUNT = "001520130573";
const DEV_ACCOUNT = "710827005802";
const PROD_ACCOUNT = "011934824531";
const CI_ACCOUNT = "188424654861";
// Index-derived logical IDs — append only, never reorder, never close a hole.
// Slots 0-2 are retired VPCs (FlowLog0, FlowLog1, FlowLog2). Clearing them
@ -104,6 +106,7 @@ new ViewAccessStack(app, "view-access", {
externalDevAccountId: EXTERNAL_DEV_ACCOUNT,
devAccountId: DEV_ACCOUNT,
prodAccountId: PROD_ACCOUNT,
ciAccountId: CI_ACCOUNT,
});
new MemberBaselineStack(app, "external-dev-baseline", {
@ -201,6 +204,27 @@ new MemberBaselineStack(app, "prod-baseline", {
orgManagedDetection: true,
});
// ── Member-account baseline: seahaven-ci (PLAT-253) ─────────────────────────
// Runner cluster account. Created at the org root, then moved into nonprod
// after this baseline exists. No SAM deploy substrate. Default VPC is deleted.
// Flow logs for the cluster VPC are owned by the actions-runner workspace.
new MemberBaselineStack(app, "ci-baseline", {
stackName: "seahaven-ci-baseline",
env: { account: CI_ACCOUNT, region: "us-east-1" },
namePrefix: "seahaven-ci",
monthlyBudgetUsd: 300,
budgetAlertEmail: "aws@seahaven.com",
ownerEmail: "adam@seahaven.com",
flowLogVpcIds: [],
managedByTag: "seahaven-org-baseline",
orgManagedDetection: true,
});
new ActionsRunnerHcptfStack(app, "seahaven-hcptf-ci", {
stackName: "seahaven-hcptf",
env: { account: CI_ACCOUNT, region: "us-east-1" },
});
// ── Per-account GitHub Actions deploy substrate ──────────────────────────────
// The shared account-level deploy plumbing for SAM pipelines: permissions
// boundary + github-cfn-execution-role (+ optional OIDC provider). mgmt's

View file

@ -0,0 +1,472 @@
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";
const ACCOUNT = "188424654861";
const GITHUB_THUMBPRINT = "ab9d0263244dd0326eb67015705a667e79cfe998";
const HCP_THUMBPRINT = "9e99a48a9960b14926bb7f3b02e22da2b0ab7280";
/**
* seahaven-ci exec roles for the actions-runner HCP workspace (PLAT-253).
*
* This account runs only the runner cluster. The stack also holds the GitHub
* and HCP OIDC providers and the org-baseline CDK deploy role, because both
* have to exist before the first CD run. Roles are a plain create.
*/
export class ActionsRunnerHcptfStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
cdk.Tags.of(this).add("Project", "actions-runner");
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
cdk.Tags.of(this).add("ManagedBy", "cdk");
const githubOidc = new iam.CfnOIDCProvider(this, "GithubOidc", {
url: "https://token.actions.githubusercontent.com",
clientIdList: ["sts.amazonaws.com"],
thumbprintList: [GITHUB_THUMBPRINT],
tags: roleTags("actions-runner"),
});
retain(githubOidc);
const hcpOidc = new iam.CfnOIDCProvider(this, "HcpOidc", {
url: "https://app.terraform.io",
clientIdList: ["aws.workload.identity"],
thumbprintList: [HCP_THUMBPRINT],
tags: roleTags("actions-runner"),
});
retain(hcpOidc);
const hcpArn = hcpOidc.attrArn;
const services = managedPolicy(
this,
"ServicesPolicy",
"actions-runner-hcptf-services",
servicesPolicy(),
);
const iamPolicy = managedPolicy(
this,
"IamPolicy",
"actions-runner-hcptf-iam",
iamPolicyDocument(),
);
const planRefresh = managedPolicy(
this,
"PlanPolicy",
"actions-runner-hcptf-plan",
planPolicy(hcpArn, githubOidc.attrArn),
);
const apply = new iam.CfnRole(this, "ApplyRole", {
roleName: "hcptf-actions-runner",
maxSessionDuration: 3600,
assumeRolePolicyDocument: trust(hcpArn, "apply"),
managedPolicyArns: [services.ref, iamPolicy.ref],
tags: roleTags("actions-runner"),
});
retain(apply);
const plan = new iam.CfnRole(this, "PlanRole", {
roleName: "hcptf-actions-runner-plan",
maxSessionDuration: 3600,
assumeRolePolicyDocument: trust(hcpArn, "plan"),
managedPolicyArns: [
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
planRefresh.ref,
],
tags: roleTags("actions-runner"),
});
retain(plan);
const deployPolicy = managedPolicy(
this,
"OrgBaselineDeployPolicy",
"actions-runner-org-baseline-deploy",
githubDeployPolicy(),
);
const deploy = new iam.CfnRole(this, "OrgBaselineDeployRole", {
roleName: "githubdeploy-seahaven-org-baseline",
description:
"GitHub Actions OIDC deploy role for seahaven-org-baseline (ci-baseline stack)",
maxSessionDuration: 3600,
assumeRolePolicyDocument: githubTrust(githubOidc.attrArn),
managedPolicyArns: [deployPolicy.ref],
tags: roleTags("actions-runner"),
});
retain(deploy);
new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn });
new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn });
new cdk.CfnOutput(this, "OrgBaselineDeployRoleArn", {
value: deploy.attrArn,
});
cdk.Aspects.of(this).add(new HcptfPolicyAspect());
}
}
function retain(resource: cdk.CfnResource): void {
resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
}
function roleTags(project: string): cdk.CfnTag[] {
return [
{ key: "Project", value: project },
{ key: "Owner", value: "adam@seahavenind.com" },
{ key: "ManagedBy", value: "cdk" },
];
}
function managedPolicy(
scope: Construct,
id: string,
name: string,
policyDocument: object,
): iam.CfnManagedPolicy {
const policy = new iam.CfnManagedPolicy(scope, id, {
managedPolicyName: name,
path: "/tf-managed/",
policyDocument,
});
retain(policy);
return policy;
}
function trust(providerArn: string, phase: "apply" | "plan"): iam.PolicyDocument {
return iam.PolicyDocument.fromJson({
Version: "2012-10-17",
Statement: [
{
Sid: phase === "apply" ? "HcpApply" : "HcpPlan",
Effect: "Allow",
Action: "sts:AssumeRoleWithWebIdentity",
Principal: { Federated: providerArn },
Condition: {
StringEquals: {
"app.terraform.io:aud": "aws.workload.identity",
"app.terraform.io:sub": `organization:seahaven:project:seahaven-ci:workspace:actions-runner:run_phase:${phase}`,
},
},
},
],
});
}
function githubTrust(providerArn: string): iam.PolicyDocument {
return iam.PolicyDocument.fromJson({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: "sts:AssumeRoleWithWebIdentity",
Principal: { Federated: providerArn },
Condition: {
StringEquals: {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
},
StringLike: {
"token.actions.githubusercontent.com:sub":
"repo:Sea-Haven-Industries/seahaven-org-baseline:ref:refs/heads/main",
},
},
},
],
});
}
function githubDeployPolicy(): object {
return {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: "sts:AssumeRole",
Resource: `arn:aws:iam::${ACCOUNT}:role/cdk-hnb659fds-*`,
},
{
Effect: "Allow",
Action: "cloudformation:DescribeStacks",
Resource: [
`arn:aws:cloudformation:us-east-1:${ACCOUNT}:stack/seahaven-ci-baseline/*`,
`arn:aws:cloudformation:us-east-1:${ACCOUNT}:stack/seahaven-hcptf/*`,
],
},
],
};
}
function servicesPolicy(): object {
const prefix = "actions-runner";
return {
Version: "2012-10-17",
Statement: [
{
Sid: "Ec2ForCluster",
Effect: "Allow",
Action: "ec2:*",
Resource: "*",
},
{
Sid: "EksCluster",
Effect: "Allow",
Action: "eks:*",
Resource: "*",
},
{
Sid: "Logs",
Effect: "Allow",
Action: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DeleteLogGroup",
"logs:DeleteRetentionPolicy",
"logs:ListTagsForResource",
"logs:PutRetentionPolicy",
"logs:TagResource",
"logs:UntagResource",
],
Resource: `arn:aws:logs:us-east-1:${ACCOUNT}:log-group:*`,
},
{
Sid: "LogGroupList",
Effect: "Allow",
Action: "logs:DescribeLogGroups",
Resource: "*",
},
{
Sid: "KmsForCluster",
Effect: "Allow",
Action: [
"kms:CreateAlias",
"kms:CreateGrant",
"kms:CreateKey",
"kms:DeleteAlias",
"kms:DescribeKey",
"kms:EnableKeyRotation",
"kms:GetKeyPolicy",
"kms:GetKeyRotationStatus",
"kms:ListAliases",
"kms:ListResourceTags",
"kms:PutKeyPolicy",
"kms:ScheduleKeyDeletion",
"kms:TagResource",
"kms:UntagResource",
"kms:UpdateAlias",
],
Resource: "*",
},
{
Sid: "EcrPublicAuth",
Effect: "Allow",
Action: "ecr-public:GetAuthorizationToken",
Resource: "*",
},
{
Sid: "StsForPublicEcr",
Effect: "Allow",
Action: "sts:GetServiceBearerToken",
Resource: "*",
},
{
Sid: "EcrRepository",
Effect: "Allow",
Action: [
"ecr:CreateRepository",
"ecr:DeleteRepository",
"ecr:DescribeRepositories",
"ecr:ListTagsForResource",
"ecr:PutImageTagMutability",
"ecr:PutLifecyclePolicy",
"ecr:DeleteLifecyclePolicy",
"ecr:GetLifecyclePolicy",
"ecr:PutImageScanningConfiguration",
"ecr:TagResource",
"ecr:UntagResource",
"ecr:SetRepositoryPolicy",
"ecr:GetRepositoryPolicy",
"ecr:DeleteRepositoryPolicy",
],
Resource: `arn:aws:ecr:us-east-1:${ACCOUNT}:repository/${prefix}`,
},
{
Sid: "SsmContract",
Effect: "Allow",
Action: [
"ssm:AddTagsToResource",
"ssm:DeleteParameter",
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:ListTagsForResource",
"ssm:PutParameter",
"ssm:RemoveTagsFromResource",
],
Resource: `arn:aws:ssm:us-east-1:${ACCOUNT}:parameter/${prefix}/*`,
},
{
Sid: "SsmList",
Effect: "Allow",
Action: "ssm:DescribeParameters",
Resource: "*",
},
],
};
}
function iamPolicyDocument(): object {
const roleArn = `arn:aws:iam::${ACCOUNT}:role/actions-runner*`;
const managedRole = `arn:aws:iam::${ACCOUNT}:role/tf-managed/*`;
const policyArn = `arn:aws:iam::${ACCOUNT}:policy/actions-runner*`;
const managedPolicyArn = `arn:aws:iam::${ACCOUNT}:policy/tf-managed/*`;
const profileArn = `arn:aws:iam::${ACCOUNT}:instance-profile/actions-runner*`;
return {
Version: "2012-10-17",
Statement: [
{
Sid: "RunnerRoles",
Effect: "Allow",
Action: [
"iam:AttachRolePolicy",
"iam:CreateRole",
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:DetachRolePolicy",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfilesForRole",
"iam:ListRolePolicies",
"iam:ListRoleTags",
"iam:PassRole",
"iam:PutRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription",
],
Resource: [roleArn, managedRole],
},
{
Sid: "RunnerPolicies",
Effect: "Allow",
Action: [
"iam:CreatePolicy",
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListPolicyTags",
"iam:ListPolicyVersions",
"iam:TagPolicy",
"iam:UntagPolicy",
],
Resource: [policyArn, managedPolicyArn],
},
{
Sid: "InstanceProfiles",
Effect: "Allow",
Action: [
"iam:AddRoleToInstanceProfile",
"iam:CreateInstanceProfile",
"iam:DeleteInstanceProfile",
"iam:GetInstanceProfile",
"iam:RemoveRoleFromInstanceProfile",
"iam:TagInstanceProfile",
],
Resource: profileArn,
},
{
Sid: "OidcProviders",
Effect: "Allow",
Action: [
"iam:AddClientIDToOpenIDConnectProvider",
"iam:CreateOpenIDConnectProvider",
"iam:DeleteOpenIDConnectProvider",
"iam:GetOpenIDConnectProvider",
"iam:ListOpenIDConnectProviderTags",
"iam:TagOpenIDConnectProvider",
"iam:UntagOpenIDConnectProvider",
"iam:UpdateOpenIDConnectProviderThumbprint",
],
Resource: `arn:aws:iam::${ACCOUNT}:oidc-provider/*`,
},
{
Sid: "ServiceLinkedRoles",
Effect: "Allow",
Action: "iam:CreateServiceLinkedRole",
Resource: `arn:aws:iam::${ACCOUNT}:role/aws-service-role/*`,
Condition: {
StringEquals: {
"iam:AWSServiceName": [
"eks.amazonaws.com",
"eks-nodegroup.amazonaws.com",
"eks-fargate-pods.amazonaws.com",
],
},
},
},
{
Sid: "PassAwsServiceRoles",
Effect: "Allow",
Action: "iam:PassRole",
Resource: `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks*`,
},
],
};
}
function planPolicy(hcpArn: string, githubArn: string): object {
return {
Version: "2012-10-17",
Statement: [
{
Sid: "NamedIamReads",
Effect: "Allow",
Action: [
"iam:GetOpenIDConnectProvider",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
],
Resource: [
`arn:aws:iam::${ACCOUNT}:role/actions-runner*`,
`arn:aws:iam::${ACCOUNT}:role/tf-managed/*`,
`arn:aws:iam::${ACCOUNT}:role/hcptf-actions-runner`,
`arn:aws:iam::${ACCOUNT}:role/hcptf-actions-runner-plan`,
`arn:aws:iam::${ACCOUNT}:role/githubdeploy-seahaven-org-baseline`,
`arn:aws:iam::${ACCOUNT}:policy/actions-runner*`,
`arn:aws:iam::${ACCOUNT}:policy/tf-managed/*`,
hcpArn,
githubArn,
],
},
{
Sid: "EksAccessReads",
Effect: "Allow",
Action: [
"eks:DescribeAccessEntry",
"eks:DescribeAddon",
"eks:DescribeCluster",
"eks:DescribeFargateProfile",
"eks:DescribeNodegroup",
"eks:DescribeUpdate",
"eks:ListAccessEntries",
"eks:ListAssociatedAccessPolicies",
],
Resource: `arn:aws:eks:us-east-1:${ACCOUNT}:cluster/actions-runner`,
},
{
Sid: "SsmReads",
Effect: "Allow",
Action: ["ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource"],
Resource: `arn:aws:ssm:us-east-1:${ACCOUNT}:parameter/actions-runner/*`,
},
],
};
}

View file

@ -14,6 +14,7 @@ export interface ViewAccessStackProps extends cdk.StackProps {
externalDevAccountId: string;
devAccountId: string;
prodAccountId: string;
ciAccountId: string;
}
/**
@ -76,6 +77,12 @@ export class ViewAccessStack extends cdk.Stack {
group,
props.prodAccountId,
);
this.assignment(
"ViewCiAssignment",
permissionSet,
group,
props.ciAccountId,
);
}
private assignment(