fix(iam): allow shoc-backend HCP apply to write deploy SSM and matching githubdeploy trust (PLAT-148) (#148)

* fix(iam): allow shoc-backend HCP apply to write deploy SSM and matching githubdeploy trust (PLAT-148)

* fix(iam): grant shoc-backend staging plan named inventory reads (PLAT-148)

* fix(iam): allow staging githubdeploy to GetObject release zips (PLAT-148)

* fix(iam): allow staging githubdeploy to write EB processed extensions (PLAT-148)

* fix(iam): allow staging githubdeploy GetObjectAcl on release zips (PLAT-148)

* fix(iam): grant staging githubdeploy named S3 reads on EB resources prefix (PLAT-148)

* fix(iam): allow staging githubdeploy to delete EB version cache objects (PLAT-148)

* fix(iam): scope staging githubdeploy S3 object access to the EB bucket (PLAT-148)

* fix(iam): allow staging githubdeploy PutObjectVersionAcl on EB artifacts (PLAT-148)

* fix(iam): allow staging githubdeploy GetBucketPolicy on the EB bucket (PLAT-148)

* fix(iam): scope staging githubdeploy S3 objects to SHOC and staging EB prefixes (PLAT-148)
This commit is contained in:
Adam Moussa 2026-09-18 18:13:01 +00:00 • committed by GitHub
parent 7a1623e8d4
commit db9465deda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 192 additions and 19 deletions

View file

@ -387,23 +387,29 @@ automatic and unchanged.
migration step. migration step.
The external-dev apply roles intentionally omit role create/delete, 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 secret-value APIs. IAM writes are limited to exact-role inline-policy and
ordinary tag updates plus exact-profile tags; role descriptions remain stable ordinary tag updates plus exact-profile tags; role descriptions remain stable
and HCP receives no `UpdateRole` or `UpdateRoleDescription`. The SCP permits and HCP receives no `UpdateRole` or `UpdateRoleDescription`. Live SHOC backend
only the three enumerated HCP apply roles to mutate a `githubdeploy-*` role apply roles may `UpdateAssumeRolePolicy` only on the matching
whose locked `HcpTerraformWorkspace` resource tag equals the caller's immutable `githubdeploy-shoc-backend-{dev,staging}` role. The OU SCP
principal tag. Adding or changing that manager tag remains administrator/CDK `external-dev-iam-guardrails` no longer denies that action; the
only. POC DNS and certificate access is tag/name constrained because their 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 physical IDs are allocated by the temporary retained CDK stack before
Terraform imports them. Dev and staging DNS writes are pinned to their existing Terraform imports them. Dev and staging DNS writes are pinned to their existing
hosted-zone IDs and API record names. 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 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 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 external-dev IAM guardrail SCP is 4,968 compact characters against its
5,120-character Organizations limit; keep size assertions in every change. 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 **External-dev SHOC frontend adoption uses separate gates and creates its
boundaries first.** The two live retained boundaries are boundaries first.** The two live retained boundaries are
@ -851,7 +857,8 @@ aws sts assume-root --target-principal <acct> \
`iam:CreateLoginProfile` — recovery there needs that SCP temporarily `iam:CreateLoginProfile` — recovery there needs that SCP temporarily
detached too. The extdev OU sits at the **5-SCP hard quota**: any new detached too. The extdev OU sits at the **5-SCP hard quota**: any new
guardrail for extdev must attach at the ACCOUNT (396287094661) or 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 **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 account at the org ROOT → it has no root credentials from birth (verify with

View file

@ -71,7 +71,7 @@ new AccountBaselineStack(app, "account-baseline", {
const EXTDEV_FLOW_LOG_VPC_IDS: string[] = []; const EXTDEV_FLOW_LOG_VPC_IDS: string[] = [];
// ── Org structure: OUs + generalized SCPs (management account only) ───────── // ── 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. // `cdk import` post-deploy — see lib/org-governance-stack.ts header + README.
new OrgGovernanceStack(app, "org-governance", { new OrgGovernanceStack(app, "org-governance", {
stackName: "seahaven-org-governance", stackName: "seahaven-org-governance",

View file

@ -398,7 +398,8 @@ export class OrgGovernanceStack extends cdk.Stack {
}); });
retain(denyRootUser); 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 // 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 // = the AWS hard limit per target. Any new guardrail for external-dev
// must attach at the ACCOUNT (396287094661, own 5-slot budget) or // must attach at the ACCOUNT (396287094661, own 5-slot budget) or
@ -436,6 +437,24 @@ export class OrgGovernanceStack extends cdk.Stack {
}); });
retain(externalDevIamGuardrails); 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", { const externalDevProtectSecurity = new organizations.CfnPolicy(this, "ExternalDevProtectSecurity", {
name: "external-dev-protect-security", name: "external-dev-protect-security",
type: "SERVICE_CONTROL_POLICY", type: "SERVICE_CONTROL_POLICY",

View file

@ -57,7 +57,7 @@
{ {
"Sid": "ProtectDeploymentPrincipalLifecycle", "Sid": "ProtectDeploymentPrincipalLifecycle",
"Effect": "Deny", "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-*"], "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-*"] } } "Condition": { "ArnNotLike": { "aws:PrincipalArn": ["arn:aws:iam::396287094661:role/OrganizationAccountAccessRole", "arn:aws:iam::396287094661:role/cdk-hnb659fds-*"] } }
}, },

View file

@ -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"
]
}
}
}
]
}

View file

