From a43ec1f0f58ce7bcd16a8b56402ea35714583304 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:42:33 +0000 Subject: [PATCH] feat(hcptf): add hcptf-mta-sts apply and plan roles to seahaven-hcptf (PLAT-243) (#178) * feat(hcptf): add hcptf-mta-sts apply and plan roles to seahaven-hcptf New MtaStsRoles construct nested in the prod seahaven-hcptf stack for the mta-sts-prod HCP workspace. Fresh roles, plain create, Retain on every resource. Trust is StringEquals on the exact workspace sub per run phase. Managed policies at /tf-managed/: - mta-sts-hcptf-iam: manage only githubdeploy-mta-sts and its boundary; CreateRole requires that boundary; DenySelfMutation on hcptf-*, githubdeploy-*, cdk, OrganizationAccountAccessRole, seahaven-* - mta-sts-hcptf-services: S3 on mta-sts-prod-*, CloudFront, ACM scoped to Project=mta-sts, SSM /mta-sts/deploy/* and the WAF ACL parameter, GitHub OIDC provider read - mta-sts-hcptf-plan: enumerated refresh reads beside ViewOnlyAccess Outputs MtaStsApplyRoleArn and MtaStsPlanRoleArn. README lists mta-sts with the other prod exec roles that live in this stack. * fix(hcptf): scope mta-sts CreatePolicy and plan policy reads to the boundary ARN CreateDeployBoundary now names the boundary ARN as its Resource instead of "*", keeping the BoundaryFor request-tag condition as a second gate. The plan sidecar's GetPolicy, GetPolicyVersion, ListPolicyVersions, and ListPolicyTags are merged into one RefreshDeployBoundary statement on the boundary ARN. The boundary is the only managed policy in Terraform state, and ViewOnlyAccess does not carry GetPolicy or GetPolicyVersion. * fix(hcptf): replace cloudfront:* in mta-sts services policy with tag-gated grants CloudFrontManage granted cloudfront:* on every CloudFront resource in the account. Split into: - CloudFrontRead: the Get and ListTagsForResource calls Terraform makes - CloudFrontCreateTagged: CreateDistribution and TagResource on the distribution ARN type, gated on request tag Project=mta-sts - CloudFrontManageTagged: Update, Delete, Tag, Untag, and CreateInvalidation gated on resource tag Project=mta-sts - CloudFrontOac: Create, Update, Delete on the origin-access-control ARN type; OACs do not support tags A distribution another workspace owns cannot be mutated by this role. The workspace provider must set Project=mta-sts in default_tags. * fix(hcptf): close mta-sts TagResource bypass and trim ACM and plan reads CloudFrontCreateTagged keeps cloudfront:TagResource, which CreateDistributionWithTags requires before the distribution has tags, but adds Null aws:ResourceTag/Project so it applies only to a distribution with no Project tag yet. An existing distribution owned by another workspace can no longer be re-tagged into CloudFrontManageTagged's scope. ACM is trimmed to what aws_acm_certificate calls: RequestCertificate, DescribeCertificate, ListTagsForCertificate, AddTagsToCertificate, RemoveTagsFromCertificate, DeleteCertificate. GetCertificate, RenewCertificate, and ListCertificates are dropped from both roles. RefreshDeployRole reads only githubdeploy-mta-sts; the two CFN-owned exec roles are not in Terraform state. * fix(hcptf): give CloudFront create actions Resource "*" in mta-sts services policy cloudfront:CreateDistribution and cloudfront:CreateOriginAccessControl have no resource type in the service authorization reference and only match Resource "*". Scoping them to the distribution and OAC ARN types would have implicitly denied the first apply. TagResource at create time stays on the distribution ARN type with the RequestTag and Null ResourceTag conditions, and OAC update and delete stay on the OAC ARN type. Verified with iam simulate-custom-policy: CreateDistribution with request tag Project=mta-sts allowed; TagResource, UpdateDistribution, and DeleteDistribution on a distribution tagged Project=seahaven-site denied. --- README.md | 16 +- bin/app.ts | 2 + lib/mta-sts-hcptf-stack.ts | 563 ++++++++++++++++++++++++++++++++++++ lib/seahaven-hcptf-stack.ts | 10 +- 4 files changed, 582 insertions(+), 9 deletions(-) create mode 100644 lib/mta-sts-hcptf-stack.ts diff --git a/README.md b/README.md index 717f4a3..9d9f10e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ are noted): | `seahaven-view-access` | 328440206208 | us-east-1 | Identity Center group `view`. Permission set `View` is `ViewOnlyAccess` on all five accounts. No data-plane reads and no assume-role. Membership is outside this stack. | | `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-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-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) and the created `hcptf-mta-sts` apply and plan roles (PLAT-243), with policies at `/tf-managed/`. Trust is pinned per workspace. On the dev and prod deploy jobs. Do not create the payments-dashboard or seahaven-site roles by hand. | | `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) | @@ -239,14 +239,16 @@ Console / one-shot CLI owns only: Do not manage prod/dev workload IAM (`hcptf-` pairs, Lambda exec roles, `policy/tf-managed/` ceilings) in the console. Do not append -new prod/dev `hcptf-` pairs to this template. New prod/dev HCP stacks -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 +new prod/dev `hcptf-` pairs to this template. Prod HCP exec roles for +payments-dashboard, seahaven-site, and mta-sts all live in `seahaven-hcptf` +(`lib/seahaven-hcptf-stack.ts`, `lib/seahaven-site-hcptf-stack.ts`, +`lib/mta-sts-hcptf-stack.ts`). A new prod HCP workspace adds a nested +construct there. Dev `seahaven-hcptf` is payments-dashboard only. Both +account copies 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, the boundary, or the seahaven-site roles. +create the payments-dashboard roles, the boundary, or the seahaven-site +roles by hand; they are imported. The mta-sts roles are a plain create. **External-dev IAM stays in this repo (PLAT-148).** SHOC backend/frontend HCP roles, SHOC deploy/runtime boundaries, `shoc-frontend-resources.ts`, and diff --git a/bin/app.ts b/bin/app.ts index 361f7d9..3951cf9 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -244,6 +244,7 @@ new AppWebAclStack(app, "app-web-acl-prod", { // payments-dashboard HCP roles, scoped policies, and the Lambda boundary. // Prod also includes the imported seahaven-site apply and plan roles // (PLAT-225). A create fails. Import site with `-c hcptfSiteImport=true`. +// Prod also creates the mta-sts apply and plan roles (PLAT-243). // Trust is pinned per workspace. const hcptfPaymentsImport = contextBoolean("hcptfPaymentsImport"); const hcptfSiteImport = contextBoolean("hcptfSiteImport"); @@ -274,6 +275,7 @@ new SeahavenHcptfStack(app, "seahaven-hcptf", { importExisting: hcptfPaymentsImport, includeSeahavenSite: true, siteImportExisting: hcptfSiteImport, + includeMtaSts: true, }); new SeahavenHcptfStack(app, "seahaven-hcptf-dev", { diff --git a/lib/mta-sts-hcptf-stack.ts b/lib/mta-sts-hcptf-stack.ts new file mode 100644 index 0000000..f3a828f --- /dev/null +++ b/lib/mta-sts-hcptf-stack.ts @@ -0,0 +1,563 @@ +import * as cdk from "aws-cdk-lib"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; + +/** + * Prod exec roles for the mta-sts HCP workspace (PLAT-243). + * + * Nested in the prod seahaven-hcptf stack beside SeahavenSiteRoles. These + * roles are new. Plain CloudFormation create, no import template. + * + * The workspace owns four S3 origin buckets named `mta-sts-prod-`, + * four CloudFront distributions with OACs, four exact-name ACM + * certificates tagged Project=mta-sts, the SSM deploy contract under + * `/mta-sts/deploy/`, and the GitHub content-deploy role + * `githubdeploy-mta-sts` with its boundary. Policy files are published by + * GitHub Actions, not Terraform, so no object-level grants beyond the + * bucket itself are needed here. + * + * CloudFront distribution and ACM writes are gated on Project=mta-sts + * request and resource tags. The workspace provider must set that tag in + * default_tags, or the first apply fails on CreateDistribution. + * + * Inline policies are managed policies at /tf-managed/. Do not rename + * the roles. + */ +export class MtaStsRoles extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + + // Overrides the parent stack's Project=payments-dashboard tag. + cdk.Tags.of(this).add("Project", "mta-sts", { priority: 200 }); + + const account = cdk.Stack.of(this).account; + const deployRole = `arn:aws:iam::${account}:role/tf-managed/githubdeploy-mta-sts`; + const boundary = `arn:aws:iam::${account}:policy/tf-managed/mta-sts-githubdeploy-boundary`; + // One ARN covers the four buckets and their objects. `*` spans `/`, so a + // second `mta-sts-prod-*/*` entry is redundant (Access Analyzer flags it). + const buckets = ["arn:aws:s3:::mta-sts-prod-*"]; + const deployParams = `arn:aws:ssm:us-east-1:${account}:parameter/mta-sts/deploy/*`; + const wafParam = `arn:aws:ssm:us-east-1:${account}:parameter/seahaven/waf/app-web-acl-arn`; + const githubOidc = `arn:aws:iam::${account}:oidc-provider/token.actions.githubusercontent.com`; + const hcpOidc = `arn:aws:iam::${account}:oidc-provider/app.terraform.io`; + + const iamPolicy = managedPolicy( + this, + "IamPolicy", + "mta-sts-hcptf-iam", + scopedIamPolicy(account, deployRole, boundary), + ); + const services = managedPolicy( + this, + "ServicesPolicy", + "mta-sts-hcptf-services", + servicesPolicy(account, buckets, deployParams, wafParam, githubOidc), + ); + const planRefresh = managedPolicy( + this, + "PlanPolicy", + "mta-sts-hcptf-plan", + planPolicy(buckets, deployParams, wafParam, githubOidc, deployRole, boundary), + ); + + const apply = new iam.CfnRole(this, "ApplyRole", { + roleName: "hcptf-mta-sts", + maxSessionDuration: 3600, + assumeRolePolicyDocument: trust(hcpOidc, "apply"), + managedPolicyArns: [iamPolicy.ref, services.ref], + tags: roleTags(), + }); + retain(apply); + + const plan = new iam.CfnRole(this, "PlanRole", { + roleName: "hcptf-mta-sts-plan", + maxSessionDuration: 3600, + assumeRolePolicyDocument: trust(hcpOidc, "plan"), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess", + planRefresh.ref, + ], + tags: roleTags(), + }); + retain(plan); + + const applyArn = new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn }); + const planArn = new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn }); + applyArn.overrideLogicalId("MtaStsApplyRoleArn"); + planArn.overrideLogicalId("MtaStsPlanRoleArn"); + } +} + +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 retain(resource: cdk.CfnResource): void { + resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; + resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; +} + +function roleTags(): cdk.CfnTag[] { + return [ + { key: "Project", value: "mta-sts" }, + { key: "Owner", value: "adam@seahavenind.com" }, + { key: "ManagedBy", value: "cdk" }, + ]; +} + +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-prod:workspace:mta-sts-prod:run_phase:${phase}`, + }, + }, + }, + ], + }); +} + +function servicesPolicy( + account: string, + buckets: string[], + deployParams: string, + wafParam: string, + githubOidc: string, +): object { + const distributions = `arn:aws:cloudfront::${account}:distribution/*`; + const oacs = `arn:aws:cloudfront::${account}:origin-access-control/*`; + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "OriginBuckets", + Effect: "Allow", + Action: "s3:*", + Resource: buckets, + }, + { + Sid: "ReadGithubOidcProvider", + Effect: "Allow", + Action: "iam:GetOpenIDConnectProvider", + Resource: githubOidc, + }, + { + Sid: "CloudFrontRead", + Effect: "Allow", + Action: [ + "cloudfront:GetDistribution", + "cloudfront:GetDistributionConfig", + "cloudfront:GetInvalidation", + "cloudfront:GetOriginAccessControl", + "cloudfront:ListTagsForResource", + ], + Resource: "*", + }, + { + // The provider calls the CreateDistributionWithTags API, authorized + // as cloudfront:CreateDistribution. That action has no resource type + // and only accepts Resource "*". Request tags come from the + // workspace's default_tags. + Sid: "CloudFrontCreateTagged", + Effect: "Allow", + Action: "cloudfront:CreateDistribution", + Resource: "*", + Condition: { StringEquals: { "aws:RequestTag/Project": "mta-sts" } }, + }, + { + // Tagging at create time, before the distribution has any tags. The + // Null condition keeps this off every distribution that already has + // a Project tag, so another workspace's distribution cannot be + // re-tagged into CloudFrontManageTagged's scope. + Sid: "CloudFrontTagUntagged", + Effect: "Allow", + Action: "cloudfront:TagResource", + Resource: distributions, + Condition: { + StringEquals: { "aws:RequestTag/Project": "mta-sts" }, + Null: { "aws:ResourceTag/Project": "true" }, + }, + }, + { + // Mutation of a distribution another workspace owns is denied by the + // resource tag, the same pattern AcmManageTagged uses. + Sid: "CloudFrontManageTagged", + Effect: "Allow", + Action: [ + "cloudfront:CreateInvalidation", + "cloudfront:DeleteDistribution", + "cloudfront:TagResource", + "cloudfront:UntagResource", + "cloudfront:UpdateDistribution", + ], + Resource: distributions, + Condition: { StringEquals: { "aws:ResourceTag/Project": "mta-sts" } }, + }, + { + // CreateOriginAccessControl has no resource type; Resource "*" only. + Sid: "CloudFrontCreateOac", + Effect: "Allow", + Action: "cloudfront:CreateOriginAccessControl", + Resource: "*", + }, + { + // Origin access controls do not support tags, so the OAC ARN type is + // the tightest available scope. + Sid: "CloudFrontManageOac", + Effect: "Allow", + Action: [ + "cloudfront:DeleteOriginAccessControl", + "cloudfront:UpdateOriginAccessControl", + ], + Resource: oacs, + }, + { + Sid: "AcmCreate", + Effect: "Allow", + Action: "acm:RequestCertificate", + Resource: "*", + Condition: { StringEquals: { "aws:RequestTag/Project": "mta-sts" } }, + }, + { + Sid: "AcmListTags", + Effect: "Allow", + Action: "acm:ListTagsForCertificate", + Resource: "*", + }, + { + // The aws_acm_certificate resource reads with DescribeCertificate and + // reconciles tags with Add and Remove. No GetCertificate, Renew, or + // ListCertificates; those belong to the data source and early renewal. + Sid: "AcmManageTagged", + Effect: "Allow", + Action: [ + "acm:AddTagsToCertificate", + "acm:DeleteCertificate", + "acm:DescribeCertificate", + "acm:RemoveTagsFromCertificate", + ], + Resource: "*", + Condition: { StringEquals: { "aws:ResourceTag/Project": "mta-sts" } }, + }, + { + Sid: "ReadAppWebAclSsm", + Effect: "Allow", + Action: ["ssm:GetParameter", "ssm:GetParameters"], + Resource: wafParam, + }, + { + Sid: "WriteDeployContract", + Effect: "Allow", + Action: [ + "ssm:AddTagsToResource", + "ssm:DeleteParameter", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:ListTagsForResource", + "ssm:PutParameter", + "ssm:RemoveTagsFromResource", + ], + Resource: deployParams, + }, + { + Sid: "DescribeParameters", + Effect: "Allow", + Action: "ssm:DescribeParameters", + Resource: "*", + }, + { + Sid: "ReadWafWebAcl", + Effect: "Allow", + Action: [ + "wafv2:GetWebACL", + "wafv2:GetWebACLForResource", + "wafv2:ListResourcesForWebACL", + "wafv2:ListWebACLs", + ], + Resource: "*", + }, + ], + }; +} + +function scopedIamPolicy(account: string, deployRole: string, boundary: string): object { + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "DenyUntaggedCreatePolicy", + Effect: "Deny", + Action: "iam:CreatePolicy", + Resource: "*", + Condition: { Null: { "aws:RequestTag/BoundaryFor": "true" } }, + }, + { + Sid: "DenyOtherCreatePolicy", + Effect: "Deny", + Action: "iam:CreatePolicy", + Resource: "*", + Condition: { + StringNotEquals: { "aws:RequestTag/BoundaryFor": "githubdeploy-mta-sts" }, + }, + }, + { + Sid: "DenyOtherPolicyVersions", + Effect: "Deny", + Action: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ], + NotResource: boundary, + }, + { + // Only the boundary ARN may be created. The request tag stays as a + // second gate so the two Deny statements above keep their meaning. + Sid: "CreateDeployBoundary", + Effect: "Allow", + Action: "iam:CreatePolicy", + Resource: boundary, + Condition: { + StringEquals: { "aws:RequestTag/BoundaryFor": "githubdeploy-mta-sts" }, + }, + }, + { + Sid: "ManageDeployBoundary", + Effect: "Allow", + Action: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:ListPolicyTags", + "iam:SetDefaultPolicyVersion", + "iam:TagPolicy", + "iam:UntagPolicy", + ], + Resource: boundary, + }, + { + // Fresh role. Creation requires the deploy boundary to be set. + Sid: "CreateDeployRoleWithBoundary", + Effect: "Allow", + Action: "iam:CreateRole", + Resource: deployRole, + Condition: { StringEquals: { "iam:PermissionsBoundary": boundary } }, + }, + { + Sid: "WriteDeployRoles", + Effect: "Allow", + Action: [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ], + Resource: deployRole, + }, + { + Sid: "PutDeployRoleBoundary", + Effect: "Allow", + Action: "iam:PutRolePermissionsBoundary", + Resource: deployRole, + Condition: { StringEquals: { "iam:PermissionsBoundary": boundary } }, + }, + { + Sid: "IamReadOnly", + Effect: "Allow", + Action: [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoleTags", + "iam:ListRoles", + ], + Resource: "*", + }, + { + Sid: "DenySelfMutation", + Effect: "Deny", + Action: [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DeleteRolePermissionsBoundary", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ], + Resource: [ + `arn:aws:iam::${account}:role/hcptf-*`, + `arn:aws:iam::${account}:role/github-cfn-execution-role`, + `arn:aws:iam::${account}:role/githubdeploy-*`, + `arn:aws:iam::${account}:role/cdk-hnb659fds-*`, + `arn:aws:iam::${account}:role/OrganizationAccountAccessRole`, + `arn:aws:iam::${account}:role/seahaven-*`, + ], + }, + { + Sid: "DenyBoundaryTampering", + Effect: "Deny", + Action: ["iam:DeleteRolePermissionsBoundary", "iam:DeleteUserPermissionsBoundary"], + Resource: [`arn:aws:iam::${account}:role/*`, `arn:aws:iam::${account}:user/*`], + }, + { + Sid: "DenyBoundaryPolicyEdit", + Effect: "Deny", + Action: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ], + Resource: `arn:aws:iam::${account}:policy/seahaven-*`, + }, + ], + }; +} + +function planPolicy( + buckets: string[], + deployParams: string, + wafParam: string, + githubOidc: string, + deployRole: string, + boundary: string, +): object { + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "RefreshDeployRole", + Effect: "Allow", + Action: [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "iam:ListRoleTags", + ], + Resource: deployRole, + }, + { + Sid: "RefreshGithubOidcProvider", + Effect: "Allow", + Action: "iam:GetOpenIDConnectProvider", + Resource: githubOidc, + }, + { + // The boundary is the only managed policy in Terraform state. + // ViewOnlyAccess carries iam:List* but not GetPolicy or + // GetPolicyVersion, so those are granted here on the exact ARN. + Sid: "RefreshDeployBoundary", + Effect: "Allow", + Action: [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:ListPolicyTags", + "iam:ListPolicyVersions", + ], + Resource: boundary, + }, + { + Sid: "RefreshOriginBuckets", + Effect: "Allow", + Action: [ + "s3:GetAccelerateConfiguration", + "s3:GetBucketAcl", + "s3:GetBucketCORS", + "s3:GetBucketLocation", + "s3:GetBucketLogging", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketOwnershipControls", + "s3:GetBucketPolicy", + "s3:GetBucketPolicyStatus", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketRequestPayment", + "s3:GetBucketTagging", + "s3:GetBucketVersioning", + "s3:GetBucketWebsite", + "s3:GetEncryptionConfiguration", + "s3:GetLifecycleConfiguration", + "s3:GetReplicationConfiguration", + "s3:ListBucket", + ], + Resource: buckets, + }, + { + Sid: "RefreshCloudFront", + Effect: "Allow", + Action: [ + "cloudfront:GetDistribution", + "cloudfront:GetDistributionConfig", + "cloudfront:GetOriginAccessControl", + "cloudfront:ListTagsForResource", + ], + Resource: "*", + }, + { + Sid: "RefreshAcm", + Effect: "Allow", + Action: ["acm:DescribeCertificate", "acm:ListTagsForCertificate"], + Resource: "*", + }, + { + Sid: "RefreshSsm", + Effect: "Allow", + Action: ["ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource"], + Resource: [wafParam, deployParams], + }, + { + // DescribeParameters accepts only Resource "*". The AWS provider + // calls it while refreshing aws_ssm_parameter. + Sid: "DescribeParameters", + Effect: "Allow", + Action: "ssm:DescribeParameters", + Resource: "*", + }, + { + Sid: "RefreshWafWebAcl", + Effect: "Allow", + Action: ["wafv2:GetWebACL", "wafv2:ListWebACLs"], + Resource: "*", + }, + ], + }; +} diff --git a/lib/seahaven-hcptf-stack.ts b/lib/seahaven-hcptf-stack.ts index 93121ee..92a0daf 100644 --- a/lib/seahaven-hcptf-stack.ts +++ b/lib/seahaven-hcptf-stack.ts @@ -2,12 +2,13 @@ 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 { MtaStsRoles } from "./mta-sts-hcptf-stack"; import { SeahavenSiteRoles } from "./seahaven-site-hcptf-stack"; /** * 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. + * hosts the seahaven-site and mta-sts 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. @@ -48,6 +49,8 @@ export interface SeahavenHcptfStackProps extends cdk.StackProps { * `-c hcptfSiteImport=true`. Omits site role tags and site outputs. */ siteImportExisting?: boolean; + /** Prod only. Fold mta-sts exec roles into this stack (PLAT-243). */ + includeMtaSts?: boolean; } export class SeahavenHcptfStack extends cdk.Stack { @@ -62,6 +65,9 @@ export class SeahavenHcptfStack extends cdk.Stack { importExisting: props.siteImportExisting === true, }); } + if (props.includeMtaSts) { + new MtaStsRoles(this, "MtaSts"); + } cdk.Aspects.of(this).add(new HcptfPolicyAspect(props.importExisting === true)); } }