diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 37446c4..e32d030 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -42,12 +42,48 @@ The role grants only: - `elasticbeanstalk:UpdateEnvironment` on environment `shoc-backend-dev` only. - `s3:ListBucket` and `s3:GetBucketLocation` on `elasticbeanstalk-us-east-1-396287094661` (the official action's - ownership-safe bucket checks), plus `s3:CreateBucket` and + ownership-safe bucket checks), `s3:GetBucketPolicy` for the policy inspection + observed in attempt 11 of run `30448885838`, plus `s3:CreateBucket` and `s3:PutBucketOwnershipControls` on the same bucket-level ARN. Under the `shoc-backend/` object prefix only: `s3:PutObject`, `s3:GetObject`, and `s3:GetObjectVersion`, which the pinned official deployment action requires to validate the `CreateApplicationVersion` source bundle after upload. +- `s3:PutObject`, `s3:GetObject`, `s3:GetObjectVersionAcl`, + `s3:PutObjectVersionAcl`, and `s3:DeleteObject` on only + `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_versions/shoc-backend/*`. + Elastic Beanstalk copies each uploaded source bundle into this + environment-specific runtime prefix during `UpdateEnvironment`, verifies it + with `HeadObject` (authorized by `s3:GetObject`), and removes the temporary + copy after the version is registered. Attempts 1 through 4 of run + `30448885838` exposed the exact source, destination, cleanup, and verification + operations after the earlier ACL denial was resolved. CloudTrail recorded + the exact `s3:GetObject` denial on attempt 4; attempt 6 then exposed the + version-specific ACL read performed on the copied object; attempt 7 exposed + the matching version-ACL write. The grant does not cover another + environment, another application, source bundles, object content versions, + non-version ACL mutation, tags, or retention. +- `s3:PutObject` on only the two embedded-extension prefixes + `elasticbeanstalk-us-east-1-396287094661/resources/_runtime/_embedded_extensions/shoc-backend/*` + and + `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/_embedded_extensions/shoc-backend/*`. + After the runtime bundle copy and version-ACL operations succeeded, attempt 8 + of run `30448885838` showed Elastic Beanstalk materializing the application's + embedded-extension manifest at the application-specific shared prefix. + Attempt 9 then showed the matching write into the exact dev-environment + prefix. CloudTrail recorded both denied actions and object ARNs. The grant + does not include reads, deletes, ACL mutation, another application, + another environment, or another bucket. +- `s3:GetObject` on only the environment-specific embedded-extension prefix + above. Attempt 10 showed that Elastic Beanstalk verifies the materialized + environment copy with `HeadObject`, which S3 authorizes through + `s3:GetObject`. The shared embedded-extension prefix remains write-only. +- `s3:GetObject` and `s3:PutObject` on only + `elasticbeanstalk-us-east-1-396287094661/resources/environments/e-hehnrqjjrt/_runtime/versions/*`. + Attempt 12 showed Elastic Beanstalk reading the previous environment version + manifest and writing its replacement under this exact dev-environment + runtime prefix. The grant excludes deletes, ACL mutation, other environments, + and application bundle content. - `s3:GetObjectAcl` on objects under the service-wide `arn:aws:s3:::elasticbeanstalk-*/*` namespace. AWS Support case `178526484500047` confirmed that `UpdateEnvironment` uses the initiating diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 36addaf..0cdef0c 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -18,6 +18,18 @@ 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', { @@ -134,6 +146,7 @@ export class DeployDevStack extends cdk.Stack { 's3:CreateBucket', 's3:PutBucketOwnershipControls', 's3:GetBucketLocation', + 's3:GetBucketPolicy', ], resources: [bucketArn], }), @@ -147,6 +160,52 @@ export class DeployDevStack extends cdk.Stack { }), ); + 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,