@ -2649,6 +2649,12 @@ Resources:
- s3:PutBucketPolicy - s3:PutBucketPolicy
- s3:PutBucketPublicAccessBlock - s3:PutBucketPublicAccessBlock
Resource: arn:aws:s3:::elasticbeanstalk-* 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: ShocBackendStagingDeployBoundary:
Type: AWS::IAM::ManagedPolicy Type: AWS::IAM::ManagedPolicy
@ -2702,14 +2708,35 @@ Resources:
Resource: arn:aws:autoscaling:us-east-1:396287094661:autoScalingGroup:*:autoScalingGroupName/awseb-e-6c9m4vb62z-stack-* Resource: arn:aws:autoscaling:us-east-1:396287094661:autoScalingGroup:*:autoScalingGroupName/awseb-e-6c9m4vb62z-stack-*
- Sid: UploadApplicationVersion - Sid: UploadApplicationVersion
Effect: Allow Effect: Allow
Action: s3:PutObject Action:
Resource: arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661/shoc-backend/* - 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 - Sid: UseBeanstalkBucket
Effect: Allow Effect: Allow
Action: Action:
- s3:GetBucketLocation - s3:GetBucketLocation
- s3:ListBucket - s3:ListBucket
- s3:GetBucketPolicy
- s3:GetBucketAcl
- s3:GetBucketVersioning
- s3:GetBucketOwnershipControls
Resource: arn:aws:s3:::elasticbeanstalk-us-east-1-396287094661 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 # Dedicated runtime ceilings preserve the non-AI portions of
# AWSElasticBeanstalkWebTier while removing its 2026 Bedrock/Marketplace # AWSElasticBeanstalkWebTier while removing its 2026 Bedrock/Marketplace
@ -2848,10 +2875,12 @@ Resources:
# DenySelfMutation protects every githubdeploy-* role, while this rehearsal # DenySelfMutation protects every githubdeploy-* role, while this rehearsal
# must adopt three exact githubdeploy roles. Each apply role instead carries # must adopt three exact githubdeploy roles. Each apply role instead carries
# an environment-scoped inline policy. No apply role can create/delete roles, # an environment-scoped inline policy. No apply role can create/delete roles,
# change managed-policy attachments or trust/boundaries, read/write secret # change managed-policy attachments or boundaries, read/write secret
# values, or pass a role. The POC gate controls its new pair independently; # values, or pass a role. Live apply roles may UpdateAssumeRolePolicy only
# the live gate stays false until the four existing dev/staging roles enter # on the matching githubdeploy-shoc-backend-{dev,staging} role so the
# through a CloudFormation IMPORT change set. # 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 # The existing app.terraform.io provider is referenced by literal ARN. The
# stack instance sets CreateOIDCProvider=false, so external-dev never attempts # stack instance sets CreateOIDCProvider=false, so external-dev never attempts
@ -3223,6 +3252,17 @@ Resources:
- secretsmanager:GetResourcePolicy - secretsmanager:GetResourcePolicy
- secretsmanager:ListSecretVersionIds - secretsmanager:ListSecretVersionIds
Resource: arn:aws:secretsmanager:us-east-1:396287094661:secret:shoc/dev/app-config-* 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: HcptfShocBackendDevApplyRole:
Type: AWS::IAM::Role Type: AWS::IAM::Role
@ -3334,6 +3374,17 @@ Resources:
- iam:TagRole - iam:TagRole
- iam:UntagRole - iam:UntagRole
Resource: arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-dev 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 - Sid: TagDevAppConfig
Effect: Allow Effect: Allow
Action: Action:
@ -3410,6 +3461,7 @@ Resources:
Effect: Allow Effect: Allow
Action: Action:
- acm:ListCertificates - acm:ListCertificates
- autoscaling:DescribeAutoScalingGroups
- ec2:DescribeSecurityGroups - ec2:DescribeSecurityGroups
- ec2:DescribeSubnets - ec2:DescribeSubnets
- ec2:DescribeVpcs - ec2:DescribeVpcs
@ -3420,12 +3472,26 @@ Resources:
- elasticbeanstalk:DescribeEnvironments - elasticbeanstalk:DescribeEnvironments
- elasticbeanstalk:ListTagsForResource - elasticbeanstalk:ListTagsForResource
- rds:DescribeDBInstances - rds:DescribeDBInstances
- route53:ListHostedZones
- route53:ListHostedZonesByName - route53:ListHostedZonesByName
Resource: "*" 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 - Sid: ReadSharedCertificate
Effect: Allow Effect: Allow
Action: Action:
- acm:DescribeCertificate - acm:DescribeCertificate
- acm:GetCertificate
- acm:ListTagsForCertificate - acm:ListTagsForCertificate
Resource: arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00 Resource: arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00
- Sid: ReadSharedRdsTags - Sid: ReadSharedRdsTags
@ -3449,6 +3515,17 @@ Resources:
- secretsmanager:GetResourcePolicy - secretsmanager:GetResourcePolicy
- secretsmanager:ListSecretVersionIds - secretsmanager:ListSecretVersionIds
Resource: arn:aws:secretsmanager:us-east-1:396287094661:secret:shoc/staging/app-config-* 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: HcptfShocBackendStagingApplyRole:
Type: AWS::IAM::Role Type: AWS::IAM::Role
@ -3493,6 +3570,13 @@ Resources:
Condition: Condition:
StringEquals: StringEquals:
"iam:PermissionsBoundary": arn:aws:iam::396287094661:policy/shoc-backend-staging-runtime-boundary "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 - Sid: TagStagingRuntimeRole
Effect: Allow Effect: Allow
Action: Action:
@ -3518,6 +3602,17 @@ Resources:
- iam:TagRole - iam:TagRole
- iam:UntagRole - iam:UntagRole
Resource: arn:aws:iam::396287094661:role/githubdeploy-shoc-backend-staging 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 - Sid: TagStagingAppConfig
Effect: Allow Effect: Allow
Action: Action: