diff --git a/README.md b/README.md index b32729c..6d6aef9 100644 --- a/README.md +++ b/README.md @@ -387,23 +387,29 @@ automatic and unchanged. migration step. The external-dev apply roles intentionally omit role create/delete, -managed-policy attach/detach, trust or boundary mutation, `iam:PassRole`, and +managed-policy attach/detach, boundary mutation, `iam:PassRole`, and secret-value APIs. IAM writes are limited to exact-role inline-policy and ordinary tag updates plus exact-profile tags; role descriptions remain stable -and HCP receives no `UpdateRole` or `UpdateRoleDescription`. The SCP permits -only the three enumerated HCP apply roles to mutate a `githubdeploy-*` role -whose locked `HcpTerraformWorkspace` resource tag equals the caller's immutable -principal tag. Adding or changing that manager tag remains administrator/CDK -only. POC DNS and certificate access is tag/name constrained because their +and HCP receives no `UpdateRole` or `UpdateRoleDescription`. Live SHOC backend +apply roles may `UpdateAssumeRolePolicy` only on the matching +`githubdeploy-shoc-backend-{dev,staging}` role. The OU SCP +`external-dev-iam-guardrails` no longer denies that action; the +account-attached SCP `external-dev-shoc-backend-deploy-trust` restores the +deny for every other `githubdeploy-*` and `hcptf-*` trust update. The SCP +permits only the three enumerated HCP apply roles to mutate a `githubdeploy-*` +inline policy whose locked `HcpTerraformWorkspace` resource tag equals the +caller's immutable principal tag. Adding or changing that manager tag remains +administrator/CDK only. POC DNS and certificate access is tag/name constrained because their physical IDs are allocated by the temporary retained CDK stack before Terraform imports them. Dev and staging DNS writes are pinned to their existing hosted-zone IDs and API record names. -Current compact policy-document sizes are 1,387 / 1,873 / 1,844 characters for +Current compact policy-document sizes are 1,387 / 2,055 / 2,030 characters for the POC/dev/staging deploy boundaries and 1,176 / 1,779 / 1,656 for their runtime boundaries, each below IAM's 6,144-character managed-policy limit. The -external-dev IAM guardrail SCP is 5,095 compact characters against its -5,120-character Organizations limit; keep size assertions in every change. +external-dev IAM guardrail SCP is 4,968 compact characters against its +5,120-character Organizations limit. The account-attached SHOC backend deploy +trust SCP is 1,237 compact characters. Keep size assertions in every change. **External-dev SHOC frontend adoption uses separate gates and creates its boundaries first.** The two live retained boundaries are @@ -851,7 +857,8 @@ aws sts assume-root --target-principal \ `iam:CreateLoginProfile` — recovery there needs that SCP temporarily detached too. The extdev OU sits at the **5-SCP hard quota**: any new guardrail for extdev must attach at the ACCOUNT (396287094661) or - consolidate into an existing policy. + consolidate into an existing policy. `external-dev-shoc-backend-deploy-trust` + is attached to that account, not the OU. **New-account flow (supersedes root-harden-before-OU-move):** create the account at the org ROOT → it has no root credentials from birth (verify with diff --git a/bin/app.ts b/bin/app.ts index 3f37371..23d3bbc 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -71,7 +71,7 @@ new AccountBaselineStack(app, "account-baseline", { const EXTDEV_FLOW_LOG_VPC_IDS: string[] = []; // ── Org structure: OUs + generalized SCPs (management account only) ───────── -// Existing external-dev OU + its 3 SCPs are adopted into this stack via +// Existing external-dev OU + its 3 imported SCPs are adopted into this stack via // `cdk import` post-deploy — see lib/org-governance-stack.ts header + README. new OrgGovernanceStack(app, "org-governance", { stackName: "seahaven-org-governance", diff --git a/lib/org-governance-stack.ts b/lib/org-governance-stack.ts index 5a9f902..a931191 100644 --- a/lib/org-governance-stack.ts +++ b/lib/org-governance-stack.ts @@ -398,7 +398,8 @@ export class OrgGovernanceStack extends cdk.Stack { }); retain(denyRootUser); - // ── Adopted (cdk-imported) external-dev OU + its 3 SCPs ───────────────── + // ── Adopted (cdk-imported) external-dev OU + its 3 SCPs, plus one + // account-attached SHOC backend deploy-trust SCP ─────────────────── // QUOTA: with deny-root-user attached (2026-07-14) this OU carries 5 SCPs // = the AWS hard limit per target. Any new guardrail for external-dev // must attach at the ACCOUNT (396287094661, own 5-slot budget) or @@ -436,6 +437,24 @@ export class OrgGovernanceStack extends cdk.Stack { }); retain(externalDevIamGuardrails); + // Account-attached: the OU is at the 5-SCP quota. This lets + // hcptf-shoc-backend-{dev,staging} UpdateAssumeRolePolicy on the matching + // githubdeploy-shoc-backend-* role only. All other githubdeploy-* and + // hcptf-* trust mutation stays denied except org/CDK. + const externalDevShocBackendDeployTrust = new organizations.CfnPolicy( + this, + "ExternalDevShocBackendDeployTrust", + { + name: "external-dev-shoc-backend-deploy-trust", + type: "SERVICE_CONTROL_POLICY", + description: + "external-dev account: SHOC backend HCP apply roles may update matching githubdeploy trust", + targetIds: ["396287094661"], + content: scpContent("external-dev-shoc-backend-deploy-trust"), + }, + ); + retain(externalDevShocBackendDeployTrust); + const externalDevProtectSecurity = new organizations.CfnPolicy(this, "ExternalDevProtectSecurity", { name: "external-dev-protect-security", type: "SERVICE_CONTROL_POLICY", diff --git a/lib/scp/external-dev-iam-guardrails.json b/lib/scp/external-dev-iam-guardrails.json index a0cbc83..4aa7d01 100644 --- a/lib/scp/external-dev-iam-guardrails.json +++ b/lib/scp/external-dev-iam-guardrails.json @@ -57,7 +57,7 @@ { "Sid": "ProtectDeploymentPrincipalLifecycle", "Effect": "Deny", - "Action": ["iam:UpdateAssumeRolePolicy", "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:DeleteRolePolicy", "iam:DeleteRole", "iam:UpdateRole", "iam:UpdateRoleDescription", "iam:PutRolePermissionsBoundary", "iam:DeleteRolePermissionsBoundary"], + "Action": ["iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:DeleteRolePolicy", "iam:DeleteRole", "iam:UpdateRole", "iam:UpdateRoleDescription", "iam:PutRolePermissionsBoundary", "iam:DeleteRolePermissionsBoundary"], "Resource": ["arn:aws:iam::396287094661:role/githubdeploy-*", "arn:aws:iam::396287094661:role/hcptf-*"], "Condition": { "ArnNotLike": { "aws:PrincipalArn": ["arn:aws:iam::396287094661:role/OrganizationAccountAccessRole", "arn:aws:iam::396287094661:role/cdk-hnb659fds-*"] } } }, diff --git a/lib/scp/external-dev-shoc-backend-deploy-trust.json b/lib/scp/external-dev-shoc-backend-deploy-trust.json new file mode 100644 index 0000000..0418734 --- /dev/null +++ b/lib/scp/external-dev-shoc-backend-deploy-trust.json @@ -0,0 +1,52 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ProtectOtherDeploymentPrincipalTrust", + "Effect": "Deny", + "Action": "iam:UpdateAssumeRolePolicy", + "NotResource": [ + "arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-dev", + "arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-staging" + ], + "Condition": { + "ArnNotLike": { + "aws:PrincipalArn": [ + "arn:aws:iam::396287094661:role/OrganizationAccountAccessRole", + "arn:aws:iam::396287094661:role/cdk-hnb659fds-*" + ] + } + } + }, + { + "Sid": "ProtectShocBackendDevGithubTrust", + "Effect": "Deny", + "Action": "iam:UpdateAssumeRolePolicy", + "Resource": "arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-dev", + "Condition": { + "ArnNotLike": { + "aws:PrincipalArn": [ + "arn:aws:iam::396287094661:role/OrganizationAccountAccessRole", + "arn:aws:iam::396287094661:role/cdk-hnb659fds-*", + "arn:aws:iam::396287094661:role/hcptf-shoc-backend-dev" + ] + } + } + }, + { + "Sid": "ProtectShocBackendStagingGithubTrust", + "Effect": "Deny", + "Action": "iam:UpdateAssumeRolePolicy", + "Resource": "arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-staging", + "Condition": { + "ArnNotLike": { + "aws:PrincipalArn": [ + "arn:aws:iam::396287094661:role/OrganizationAccountAccessRole", + "arn:aws:iam::396287094661:role/cdk-hnb659fds-*", + "arn:aws:iam::396287094661:role/hcptf-shoc-backend-staging" + ] + } + } + } + ] +} diff --git a/lib/terraform-substrate/terraform-substrate.template.yaml b/lib/terraform-substrate/terraform-substrate.template.yaml index edd2281..3667122 100644 --- a/lib/terraform-substrate/terraform-substrate.template.yaml +++ b/lib/terraform-substrate/terraform-substrate.template.yaml @@ -2649,6 +2649,12 @@ Resources: - s3:PutBucketPolicy - s3:PutBucketPublicAccessBlock Resource: arn:aws:s3:::elasticbeanstalk-* + - Sid: ReadDeployParameters + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: arn:aws:ssm:us-east-1:396287094661:parameter/shoc-backend/dev/deploy/* ShocBackendStagingDeployBoundary: Type: AWS::IAM::ManagedPolicy @@ -2702,14 +2708,35 @@ Resources: Resource: arn:aws:autoscaling:us-east-1:396287094661:autoScalingGroup:*:autoScalingGroupName/awseb-e-6c9m4vb62z-stack-* - Sid: UploadApplicationVersion Effect: Allow - Action: s3:PutObject - Resource: arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661/shoc-backend/* + Action: + - s3:PutObject + - s3:PutObjectAcl + - s3:PutObjectVersionAcl + - s3:GetObject + - s3:GetObjectAcl + - s3:GetObjectVersion + - s3:GetObjectVersionAcl + - s3:DeleteObject + Resource: + - arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661/shoc-backend/* + - arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661/resources/_runtime/_embedded_extensions/shoc-backend/* + - arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661/resources/environments/e-6c9m4vb62z/* - Sid: UseBeanstalkBucket Effect: Allow Action: - s3:GetBucketLocation - s3:ListBucket + - s3:GetBucketPolicy + - s3:GetBucketAcl + - s3:GetBucketVersioning + - s3:GetBucketOwnershipControls Resource: arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661 + - Sid: ReadDeployParameters + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: arn:aws:ssm:us-east-1:396287094661:parameter/shoc-backend/staging/deploy/* # Dedicated runtime ceilings preserve the non-AI portions of # AWSElasticBeanstalkWebTier while removing its 2026 Bedrock/Marketplace @@ -2848,10 +2875,12 @@ Resources: # DenySelfMutation protects every githubdeploy-* role, while this rehearsal # must adopt three exact githubdeploy roles. Each apply role instead carries # an environment-scoped inline policy. No apply role can create/delete roles, - # change managed-policy attachments or trust/boundaries, read/write secret - # values, or pass a role. The POC gate controls its new pair independently; - # the live gate stays false until the four existing dev/staging roles enter - # through a CloudFormation IMPORT change set. + # change managed-policy attachments or boundaries, read/write secret + # values, or pass a role. Live apply roles may UpdateAssumeRolePolicy only + # on the matching githubdeploy-shoc-backend-{dev,staging} role so the + # GitHub OIDC job_workflow_ref seam can land. The POC gate controls its new + # pair independently; the live gate stays false until the four existing + # dev/staging roles enter through a CloudFormation IMPORT change set. # # The existing app.terraform.io provider is referenced by literal ARN. The # stack instance sets CreateOIDCProvider=false, so external-dev never attempts @@ -3223,6 +3252,17 @@ Resources: - secretsmanager:GetResourcePolicy - secretsmanager:ListSecretVersionIds Resource: arn:aws:secretsmanager:us-east-1:396287094661:secret:shoc/dev/app-config-* + - Sid: ReadDevDeploySsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + - ssm:ListTagsForResource + Resource: arn:aws:ssm:us-east-1:396287094661:parameter/shoc-backend/dev/deploy/* + - Sid: DescribeDevDeploySsm + Effect: Allow + Action: ssm:DescribeParameters + Resource: "*" HcptfShocBackendDevApplyRole: Type: AWS::IAM::Role @@ -3334,6 +3374,17 @@ Resources: - iam:TagRole - iam:UntagRole Resource: arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-dev + - Sid: UpdateDevGithubDeployTrust + Effect: Allow + Action: iam:UpdateAssumeRolePolicy + Resource: arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-dev + - Sid: ManageDevDeploySsm + Effect: Allow + Action: + - ssm:PutParameter + - ssm:AddTagsToResource + - ssm:RemoveTagsFromResource + Resource: arn:aws:ssm:us-east-1:396287094661:parameter/shoc-backend/dev/deploy/* - Sid: TagDevAppConfig Effect: Allow Action: @@ -3410,6 +3461,7 @@ Resources: Effect: Allow Action: - acm:ListCertificates + - autoscaling:DescribeAutoScalingGroups - ec2:DescribeSecurityGroups - ec2:DescribeSubnets - ec2:DescribeVpcs @@ -3420,12 +3472,26 @@ Resources: - elasticbeanstalk:DescribeEnvironments - elasticbeanstalk:ListTagsForResource - rds:DescribeDBInstances + - route53:ListHostedZones - route53:ListHostedZonesByName Resource: "*" + # Elastic Beanstalk DescribeConfigurationSettings calls + # CreateBucket against its existing regional service bucket + # during both plan and apply refresh. + - Sid: AuthorizeExistingEbBucketDiscovery + Effect: Allow + Action: + - s3:CreateBucket + - s3:PutBucketOwnershipControls + Resource: arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661 + Condition: + StringEquals: + s3:x-amz-object-ownership: ObjectWriter - Sid: ReadSharedCertificate Effect: Allow Action: - acm:DescribeCertificate + - acm:GetCertificate - acm:ListTagsForCertificate Resource: arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00 - Sid: ReadSharedRdsTags @@ -3449,6 +3515,17 @@ Resources: - secretsmanager:GetResourcePolicy - secretsmanager:ListSecretVersionIds Resource: arn:aws:secretsmanager:us-east-1:396287094661:secret:shoc/staging/app-config-* + - Sid: ReadStagingDeploySsm + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + - ssm:ListTagsForResource + Resource: arn:aws:ssm:us-east-1:396287094661:parameter/shoc-backend/staging/deploy/* + - Sid: DescribeStagingDeploySsm + Effect: Allow + Action: ssm:DescribeParameters + Resource: "*" HcptfShocBackendStagingApplyRole: Type: AWS::IAM::Role @@ -3493,6 +3570,13 @@ Resources: Condition: StringEquals: "iam:PermissionsBoundary": arn:aws:iam::396287094661:policy/shoc-backend-staging-runtime-boundary + - Sid: UpdateStagingRuntimeTrust + Effect: Allow + Action: + - iam:UpdateAssumeRolePolicy + - iam:UpdateRole + - iam:UpdateRoleDescription + Resource: arn:aws:iam::396287094661:role/shoc-backend-staging - Sid: TagStagingRuntimeRole Effect: Allow Action: @@ -3518,6 +3602,17 @@ Resources: - iam:TagRole - iam:UntagRole Resource: arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-staging + - Sid: UpdateStagingGithubDeployTrust + Effect: Allow + Action: iam:UpdateAssumeRolePolicy + Resource: arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-staging + - Sid: ManageStagingDeploySsm + Effect: Allow + Action: + - ssm:PutParameter + - ssm:AddTagsToResource + - ssm:RemoveTagsFromResource + Resource: arn:aws:ssm:us-east-1:396287094661:parameter/shoc-backend/staging/deploy/* - Sid: TagStagingAppConfig Effect: Allow Action: