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: "ReadEksServiceRoles", Effect: "Allow", Action: "iam:GetRole", Resource: [ `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-fargate-pods.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-fargate.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-nodegroup.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKS`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKSForFargate`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKSNodegroup`, ], }, { 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", "eks-fargate.amazonaws.com", ], }, }, }, { Sid: "ServiceLinkedRoleNames", Effect: "Allow", Action: ["iam:CreateServiceLinkedRole", "iam:TagRole"], Resource: [ `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKS`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKSForFargate`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKSNodegroup`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-fargate-pods.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-fargate.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-nodegroup.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:ListPolicyTags", "iam:ListRolePolicies", "iam:ListRoleTags", ], Resource: [ `arn:aws:iam::${ACCOUNT}:role/actions-runner*`, `arn:aws:iam::${ACCOUNT}:role/tf-managed/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-fargate-pods.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-fargate.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/aws-service-role/eks-nodegroup.amazonaws.com/*`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKS`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKSForFargate`, `arn:aws:iam::${ACCOUNT}:role/AWSServiceRoleForAmazonEKSNodegroup`, `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/*`, `arn:aws:iam::${ACCOUNT}:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/*`, hcpArn, githubArn, ], }, { Sid: "CollectionReads", Effect: "Allow", Action: ["kms:ListAliases", "ssm:DescribeParameters"], Resource: "*", }, { Sid: "EcrPublicAuth", Effect: "Allow", Action: "ecr-public:GetAuthorizationToken", Resource: "*", }, { Sid: "StsForPublicEcr", Effect: "Allow", Action: "sts:GetServiceBearerToken", Resource: "*", }, { Sid: "EcrReads", Effect: "Allow", Action: [ "ecr:DescribeRepositories", "ecr:GetLifecyclePolicy", "ecr:GetRepositoryPolicy", "ecr:ListTagsForResource", ], Resource: `arn:aws:ecr:us-east-1:${ACCOUNT}:repository/actions-runner`, }, { 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/*`, }, ], }; }