fix(cdk): apply supported Beanstalk S3 policy

This commit is contained in:
brandizzi 2026-07-30 12:20:56 -03:00
parent 9966c22240
commit 4a6632f025
2 changed files with 40 additions and 107 deletions

View file

@ -203,12 +203,8 @@ The previous OIDC deployment remained fail-closed after Elastic Beanstalk
reported a generic `s3:GetObjectAcl` denial outside the account-owned source reported a generic `s3:GetObjectAcl` denial outside the account-owned source
prefix. AWS Support case `178526484500047` subsequently confirmed that prefix. AWS Support case `178526484500047` subsequently confirmed that
`UpdateEnvironment` checks objects in AWS-owned Elastic Beanstalk buckets `UpdateEnvironment` checks objects in AWS-owned Elastic Beanstalk buckets
using the initiating role and requires the `elasticbeanstalk-*/*` resource using the initiating role and requires the service-wide
namespace. This policy adds only the denied ACL-read action on that namespace; `elasticbeanstalk-*` bucket/object namespaces.
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.
The July 30 deployment of backend PR #41 then reached `UpdateEnvironment` and The July 30 deployment of backend PR #41 then reached `UpdateEnvironment` and
failed on `ec2:DescribeVpcs`. Elastic Beanstalk performs this read-only network failed on `ec2:DescribeVpcs`. Elastic Beanstalk performs this read-only network
@ -216,22 +212,25 @@ discovery using the initiating role, so the CDK policy includes that action
alongside the existing EC2 describe permissions. It remains resource `*` alongside the existing EC2 describe permissions. It remains resource `*`
because `DescribeVpcs` does not support resource-level permissions. because `DescribeVpcs` does not support resource-level permissions.
After that permission was deployed, the next exact rerun reached the S3 cleanup Successive exact reruns then reached S3 cleanup, the delegated CloudFormation
phase and failed because Elastic Beanstalk could not delete one or more update, and the CloudFormation template fetch. The observed failures were
temporary objects. The service does not expose the AWS-owned bucket/key in its `s3:DeleteObject`, `cloudformation:UpdateStack`, and finally an opaque
event, so `s3:DeleteObject` is scoped to the same AWS-confirmed CloudFormation `S3 error: Access Denied` after narrower object reads had been
`arn:aws:s3:::elasticbeanstalk-*/*` object namespace as `GetObjectAcl`. The added. Because AWS does not expose the AWS-owned bucket/key or exact internal
policy does not grant wildcard S3 actions or access to non-Beanstalk buckets. 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 - `s3:Delete*`, `s3:Get*`, and `s3:Put*` on
CloudFormation update, which required `cloudformation:UpdateStack`. That `arn:aws:s3:::elasticbeanstalk-*/*`.
mutation is limited to the single existing Elastic Beanstalk-managed stack ARN - `s3:GetBucket*`, `s3:ListBucket`, `s3:PutBucketPolicy`,
for `shoc-backend-dev`; it cannot create stacks or update any other stack. `s3:PutBucketPublicAccessBlock`, and `s3:PutBucketOwnershipControls` on
`arn:aws:s3:::elasticbeanstalk-*`.
Once `UpdateStack` was allowed, CloudFormation returned `S3 error: Access `s3:CreateBucket` remains excluded because this workflow targets an existing
Denied` while resolving the Elastic Beanstalk-managed template. The caller application/environment and explicitly disables bucket creation. No S3 access
therefore receives `s3:GetObject` on the same Beanstalk-only object namespace; is granted to non-Elastic-Beanstalk bucket names. The CloudFormation mutation
the policy still does not grant `s3:Get*` or access to other bucket names. remains limited to the single existing `shoc-backend-dev` managed stack ARN; it
cannot create stacks or update another stack.
The pinned deployment action can return success after Elastic Beanstalk emits a The pinned deployment action can return success after Elastic Beanstalk emits a
fatal deployment event. The following workflow step therefore verifies that 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_ID = 'e-hehnrqjjrt';
const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`; const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`;
const REPO = 'Sea-Haven-Industries/shoc-backend'; const REPO = 'Sea-Haven-Industries/shoc-backend';
const BUCKET_NAME = `elasticbeanstalk-${REGION}-${ACCOUNT_ID}`;
export class DeployDevStack extends cdk.Stack { export class DeployDevStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) { 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 applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`;
const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_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 oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`;
const deployRole = new iam.Role(this, 'GithubDeployRole', { const deployRole = new iam.Role(this, 'GithubDeployRole', {
@ -143,83 +129,31 @@ export class DeployDevStack extends cdk.Stack {
deployRole.addToPolicy( deployRole.addToPolicy(
new iam.PolicyStatement({ new iam.PolicyStatement({
effect: iam.Effect.ALLOW, effect: iam.Effect.ALLOW,
actions: [ actions: ['s3:Delete*', 's3:Get*', 's3:Put*'],
's3:ListBucket', // AWS Support case 178526484500047 confirmed that UpdateEnvironment
's3:CreateBucket', // reads, writes, versions, ACL-checks, and removes objects in both the
's3:PutBucketOwnershipControls', // account bucket and AWS-owned Elastic Beanstalk service buckets.
'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.
resources: ['arn:aws:s3:::elasticbeanstalk-*/*'], 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', { new cdk.CfnOutput(this, 'GithubDeployRoleArn', {
value: deployRole.roleArn, value: deployRole.roleArn,
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.', description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',