From fa99240af50edd8dacae62f59f280bf63d2a770a Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 28 Jul 2026 14:03:10 -0300 Subject: [PATCH] fix(cdk): model EB deployment capability --- infra/cdk/README.md | 50 +++++++++++++++++++++++++---------- infra/cdk/deploy-dev-stack.ts | 46 ++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/infra/cdk/README.md b/infra/cdk/README.md index b3953b7..6e27be7 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -77,9 +77,11 @@ The role grants only: `githubdeploy-shoc-backend-dev/GitHubActions` and was denied. It is scoped to the exact bucket-level ARN and grants no object access. -- `cloudformation:GetTemplate`, `cloudformation:DescribeStackResources`, and - `cloudformation:ListStackResources` on the Elastic Beanstalk-managed - CloudFormation stack `awseb-e-hehnrqjjrt-stack`, scoped to +- The six CloudFormation discovery calls observed across the failed OIDC and + successful administrator deployments (`DescribeStackEvents`, + `DescribeStackResource`, `DescribeStackResources`, `DescribeStacks`, + `GetTemplate`, and `ListStackResources`) on the Elastic Beanstalk-managed + stack `awseb-e-hehnrqjjrt-stack`, scoped to `arn:aws:cloudformation:us-east-1:396287094661:stack/awseb-e-hehnrqjjrt-stack/*`. These read-only calls are emitted by Elastic Beanstalk's `UpdateEnvironment` path under the GitHub deploy role. `GetTemplate` was @@ -95,20 +97,39 @@ The role grants only: mutate that stack; it is owned by Elastic Beanstalk and referenced by identifier only. -- `ec2:DescribeSubnets` as a read-only account-level query. Elastic Beanstalk - emitted this call during run 30375409934 attempt 3 after the environment - update started. EC2 does not support resource-level constraints for this - Describe action, so IAM requires `Resource: "*"`. No EC2 mutation action is - granted. +- `ec2:DescribeAvailabilityZones`, `ec2:DescribeImages`, and + `ec2:DescribeSubnets` as read-only account-level discovery queries. + CloudTrail identified the GitHub deploy role as the caller during run + `30375409934`; attempt 5 confirmed the first two denials after + `DescribeSubnets` was allowed. EC2 does not support resource-level + constraints for these Describe actions, so IAM requires `Resource: "*"`. + No EC2 mutation action is granted. +- The Auto Scaling discovery calls `DescribeAutoScalingGroups` and + `DescribeScalingActivities` on `Resource: "*"` plus + `PutNotificationConfiguration`, `ResumeProcesses`, and `SuspendProcesses` + on only Auto Scaling groups whose name starts with + `awseb-e-hehnrqjjrt-stack-`. These are the exact calls recorded during the + successful administrator deployment. AWS supports resource-level + constraints for all three mutations, so replacement ASGs remain covered + without granting access to another environment. - `s3:GetObjectAcl` under the existing `elasticbeanstalk-us-east-1-396287094661/shoc-backend/*` object prefix. Elastic Beanstalk emitted this read during the same attempt while validating the uploaded application bundle. It grants no bucket-wide or cross-prefix object access. +- `s3:GetObjectAcl` on only + `resources/environments/e-hehnrqjjrt/_runtime/*` in the same bucket. A + successful break-glass deployment showed that EB copies each immutable + application version, its embedded extensions, and its manifest under that + exact environment runtime prefix. The failed OIDC path already allowed the + source object, so the remaining ACL validation is limited to that proven + runtime boundary. This grant exposes ACL metadata only; it does not grant + runtime content reads or writes. It grants **no** IAM mutation or `PassRole`, **no** RDS / Secrets Manager -access, no EC2 mutation, and **no** administrator policy. There are no wildcard -mutation surfaces. +access, no EC2 mutation, and **no** administrator policy. The only non-EB/S3 +mutations are the three deployment-process Auto Scaling calls, restricted to +this environment's ASG name pattern. There are no wildcard mutation surfaces. ## Prerequisites @@ -139,10 +160,11 @@ All commands run from `infra/cdk/`. - Trust policy `StringEquals` matches the exact audience and subject above. - The inline policy contains no `Resource: "*"` mutation action and no service - outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2`. The - `cloudformation` grants are the three observed read-only actions on the - single EB-managed stack prefix; the only EC2 grant is the read-only - `DescribeSubnets` action whose IAM resource model requires `Resource: "*"`. + outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2` / + `autoscaling`. CloudFormation discovery is limited to the single EB-managed + stack prefix. EC2 and Auto Scaling discovery use `Resource: "*"` only where + the IAM resource model requires it; Auto Scaling mutations are limited to + this environment's ASG name pattern. The workflow's `AWS_DEPLOY_ROLE_ARN` repository secret (environment `dev`) must hold the ARN output by this stack (`GithubDeployRoleArn`). diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 9302a9f..bd56198 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -6,6 +6,8 @@ const ACCOUNT_ID = '396287094661'; const REGION = 'us-east-1'; const APPLICATION_NAME = 'shoc-backend'; const ENVIRONMENT_NAME = 'shoc-backend-dev'; +const ENVIRONMENT_ID = 'e-hehnrqjjrt'; +const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`; const REPO = 'Sea-Haven-Industries/shoc-backend'; const BUCKET_NAME = `elasticbeanstalk-${REGION}-${ACCOUNT_ID}`; @@ -74,12 +76,15 @@ export class DeployDevStack extends cdk.Stack { new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ + 'cloudformation:DescribeStackEvents', + 'cloudformation:DescribeStackResource', 'cloudformation:GetTemplate', 'cloudformation:DescribeStackResources', + 'cloudformation:DescribeStacks', 'cloudformation:ListStackResources', ], resources: [ - `arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/awseb-e-hehnrqjjrt-stack/*`, + `arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/${ENVIRONMENT_STACK_NAME}/*`, ], }), ); @@ -87,11 +92,40 @@ export class DeployDevStack extends cdk.Stack { deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - actions: ['ec2:DescribeSubnets'], + actions: [ + 'ec2:DescribeAvailabilityZones', + 'ec2:DescribeImages', + 'ec2:DescribeSubnets', + ], resources: ['*'], }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'autoscaling:DescribeAutoScalingGroups', + 'autoscaling:DescribeScalingActivities', + ], + resources: ['*'], + }), + ); + + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 'autoscaling:PutNotificationConfiguration', + 'autoscaling:ResumeProcesses', + 'autoscaling:SuspendProcesses', + ], + resources: [ + `arn:aws:autoscaling:${REGION}:${ACCOUNT_ID}:autoScalingGroup:*:autoScalingGroupName/${ENVIRONMENT_STACK_NAME}-*`, + ], + }), + ); + deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, @@ -113,6 +147,14 @@ export class DeployDevStack extends cdk.Stack { }), ); + deployRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['s3:GetObjectAcl'], + resources: [`${bucketArn}/resources/environments/${ENVIRONMENT_ID}/_runtime/*`], + }), + ); + new cdk.CfnOutput(this, 'GithubDeployRoleArn', { value: deployRole.roleArn, description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',