seahaven-org-baseline/lib/actions-runner-hcptf-stack.ts
Adam Moussa 98c11e09ad
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
2026-10-05 19:36:55 -04:00

472 lines
14 KiB
TypeScript

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/*`,
},
],
};
}