import * as cdk from "aws-cdk-lib"; import * as cfninc from "aws-cdk-lib/cloudformation-include"; import * as iam from "aws-cdk-lib/aws-iam"; import { CfnTag } from "aws-cdk-lib/core"; import { Construct } from "constructs"; const ACCOUNT_ID = "396287094661"; const CACHE_POLICY_ID = "658327ea-f89d-4fab-a63d-7e88639e58f6"; const SHARED_CERTIFICATE_ARN = "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"; const EXECUTION_BOUNDARY_ARN = "arn:aws:iam::396287094661:policy/external-dev-execution-boundary"; const HCP_PROVIDER_ARN = "arn:aws:iam::396287094661:oidc-provider/app.terraform.io"; const GITHUB_PROVIDER_ARN = "arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com"; const FORBIDDEN_POC_IDENTIFIERS = new Set([ "E2CWLM1AFB964P", "E30VSIK87N8H64", "us-east-1shocfrontenddevSpaRewrite58674DB8", "Z07671212N75U4YLPWZR8", "E2JDVEZ6EGD49J", "E1PF5R6QQNBZAI", "us-east-1shocfrontendstagingSpaRewriteE9C0CBDA", "Z02602739VQWBWCAGXP4", "Z02451891BSZD93CMMGDU", SHARED_CERTIFICATE_ARN, ]); interface FrontendEnvironment { readonly key: "tf-poc" | "dev" | "staging"; readonly workspace: string; readonly bucketName: string; readonly domainName: string; readonly hostedZoneId: string; readonly certificateArn: string; readonly deployRoleName: string; readonly distributionId: string; readonly originAccessControlId: string; readonly functionName: string; readonly roleCondition: cdk.CfnCondition; readonly invalidationCondition?: cdk.CfnCondition; } interface ShocFrontendResourcesProps { readonly template: cfninc.CfnInclude; readonly enablePocRoles: boolean; readonly enableLiveRoles: boolean; readonly pocDistributionId: string; readonly pocOriginAccessControlId: string; readonly pocFunctionName: string; readonly pocHostedZoneId: string; readonly pocCertificateArn: string; } const retain = (resource: cdk.CfnResource): void => { resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; }; const roleArn = (roleName: string): string => `arn:aws:iam::${ACCOUNT_ID}:role/${roleName}`; const bucketArn = (bucketName: string): string => `arn:aws:s3:::${bucketName}`; const distributionArn = (distributionId: string): string => `arn:aws:cloudfront::${ACCOUNT_ID}:distribution/${distributionId}`; const functionArn = (functionName: string): string => `arn:aws:cloudfront::${ACCOUNT_ID}:function/${functionName}`; const originAccessControlArn = (originAccessControlId: string): string => `arn:aws:cloudfront::${ACCOUNT_ID}:origin-access-control/${originAccessControlId}`; const hostedZoneArn = (hostedZoneId: string): string => `arn:aws:route53:::hostedzone/${hostedZoneId}`; const deployParameterArn = (environment: FrontendEnvironment): string => `arn:aws:ssm:us-east-1:${ACCOUNT_ID}:parameter/shoc-frontend-new/${environment.key}/deploy/*`; const isLiveFrontendEnvironment = ( environment: FrontendEnvironment, ): boolean => environment.key === "dev" || environment.key === "staging"; const environmentSid = (environment: FrontendEnvironment): string => environment.key.charAt(0).toUpperCase() + environment.key.slice(1); const frontendReadPolicy = ( environment: FrontendEnvironment, ): Record => { const siteBucketArn = bucketArn(environment.bucketName); const statements: Record[] = [ { Sid: "CallerIdentity", Effect: "Allow", Action: "sts:GetCallerIdentity", Resource: "*", }, { Sid: "ReadExactSiteBucket", 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: siteBucketArn, }, { Sid: "ReadReleasePointerObject", Effect: "Allow", Action: ["s3:GetObject", "s3:GetObjectTagging", "s3:GetObjectVersion"], Resource: `${siteBucketArn}/.release/current`, }, { Sid: "ReadExactCloudFrontResources", Effect: "Allow", Action: [ "cloudfront:DescribeFunction", "cloudfront:GetDistribution", "cloudfront:GetDistributionConfig", "cloudfront:GetFunction", "cloudfront:GetOriginAccessControl", "cloudfront:ListTagsForResource", ], Resource: [ distributionArn(environment.distributionId), functionArn(environment.functionName), originAccessControlArn(environment.originAccessControlId), ], }, { Sid: "ListCloudFrontInventory", Effect: "Allow", Action: [ "cloudfront:ListDistributions", "cloudfront:ListFunctions", "cloudfront:ListOriginAccessControls", ], Resource: "*", }, { Sid: "ReadManagedCachePolicy", Effect: "Allow", Action: "cloudfront:GetCachePolicy", Resource: `arn:aws:cloudfront::${ACCOUNT_ID}:cache-policy/${CACHE_POLICY_ID}`, }, { Sid: "ListCachePolicies", Effect: "Allow", Action: "cloudfront:ListCachePolicies", Resource: "*", }, { Sid: "ReadExactDeployRole", Effect: "Allow", Action: [ "iam:GetRole", "iam:GetRolePolicy", "iam:ListAttachedRolePolicies", "iam:ListRolePolicies", "iam:ListRoleTags", ], Resource: roleArn(environment.deployRoleName), }, { Sid: "ReadExactDeployBoundary", Effect: "Allow", Action: ["iam:GetPolicy", "iam:GetPolicyVersion"], Resource: `arn:aws:iam::${ACCOUNT_ID}:policy/shoc-frontend-new-${environment.key}-deploy-boundary`, }, { Sid: "ReadGithubOidcProvider", Effect: "Allow", Action: "iam:GetOpenIDConnectProvider", Resource: GITHUB_PROVIDER_ARN, }, { Sid: "ListOidcProviders", Effect: "Allow", Action: "iam:ListOpenIDConnectProviders", Resource: "*", }, { Sid: "ReadExactCertificate", Effect: "Allow", Action: [ "acm:DescribeCertificate", "acm:GetCertificate", "acm:ListTagsForCertificate", ], Resource: environment.certificateArn, }, { Sid: "ListCertificates", Effect: "Allow", Action: "acm:ListCertificates", Resource: "*", }, { Sid: "ReadExactDns", Effect: "Allow", Action: [ "route53:GetHostedZone", "route53:ListResourceRecordSets", "route53:ListTagsForResource", ], Resource: hostedZoneArn(environment.hostedZoneId), }, { Sid: "FindHostedZone", Effect: "Allow", Action: ["route53:ListHostedZones", "route53:ListHostedZonesByName"], Resource: "*", }, { Sid: "ReadDnsChanges", Effect: "Allow", Action: "route53:GetChange", Resource: "arn:aws:route53:::change/*", }, ]; if (isLiveFrontendEnvironment(environment)) { statements.push( { Sid: `Read${environmentSid(environment)}DeploySsm`, Effect: "Allow", Action: [ "ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource", ], Resource: deployParameterArn(environment), }, { Sid: `Describe${environmentSid(environment)}DeploySsm`, Effect: "Allow", Action: "ssm:DescribeParameters", Resource: "*", }, ); } return { Version: "2012-10-17", Statement: statements, }; }; const frontendApplyPolicy = ( environment: FrontendEnvironment, ): Record => { const live = isLiveFrontendEnvironment(environment); const statements: Record[] = [ { Sid: "DenyRoleLifecycleAndTrustMutation", Effect: "Deny", Action: [ "iam:AttachRolePolicy", "iam:CreateRole", "iam:CreateServiceLinkedRole", "iam:DeleteRole", "iam:DeleteRolePermissionsBoundary", "iam:DeleteRolePolicy", "iam:DetachRolePolicy", "iam:PassRole", "iam:PutRolePermissionsBoundary", "iam:UpdateRole", "iam:UpdateRoleDescription", ], Resource: "*", }, { Sid: "DenyManagedPolicyMutation", Effect: "Deny", Action: [ "iam:CreatePolicy", "iam:CreatePolicyVersion", "iam:DeletePolicy", "iam:DeletePolicyVersion", "iam:SetDefaultPolicyVersion", ], Resource: "*", }, { Sid: "DenyInfrastructureReplacement", Effect: "Deny", Action: [ "cloudfront:CreateDistribution", "cloudfront:CreateFunction", "cloudfront:CreateOriginAccessControl", "cloudfront:DeleteDistribution", "cloudfront:DeleteFunction", "cloudfront:DeleteOriginAccessControl", "cloudfront:UpdateOriginAccessControl", "s3:CreateBucket", "s3:DeleteBucket", "s3:DeleteBucketEncryption", "s3:DeleteBucketOwnershipControls", "s3:DeleteBucketPolicy", "s3:DeleteBucketPublicAccessBlock", "s3:PutBucketOwnershipControls", "s3:PutBucketPublicAccessBlock", "s3:PutBucketVersioning", "s3:PutEncryptionConfiguration", ], Resource: "*", }, { Sid: "DenySecretAccess", Effect: "Deny", Action: live ? ["kms:Decrypt", "secretsmanager:*"] : [ "kms:Decrypt", "secretsmanager:*", "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath", ], Resource: "*", }, ]; if (live) { statements.push({ Sid: "DenyUnrelatedParameterReads", Effect: "Deny", Action: [ "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath", ], NotResource: deployParameterArn(environment), }); } statements.push( { Sid: "LockHcpTerraformWorkspaceTag", Effect: "Deny", Action: ["iam:TagRole", "iam:UntagRole"], Resource: "*", Condition: { "ForAnyValue:StringEquals": { "aws:TagKeys": "HcpTerraformWorkspace", }, }, }, { Sid: "TagExactSiteBucket", Effect: "Allow", Action: "s3:PutBucketTagging", Resource: bucketArn(environment.bucketName), }, { Sid: "ReplaceExactBucketPolicy", Effect: "Allow", Action: "s3:PutBucketPolicy", Resource: bucketArn(environment.bucketName), }, { Sid: "TagExactCloudFrontResources", Effect: "Allow", Action: ["cloudfront:TagResource", "cloudfront:UntagResource"], Resource: [ distributionArn(environment.distributionId), functionArn(environment.functionName), ], }, // TagResource is already allowed. Update* and PublishFunction were denied // on * so Phase 2 in-place CloudFront updates could not apply. Scope them // to exact ARNs. The AWS provider publishes after UpdateFunction. { Sid: "UpdateExactDistribution", Effect: "Allow", Action: "cloudfront:UpdateDistribution", Resource: distributionArn(environment.distributionId), }, { Sid: "UpdateExactFunction", Effect: "Allow", Action: ["cloudfront:UpdateFunction", "cloudfront:PublishFunction"], Resource: functionArn(environment.functionName), }, { Sid: "InvalidateExactDistribution", Effect: "Allow", Action: ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"], Resource: distributionArn(environment.distributionId), }, { Sid: "WriteReleasePointerObject", Effect: "Allow", Action: [ "s3:GetObject", "s3:GetObjectTagging", "s3:GetObjectVersion", "s3:PutObject", "s3:PutObjectTagging", ], Resource: `${bucketArn(environment.bucketName)}/.release/current`, }, { Sid: "ReplaceExactDeployInlinePolicy", Effect: "Allow", Action: "iam:PutRolePolicy", Resource: roleArn(environment.deployRoleName), Condition: { StringEquals: { "iam:PermissionsBoundary": `arn:aws:iam::${ACCOUNT_ID}:policy/shoc-frontend-new-${environment.key}-deploy-boundary`, }, }, }, { Sid: "TagExactDeployRole", Effect: "Allow", Action: ["iam:TagRole", "iam:UntagRole"], Resource: roleArn(environment.deployRoleName), }, ); if (live) { statements.push( { Sid: `Update${environmentSid(environment)}GithubDeployTrust`, Effect: "Allow", Action: "iam:UpdateAssumeRolePolicy", Resource: roleArn(environment.deployRoleName), }, { Sid: `Manage${environmentSid(environment)}DeploySsm`, Effect: "Allow", Action: [ "ssm:PutParameter", "ssm:AddTagsToResource", "ssm:RemoveTagsFromResource", ], Resource: deployParameterArn(environment), }, ); } statements.push({ Sid: "ChangeExactSiteAliases", Effect: "Allow", Action: "route53:ChangeResourceRecordSets", Resource: hostedZoneArn(environment.hostedZoneId), Condition: { "ForAllValues:StringEquals": { "route53:ChangeResourceRecordSetsActions": [ "CREATE", "DELETE", "UPSERT", ], "route53:ChangeResourceRecordSetsNormalizedRecordNames": [ environment.domainName, ], "route53:ChangeResourceRecordSetsRecordTypes": ["A", "AAAA"], }, }, }); return { Version: "2012-10-17", Statement: statements, }; }; const assumeRolePolicy = ( workspace: string, runPhase: "plan" | "apply", ): Record => ({ Version: "2012-10-17", Statement: [ { Effect: "Allow", Principal: { Federated: HCP_PROVIDER_ARN }, Action: "sts:AssumeRoleWithWebIdentity", Condition: { StringEquals: { "app.terraform.io:aud": "aws.workload.identity", "app.terraform.io:sub": `organization:seahaven:project:seahaven-external-dev:` + `workspace:${workspace}:run_phase:${runPhase}`, }, }, }, ], }); const roleTags = ( environment: FrontendEnvironment, includeManagerTag: boolean, ): CfnTag[] => { const tags = [ { key: "Environment", value: environment.key }, { key: "Workspace", value: environment.workspace }, ]; if (includeManagerTag) { tags.push({ key: "HcpTerraformWorkspace", value: environment.workspace, }); } return tags; }; export class ShocFrontendResources extends Construct { constructor(scope: Construct, id: string, props: ShocFrontendResourcesProps) { super(scope, id); this.validatePocIdentifiers(props); const pocInvalidationCondition = new cdk.CfnCondition( this, "HasShocFrontendPocDistribution", { expression: cdk.Fn.conditionNot( cdk.Fn.conditionEquals(props.pocDistributionId, ""), ), }, ); pocInvalidationCondition.overrideLogicalId( "HasShocFrontendPocDistribution", ); const pocRoleCondition = new cdk.CfnCondition( this, "ShouldManageShocFrontendPocRoles", { expression: cdk.Fn.conditionAnd( cdk.Fn.conditionEquals(cdk.Aws.ACCOUNT_ID, ACCOUNT_ID), cdk.Fn.conditionEquals( props.enablePocRoles ? "true" : "false", "true", ), cdk.Fn.conditionNot( cdk.Fn.conditionEquals(props.pocDistributionId, ""), ), cdk.Fn.conditionNot( cdk.Fn.conditionEquals(props.pocOriginAccessControlId, ""), ), cdk.Fn.conditionNot( cdk.Fn.conditionEquals(props.pocFunctionName, ""), ), cdk.Fn.conditionNot( cdk.Fn.conditionEquals(props.pocHostedZoneId, ""), ), cdk.Fn.conditionNot( cdk.Fn.conditionEquals(props.pocCertificateArn, ""), ), ), }, ); pocRoleCondition.overrideLogicalId("ShouldManageShocFrontendPocRoles"); const liveRoleCondition = new cdk.CfnCondition( this, "ShouldManageShocFrontendLiveRoles", { expression: cdk.Fn.conditionAnd( cdk.Fn.conditionEquals(cdk.Aws.ACCOUNT_ID, ACCOUNT_ID), cdk.Fn.conditionEquals( props.enableLiveRoles ? "true" : "false", "true", ), ), }, ); liveRoleCondition.overrideLogicalId("ShouldManageShocFrontendLiveRoles"); const externalDevCondition = props.template.getCondition( "IsExternalDevAccount", ); const environments: FrontendEnvironment[] = [ { key: "tf-poc", workspace: "shoc-frontend-new-tf-poc", bucketName: "seahaven-shoc-frontend-tf-poc", domainName: "frontend-tf-poc.seahaven.com", hostedZoneId: props.pocHostedZoneId, certificateArn: props.pocCertificateArn, deployRoleName: "githubdeploy-shoc-frontend-new-tf-poc", distributionId: props.pocDistributionId, originAccessControlId: props.pocOriginAccessControlId, functionName: props.pocFunctionName, roleCondition: pocRoleCondition, invalidationCondition: pocInvalidationCondition, }, { key: "dev", workspace: "shoc-frontend-new-dev", bucketName: "seahaven-shoc-frontend-dev", domainName: "dev.seahaven.com", hostedZoneId: "Z07671212N75U4YLPWZR8", certificateArn: SHARED_CERTIFICATE_ARN, deployRoleName: "githubdeploy-shoc-frontend-new-dev", distributionId: "E2CWLM1AFB964P", originAccessControlId: "E30VSIK87N8H64", functionName: "us-east-1shocfrontenddevSpaRewrite58674DB8", roleCondition: liveRoleCondition, }, { key: "staging", workspace: "shoc-frontend-new-staging", bucketName: "seahaven-shoc-frontend-staging", domainName: "staging.seahaven.com", hostedZoneId: "Z02602739VQWBWCAGXP4", certificateArn: SHARED_CERTIFICATE_ARN, deployRoleName: "githubdeploy-shoc-frontend-new-staging", distributionId: "E2JDVEZ6EGD49J", originAccessControlId: "E1PF5R6QQNBZAI", functionName: "us-east-1shocfrontendstagingSpaRewriteE9C0CBDA", roleCondition: liveRoleCondition, }, ]; for (const environment of environments) { this.addEnvironment(environment, externalDevCondition); } } private validatePocIdentifiers(props: ShocFrontendResourcesProps): void { const distributionPattern = /^E[A-Z0-9]+$/; const functionPattern = /^[A-Za-z0-9_-]+$/; const hostedZonePattern = /^Z[A-Z0-9]+$/; const certificatePattern = /^arn:aws:acm:us-east-1:396287094661:certificate\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const identifiers = [ props.pocDistributionId, props.pocOriginAccessControlId, props.pocFunctionName, props.pocHostedZoneId, props.pocCertificateArn, ]; const hasPartialIdentifiers = identifiers.some((value) => value !== "") && identifiers.some((value) => value === ""); if (hasPartialIdentifiers) { throw new Error( "All five shocFrontendPoc identifiers must be set together", ); } if ( props.pocDistributionId !== "" && (!distributionPattern.test(props.pocDistributionId) || !distributionPattern.test(props.pocOriginAccessControlId) || !functionPattern.test(props.pocFunctionName) || !hostedZonePattern.test(props.pocHostedZoneId) || !certificatePattern.test(props.pocCertificateArn)) ) { throw new Error("Invalid shocFrontendPoc identifier"); } if ( identifiers.some((identifier) => FORBIDDEN_POC_IDENTIFIERS.has(identifier), ) ) { throw new Error( "shocFrontendPoc identifiers must not reuse live frontend or backend tf-poc resources", ); } if (props.enablePocRoles && props.pocDistributionId === "") { throw new Error( "enableShocFrontendPocRoles requires all five identifiers", ); } } private addEnvironment( environment: FrontendEnvironment, externalDevCondition: cdk.CfnCondition, ): void { const logicalSuffix = environment.key === "tf-poc" ? "Poc" : environment.key.charAt(0).toUpperCase() + environment.key.slice(1); const siteBucketArn = bucketArn(environment.bucketName); const exactDistributionArn = distributionArn(environment.distributionId); const boundaryStatements: unknown[] = [ { Sid: "ReadDeploymentBucket", Effect: "Allow", Action: [ "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:ListBucket", "s3:ListBucketVersions", ], Resource: siteBucketArn, }, { Sid: "PublishRollbackAndPruneSiteObjects", Effect: "Allow", Action: [ "s3:DeleteObject", "s3:DeleteObjectVersion", "s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", ], Resource: `${siteBucketArn}/*`, }, ]; const wrapDistributionStatement = ( statement: Record, ): unknown => environment.invalidationCondition === undefined ? statement : cdk.Fn.conditionIf( environment.invalidationCondition.logicalId, statement, cdk.Aws.NO_VALUE, ); // GitHub verify and live-state summary call get-distribution. The identity // policy already grants these; the boundary was the deny. const readDistributionStatement = { Sid: "ReadExactDistribution", Effect: "Allow", Action: [ "cloudfront:GetDistribution", "cloudfront:GetDistributionConfig", ], Resource: exactDistributionArn, }; const invalidationStatement = { Sid: "InvalidateExactDistribution", Effect: "Allow", Action: ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"], Resource: exactDistributionArn, }; boundaryStatements.push( wrapDistributionStatement(readDistributionStatement), wrapDistributionStatement(invalidationStatement), ); if (isLiveFrontendEnvironment(environment)) { boundaryStatements.push({ Sid: "ReadDeployParams", Effect: "Allow", Action: ["ssm:GetParameter", "ssm:GetParameters"], Resource: deployParameterArn(environment), }); } const deployBoundary = new iam.CfnManagedPolicy( this, `ShocFrontend${logicalSuffix}DeployBoundary`, { managedPolicyName: `shoc-frontend-new-${environment.key}-deploy-boundary`, description: `Maximum content deployment permissions for ` + `${environment.deployRoleName}.`, policyDocument: { Version: "2012-10-17", Statement: boundaryStatements, }, }, ); deployBoundary.cfnOptions.condition = externalDevCondition; deployBoundary.overrideLogicalId( `ShocFrontend${logicalSuffix}DeployBoundary`, ); retain(deployBoundary); const planRole = new iam.CfnRole( this, `HcptfShocFrontend${logicalSuffix}PlanRole`, { roleName: `${environment.workspace}-plan`.replace( "shoc-frontend-new", "hcptf-shoc-frontend-new", ), description: `Read-only HCP Terraform plan role for ${environment.workspace}.`, permissionsBoundary: EXECUTION_BOUNDARY_ARN, maxSessionDuration: 3600, assumeRolePolicyDocument: assumeRolePolicy( environment.workspace, "plan", ), policies: [ { policyName: `${environment.workspace}-import-read`, policyDocument: frontendReadPolicy(environment), }, ], tags: roleTags(environment, false), }, ); planRole.cfnOptions.condition = environment.roleCondition; planRole.overrideLogicalId(`HcptfShocFrontend${logicalSuffix}PlanRole`); retain(planRole); const applyRole = new iam.CfnRole( this, `HcptfShocFrontend${logicalSuffix}ApplyRole`, { roleName: environment.workspace.replace( "shoc-frontend-new", "hcptf-shoc-frontend-new", ), description: `Constrained HCP Terraform apply role for ${environment.workspace}.`, permissionsBoundary: EXECUTION_BOUNDARY_ARN, maxSessionDuration: 3600, assumeRolePolicyDocument: assumeRolePolicy( environment.workspace, "apply", ), policies: [ { policyName: `${environment.workspace}-import-read`, policyDocument: frontendReadPolicy(environment), }, { policyName: `${environment.workspace}-import-apply`, policyDocument: frontendApplyPolicy(environment), }, ], tags: roleTags(environment, true), }, ); applyRole.cfnOptions.condition = environment.roleCondition; applyRole.overrideLogicalId(`HcptfShocFrontend${logicalSuffix}ApplyRole`); applyRole.addResourceDependency(deployBoundary); retain(applyRole); } }