Compare commits

...

3 commits

Author SHA1 Message Date
brandizzi
52dbdde576 fix(cdk): allow Beanstalk resource discovery 2026-07-30 12:32:03 -03:00
brandizzi
6fa6277808 fix(cdk): allow load balancer discovery and cancel 2026-07-30 12:26:16 -03:00
brandizzi
4a6632f025 fix(cdk): apply supported Beanstalk S3 policy 2026-07-30 12:20:56 -03:00
2 changed files with 61 additions and 135 deletions

View file

@ -191,10 +191,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` /
`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.
`elasticloadbalancing` / `autoscaling`. CloudFormation discovery and
mutations are limited to the single EB-managed stack prefix. EC2, Elastic
Load Balancing, 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`).
@ -203,12 +204,8 @@ The previous OIDC deployment remained fail-closed after Elastic Beanstalk
reported a generic `s3:GetObjectAcl` denial outside the account-owned source
prefix. AWS Support case `178526484500047` subsequently confirmed that
`UpdateEnvironment` checks objects in AWS-owned Elastic Beanstalk buckets
using the initiating role and requires the `elasticbeanstalk-*/*` resource
namespace. This policy adds only the denied ACL-read action on that namespace;
it intentionally does not copy the managed
`AdministratorAccess-AWSElasticBeanstalk` policy's broad `s3:Get*`,
`s3:Put*`, or `s3:Delete*` grants. Any later denial must be evaluated and
granted independently.
using the initiating role and requires the service-wide
`elasticbeanstalk-*` bucket/object namespaces.
The July 30 deployment of backend PR #41 then reached `UpdateEnvironment` and
failed on `ec2:DescribeVpcs`. Elastic Beanstalk performs this read-only network
@ -216,22 +213,37 @@ discovery using the initiating role, so the CDK policy includes that action
alongside the existing EC2 describe permissions. It remains resource `*`
because `DescribeVpcs` does not support resource-level permissions.
After that permission was deployed, the next exact rerun reached the S3 cleanup
phase and failed because Elastic Beanstalk could not delete one or more
temporary objects. The service does not expose the AWS-owned bucket/key in its
event, so `s3:DeleteObject` is scoped to the same AWS-confirmed
`arn:aws:s3:::elasticbeanstalk-*/*` object namespace as `GetObjectAcl`. The
policy does not grant wildcard S3 actions or access to non-Beanstalk buckets.
Successive exact reruns then reached S3 cleanup, the delegated CloudFormation
update, and the CloudFormation template fetch. The observed failures were
`s3:DeleteObject`, `cloudformation:UpdateStack`, and finally an opaque
CloudFormation `S3 error: Access Denied` after narrower object reads had been
added. Because AWS does not expose the AWS-owned bucket/key or exact internal
S3 read in that final error, the CDK now uses AWS Support's authoritative
UpdateEnvironment S3 set:
The following rerun passed both denials and reached the delegated
CloudFormation update, which required `cloudformation:UpdateStack`. That
mutation is limited to the single existing Elastic Beanstalk-managed stack ARN
for `shoc-backend-dev`; it cannot create stacks or update any other stack.
- `s3:Delete*`, `s3:Get*`, and `s3:Put*` on
`arn:aws:s3:::elasticbeanstalk-*/*`.
- `s3:GetBucket*`, `s3:ListBucket`, `s3:PutBucketPolicy`,
`s3:PutBucketPublicAccessBlock`, and `s3:PutBucketOwnershipControls` on
`arn:aws:s3:::elasticbeanstalk-*`.
Once `UpdateStack` was allowed, CloudFormation returned `S3 error: Access
Denied` while resolving the Elastic Beanstalk-managed template. The caller
therefore receives `s3:GetObject` on the same Beanstalk-only object namespace;
the policy still does not grant `s3:Get*` or access to other bucket names.
`s3:CreateBucket` remains excluded because this workflow targets an existing
application/environment and explicitly disables bucket creation. No S3 access
is granted to non-Elastic-Beanstalk bucket names. The CloudFormation mutation
remains limited to the single existing `shoc-backend-dev` managed stack ARN; it
cannot create stacks or update another stack.
The next rerun cleared S3 and then required the read-only
`elasticloadbalancing:DescribeLoadBalancers` discovery action. Its failed
managed-stack update also required `cloudformation:CancelUpdateStack`; the
cancel action is scoped to the same single stack ARN as `UpdateStack`.
The subsequent rerun progressed into Auto Scaling and required
`autoscaling:DescribeLaunchConfigurations`. Because Elastic Beanstalk's
managed update workflow performs variable resource discovery, the role follows
the documented read-only discovery families for EC2, Elastic Load Balancing,
and Auto Scaling (`Describe*`). These grants expose metadata across the account
but do not authorize any mutation; write actions remain separately scoped.
The pinned deployment action can return success after Elastic Beanstalk emits a
fatal deployment event. The following workflow step therefore verifies that

View file

@ -9,7 +9,6 @@ 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}`;
export class DeployDevStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) {
@ -17,19 +16,6 @@ export class DeployDevStack extends cdk.Stack {
const applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`;
const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_NAME}`;
const bucketArn = `arn:aws:s3:::${BUCKET_NAME}`;
const runtimeVersionArn =
`${bucketArn}/resources/environments/${ENVIRONMENT_ID}` +
`/_runtime/_versions/${APPLICATION_NAME}/*`;
const embeddedExtensionArn =
`${bucketArn}/resources/_runtime/_embedded_extensions/` +
`${APPLICATION_NAME}/*`;
const environmentEmbeddedExtensionArn =
`${bucketArn}/resources/environments/${ENVIRONMENT_ID}` +
`/_runtime/_embedded_extensions/${APPLICATION_NAME}/*`;
const runtimeManifestArn =
`${bucketArn}/resources/environments/${ENVIRONMENT_ID}` +
'/_runtime/versions/*';
const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`;
const deployRole = new iam.Role(this, 'GithubDeployRole', {
@ -57,9 +43,12 @@ export class DeployDevStack extends cdk.Stack {
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'autoscaling:Describe*',
'ec2:Describe*',
'elasticbeanstalk:DescribeEnvironments',
'elasticbeanstalk:DescribeApplicationVersions',
'elasticbeanstalk:DescribeEvents',
'elasticloadbalancing:Describe*',
],
resources: ['*'],
}),
@ -94,6 +83,7 @@ export class DeployDevStack extends cdk.Stack {
'cloudformation:DescribeStackResources',
'cloudformation:DescribeStacks',
'cloudformation:ListStackResources',
'cloudformation:CancelUpdateStack',
'cloudformation:UpdateStack',
],
resources: [
@ -102,30 +92,6 @@ export class DeployDevStack extends cdk.Stack {
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ec2:DescribeAvailabilityZones',
'ec2:DescribeImages',
'ec2:DescribeSubnets',
'ec2:DescribeVpcs',
],
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,
@ -143,83 +109,31 @@ export class DeployDevStack extends cdk.Stack {
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:ListBucket',
's3:CreateBucket',
's3:PutBucketOwnershipControls',
's3:GetBucketLocation',
's3:GetBucketPolicy',
],
resources: [bucketArn],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject', 's3:GetObjectVersion', 's3:PutObject'],
resources: [`${bucketArn}/${APPLICATION_NAME}/*`],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:DeleteObject',
's3:GetObject',
's3:GetObjectVersionAcl',
's3:PutObject',
's3:PutObjectVersionAcl',
],
// UpdateEnvironment copies the uploaded source bundle into this
// environment-specific runtime prefix, verifies the temporary copy,
// preserves its version ACL, and removes it after registration.
resources: [runtimeVersionArn],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:PutObject'],
// UpdateEnvironment materializes the application's embedded-extension
// manifest under the shared and environment-specific runtime prefixes.
resources: [embeddedExtensionArn, environmentEmbeddedExtensionArn],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject'],
// Elastic Beanstalk verifies the environment copy with HeadObject.
resources: [environmentEmbeddedExtensionArn],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject', 's3:PutObject'],
// UpdateEnvironment reads the prior environment version manifest and
// writes its replacement under this environment-only runtime prefix.
resources: [runtimeManifestArn],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:DeleteObject', 's3:GetObject', 's3:GetObjectAcl'],
// UpdateEnvironment also checks and removes temporary objects in
// AWS-owned Elastic Beanstalk buckets. AWS Support case
// 178526484500047 confirmed that the caller's identity policy must
// cover the service-wide bucket namespace.
actions: ['s3:Delete*', 's3:Get*', 's3:Put*'],
// AWS Support case 178526484500047 confirmed that UpdateEnvironment
// reads, writes, versions, ACL-checks, and removes objects in both the
// account bucket and AWS-owned Elastic Beanstalk service buckets.
resources: ['arn:aws:s3:::elasticbeanstalk-*/*'],
}),
);
deployRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:GetBucket*',
's3:ListBucket',
's3:PutBucketOwnershipControls',
's3:PutBucketPolicy',
's3:PutBucketPublicAccessBlock',
],
// This is AWS Support's bucket-level UpdateEnvironment set, excluding
// CreateBucket because the workflow deploys only to an existing
// application/environment and disables bucket creation.
resources: ['arn:aws:s3:::elasticbeanstalk-*'],
}),
);
new cdk.CfnOutput(this, 'GithubDeployRoleArn', {
value: deployRole.roleArn,
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',