diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 93273f3..e32d030 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -42,13 +42,56 @@ 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`, `s3:GetObjectAcl`, and - `s3:GetObjectVersion`, which the pinned official deployment action and - Elastic Beanstalk require to validate the `CreateApplicationVersion` source - bundle after upload. + `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 + role to inspect objects in AWS-owned Elastic Beanstalk buckets, not only the + account-owned source-bundle bucket. The wildcard is limited to one read-only + ACL action and the Elastic Beanstalk bucket namespace; it grants no object + content read, write, delete, bucket-management, IAM, or `PassRole` + capability. `s3:CreateBucket` is part of the pinned `aws-actions/aws-elastic-beanstalk-deploy@cfad3e5e...` (v1.0.6) IAM @@ -112,15 +155,11 @@ The role grants only: 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. It grants **no** IAM mutation or `PassRole`, **no** RDS / Secrets Manager 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. +this environment's ASG name pattern. There are no wildcard mutation surfaces; +the only service-wide object grant is read-only ACL metadata. ## Prerequisites @@ -160,13 +199,16 @@ All commands run from `infra/cdk/`. The workflow's `AWS_DEPLOY_ROLE_ARN` repository secret (environment `dev`) must hold the ARN output by this stack (`GithubDeployRoleArn`). -The OIDC deployment is currently fail-closed, not repaired. Elastic Beanstalk -still reports a generic `s3:GetObjectAcl` denial after the account-owned source -prefix and every exact cross-account object referenced by the sanitized live -stack were tested independently. The runtime-prefix, platform-assets, and -launch-control hypotheses were disproved and are intentionally absent from the -policy. Do not broaden `GetObjectAcl` without an exact principal/action/resource -record from AWS Support or the AWS-owned bucket's diagnostic owner. +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. The pinned deployment action can return success after Elastic Beanstalk emits a fatal deployment event. The following workflow step therefore verifies that diff --git a/infra/cdk/deploy-dev-stack.ts b/infra/cdk/deploy-dev-stack.ts index 3575452..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], }), @@ -142,11 +155,68 @@ export class DeployDevStack extends cdk.Stack { deployRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, - actions: ['s3:GetObject', 's3:GetObjectAcl', 's3:GetObjectVersion', 's3:PutObject'], + 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:GetObjectAcl'], + // UpdateEnvironment also checks 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-*/*'], + }), + ); + new cdk.CfnOutput(this, 'GithubDeployRoleArn', { value: deployRole.roleArn, description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',