diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index c62fc6f..7c14f45 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -56,6 +56,9 @@ jobs: uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" + # seahaven-site-hcptf stays off this list until `cdk import` adopts the + # live roles. A create fails, and a failed create blocks the import. + # Add the id here in the change that follows a successful import. stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod terraform-substrate-prod app-web-acl-prod" stack-name: "seahaven-prod-baseline" secrets: diff --git a/README.md b/README.md index 6e68f77..05f0e79 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ are noted): | `seahaven-org-governance` | 328440206208 | us-east-1 | AWS Organizations OU tree + SCPs (incl. the cdk-imported external-dev guardrails) | | `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 | Prod/dev HCP substrate: OIDC + shared IAM manager + eight existing `hcptf-*` pairs (DeletionPolicy Retain). Pending PLAT-147 delete after consumer imports. New prod/dev HCP IAM is not added here. | +| `seahaven-site-hcptf` | 011934824531 | us-east-1 | Imported `hcptf-seahaven-site` apply and plan roles (PLAT-225). Adopt with `cdk import` after the site workspace drops them from state. Do not create. | | `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) | diff --git a/bin/app.ts b/bin/app.ts index 23d3bbc..3087f14 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -10,6 +10,7 @@ import { DeploySubstrateStack } from "../lib/deploy-substrate-stack"; import { TerraformSubstrateStack } from "../lib/terraform-substrate-stack"; import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack"; import { AppWebAclStack } from "../lib/app-web-acl-stack"; +import { SeahavenSiteHcptfStack } from "../lib/seahaven-site-hcptf-stack"; import { MemberBaselineStack } from "../lib/member-baseline-stack"; import { OrgGovernanceStack } from "../lib/org-governance-stack"; @@ -227,6 +228,14 @@ new AppWebAclStack(app, "app-web-acl-prod", { env: { account: PROD_ACCOUNT, region: "us-east-1" }, }); +// seahaven-site exec roles (PLAT-225). Not part of terraform-substrate. +// First operation is `cdk import`, after the site workspace drops the roles +// from its state. A create fails because the roles already exist. +new SeahavenSiteHcptfStack(app, "seahaven-site-hcptf", { + stackName: "seahaven-site-hcptf", + env: { account: PROD_ACCOUNT, region: "us-east-1" }, +}); + const terraformSubstrateDev = new TerraformSubstrateStack( app, "terraform-substrate-dev", diff --git a/lib/seahaven-site-hcptf-stack.ts b/lib/seahaven-site-hcptf-stack.ts new file mode 100644 index 0000000..aa57cf7 --- /dev/null +++ b/lib/seahaven-site-hcptf-stack.ts @@ -0,0 +1,451 @@ +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 seahaven-site HCP workspace (PLAT-225). + * + * These roles already exist. Adopt them. Do not create them. The substrate + * template no longer declares them. Its next deploy drops them from that + * stack and retains the live roles. + * + * Import only after that deploy, and after seahaven-site-prod applies its + * `removed` blocks: + * + * npx cdk import seahaven-site-hcptf + * + * Import identifiers are the role names `hcptf-seahaven-site` and + * `hcptf-seahaven-site-plan`. The stack stays off the prod deploy job until + * that import succeeds. A plain create fails because the roles already + * exist, and a failed create blocks the import. + */ +export class SeahavenSiteHcptfStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: cdk.StackProps) { + super(scope, id, props); + + const account = this.account; + const deployRole = `arn:aws:iam::${account}:role/tf-managed/githubdeploy-seahaven-site`; + const boundary = `arn:aws:iam::${account}:policy/tf-managed/seahaven-site-githubdeploy-boundary`; + const bucket = "arn:aws:s3:::seahaven-site-prod"; + const functionArn = `arn:aws:cloudfront::${account}:function/seahaven-site-prod-directory-index`; + const deployParams = `arn:aws:ssm:us-east-1:${account}:parameter/seahaven-site/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 apply = new iam.CfnRole(this, "ApplyRole", { + roleName: "hcptf-seahaven-site", + maxSessionDuration: 3600, + assumeRolePolicyDocument: trust(hcpOidc, "apply"), + managedPolicyArns: [], + policies: [ + { + policyName: "seahaven-site-services", + policyDocument: servicesPolicy(bucket, deployParams, wafParam, githubOidc), + }, + { + policyName: "scoped-iam-management", + policyDocument: scopedIamPolicy(account, deployRole, boundary), + }, + ], + tags: roleTags(), + }); + retain(apply); + + const plan = new iam.CfnRole(this, "PlanRole", { + roleName: "hcptf-seahaven-site-plan", + maxSessionDuration: 3600, + assumeRolePolicyDocument: trust(hcpOidc, "plan"), + managedPolicyArns: ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"], + policies: [ + { + policyName: "seahaven-site-plan-refresh", + policyDocument: planPolicy(account, bucket, functionArn, deployParams, wafParam, githubOidc, deployRole), + }, + ], + tags: roleTags(), + }); + retain(plan); + + cdk.Tags.of(this).add("Project", "seahaven-site"); + cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); + cdk.Tags.of(this).add("ManagedBy", "cdk"); + } +} + +function retain(role: iam.CfnRole): void { + role.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN; + role.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN; +} + +function roleTags(): cdk.CfnTag[] { + return [ + { key: "Project", value: "seahaven-site" }, + { 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:seahaven-site-prod:run_phase:${phase}`, + }, + }, + }, + ], + }); +} + +function servicesPolicy( + bucket: string, + deployParams: string, + wafParam: string, + githubOidc: string, +): iam.PolicyDocument { + return iam.PolicyDocument.fromJson({ + Version: "2012-10-17", + Statement: [ + { + Sid: "OriginBucket", + Effect: "Allow", + Action: "s3:*", + Resource: [bucket, `${bucket}/*`], + }, + { + Sid: "ReadGithubOidcProvider", + Effect: "Allow", + Action: "iam:GetOpenIDConnectProvider", + Resource: githubOidc, + }, + { + Sid: "CloudFrontManage", + Effect: "Allow", + Action: "cloudfront:*", + Resource: "*", + }, + { + Sid: "AcmCreate", + Effect: "Allow", + Action: "acm:RequestCertificate", + Resource: "*", + Condition: { StringEquals: { "aws:RequestTag/Project": "seahaven-site" } }, + }, + { + Sid: "AcmList", + Effect: "Allow", + Action: ["acm:ListCertificates", "acm:ListTagsForCertificate"], + Resource: "*", + }, + { + Sid: "AcmManageTagged", + Effect: "Allow", + Action: [ + "acm:AddTagsToCertificate", + "acm:DeleteCertificate", + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:RemoveTagsFromCertificate", + "acm:RenewCertificate", + ], + Resource: "*", + Condition: { StringEquals: { "aws:ResourceTag/Project": "seahaven-site" } }, + }, + { + 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): iam.PolicyDocument { + return iam.PolicyDocument.fromJson({ + 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-seahaven-site" }, + }, + }, + { + Sid: "DenyOtherPolicyVersions", + Effect: "Deny", + Action: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ], + NotResource: boundary, + }, + { + Sid: "CreateDeployBoundary", + Effect: "Allow", + Action: "iam:CreatePolicy", + Resource: "*", + Condition: { + StringEquals: { "aws:RequestTag/BoundaryFor": "githubdeploy-seahaven-site" }, + }, + }, + { + Sid: "ManageDeployBoundary", + Effect: "Allow", + Action: [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:TagPolicy", + ], + Resource: boundary, + }, + { + Sid: "WriteDeployRoles", + Effect: "Allow", + Action: [ + "iam:AttachRolePolicy", + "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( + account: string, + bucket: string, + functionArn: string, + deployParams: string, + wafParam: string, + githubOidc: string, + deployRole: string, +): iam.PolicyDocument { + return iam.PolicyDocument.fromJson({ + Version: "2012-10-17", + Statement: [ + { + Sid: "RefreshDeployRole", + Effect: "Allow", + Action: [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "iam:ListRoleTags", + ], + Resource: [ + deployRole, + `arn:aws:iam::${account}:role/hcptf-seahaven-site`, + `arn:aws:iam::${account}:role/hcptf-seahaven-site-plan`, + ], + }, + { + Sid: "RefreshGithubOidcProvider", + Effect: "Allow", + Action: "iam:GetOpenIDConnectProvider", + Resource: githubOidc, + }, + { + Sid: "RefreshManagedPolicies", + Effect: "Allow", + Action: ["iam:GetPolicy", "iam:GetPolicyVersion"], + Resource: "*", + }, + { + Sid: "RefreshOriginBucket", + 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: [bucket, `${bucket}/*`], + }, + { + Sid: "RefreshCloudFront", + Effect: "Allow", + Action: [ + "cloudfront:GetDistribution", + "cloudfront:GetDistributionConfig", + "cloudfront:GetOriginAccessControl", + "cloudfront:ListTagsForResource", + ], + Resource: "*", + }, + { + Sid: "RefreshCloudFrontFunction", + Effect: "Allow", + Action: ["cloudfront:DescribeFunction", "cloudfront:GetFunction"], + Resource: functionArn, + }, + { + Sid: "RefreshAcm", + Effect: "Allow", + Action: [ + "acm:DescribeCertificate", + "acm:GetCertificate", + "acm:ListCertificates", + "acm:ListTagsForCertificate", + ], + Resource: "*", + }, + { + Sid: "RefreshAppWebAclSsm", + Effect: "Allow", + Action: ["ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource"], + Resource: [wafParam, deployParams], + }, + { + Sid: "RefreshWafWebAcl", + Effect: "Allow", + Action: ["wafv2:GetWebACL", "wafv2:ListWebACLs"], + Resource: "*", + }, + ], + }); +} diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index 508273a..f67ecc7 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -1648,193 +1648,9 @@ Resources: Resource: - !Sub "arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:procurement-ingest/web-ui-auth-token-*" - # --------------------------------------------------------------------------- - # seahaven-site-prod (PLAT-91) — static site S3 + CloudFront + ACM + GHA - # content-deploy role. No Lambda → no boundary widen. No Route53 (apex DNS - # stays OOB in mgmt). Plan role: ViewOnly + plan-refresh sidecar. - # --------------------------------------------------------------------------- - HcptfSeahavenSitePlanRole: - Type: AWS::IAM::Role - Condition: IsProdAccount - DeletionPolicy: Retain - UpdateReplacePolicy: Retain - Properties: - RoleName: hcptf-seahaven-site-plan - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/app.terraform.io" - Action: sts:AssumeRoleWithWebIdentity - Condition: - StringEquals: - "app.terraform.io:aud": aws.workload.identity - "app.terraform.io:sub": organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:plan - ManagedPolicyArns: - - arn:aws:iam::aws:policy/job-function/ViewOnlyAccess - Policies: - - PolicyName: seahaven-site-plan-refresh - PolicyDocument: - Version: "2012-10-17" - Statement: - - Sid: RefreshDeployRole - Effect: Allow - Action: - - iam:GetRole - - iam:GetRolePolicy - - iam:ListRolePolicies - - iam:ListAttachedRolePolicies - - iam:ListRoleTags - Resource: - - !Sub "arn:aws:iam::${AWS::AccountId}:role/tf-managed/githubdeploy-seahaven-site" - - Sid: RefreshGithubOidcProvider - Effect: Allow - Action: - - iam:GetOpenIDConnectProvider - Resource: - - !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" - - Sid: RefreshManagedPolicies - Effect: Allow - Action: - - iam:GetPolicy - - iam:GetPolicyVersion - Resource: "*" - - Sid: RefreshOriginBucket - Effect: Allow - Action: - - s3:Get* - - s3:ListBucket - Resource: - - arn:aws:s3:::seahaven-site-prod - - arn:aws:s3:::seahaven-site-prod/* - - Sid: RefreshCloudFront - Effect: Allow - Action: - - cloudfront:Get* - - cloudfront:List* - Resource: "*" - # aws_cloudfront_function refresh reads DEVELOPMENT via - # DescribeFunction. Get* does not cover that API (PLAT-106). - - Sid: RefreshCloudFrontFunction - Effect: Allow - Action: - - cloudfront:DescribeFunction - Resource: - - !Sub "arn:aws:cloudfront::${AWS::AccountId}:function/seahaven-site-prod-directory-index" - - Sid: RefreshAcm - Effect: Allow - Action: - - acm:DescribeCertificate - - acm:ListCertificates - - acm:ListTagsForCertificate - - acm:GetCertificate - Resource: "*" - - Sid: RefreshAppWebAclSsm - Effect: Allow - Action: - - ssm:GetParameter - - ssm:GetParameters - Resource: - - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" - - Sid: RefreshWafWebAcl - Effect: Allow - Action: - - wafv2:GetWebACL - - wafv2:ListWebACLs - Resource: "*" - - HcptfSeahavenSiteApplyRole: - Type: AWS::IAM::Role - Condition: IsProdAccount - DeletionPolicy: Retain - UpdateReplacePolicy: Retain - Properties: - RoleName: hcptf-seahaven-site - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/app.terraform.io" - Action: sts:AssumeRoleWithWebIdentity - Condition: - StringEquals: - "app.terraform.io:aud": aws.workload.identity - "app.terraform.io:sub": organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:apply - ManagedPolicyArns: - - !Ref HcptfIamManagementPolicy - Policies: - - PolicyName: seahaven-site-services - PolicyDocument: - Version: "2012-10-17" - Statement: - - Sid: OriginBucket - Effect: Allow - Action: - - s3:* - Resource: - - arn:aws:s3:::seahaven-site-prod - - arn:aws:s3:::seahaven-site-prod/* - - Sid: ReadGithubOidcProvider - Effect: Allow - Action: - - iam:GetOpenIDConnectProvider - Resource: - - !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com" - - Sid: CloudFrontManage - Effect: Allow - Action: - - cloudfront:* - Resource: "*" - # RequestCertificate is account-level; pin via RequestTag matching - # provider default_tags (Project=seahaven-site). Post-create manage - # requires the same ResourceTag. - - Sid: AcmCreate - Effect: Allow - Action: - - acm:RequestCertificate - Resource: "*" - Condition: - StringEquals: - "aws:RequestTag/Project": seahaven-site - - Sid: AcmList - Effect: Allow - Action: - - acm:ListCertificates - - acm:ListTagsForCertificate - Resource: "*" - - Sid: AcmManageTagged - Effect: Allow - Action: - - acm:DescribeCertificate - - acm:GetCertificate - - acm:DeleteCertificate - - acm:AddTagsToCertificate - - acm:RemoveTagsFromCertificate - - acm:RenewCertificate - Resource: "*" - Condition: - StringEquals: - "aws:ResourceTag/Project": seahaven-site - # CloudFront web_acl_id is set via UpdateDistribution (cloudfront:* - # above). Read the shared ACL ARN from SSM (PLAT-92) and allow - # WAFv2 describe so plans/applies can validate the association. - - Sid: ReadAppWebAclSsm - Effect: Allow - Action: - - ssm:GetParameter - - ssm:GetParameters - Resource: - - !Sub "arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/seahaven/waf/app-web-acl-arn" - - Sid: ReadWafWebAcl - Effect: Allow - Action: - - wafv2:GetWebACL - - wafv2:GetWebACLForResource - - wafv2:ListWebACLs - - wafv2:ListResourcesForWebACL - Resource: "*" + # seahaven-site-prod exec roles moved to stack seahaven-site-hcptf (PLAT-225). + # DeletionPolicy on the removed resources was Retain, so dropping them here + # keeps the live roles for that stack to import. # --------------------------------------------------------------------------- # meal-order-manager-prod (PLAT-70, imported to the app workspace in PLAT-146).