diff --git a/infra/cdk/README.md b/infra/cdk/README.md index 1d11f47..95c687c 100644 --- a/infra/cdk/README.md +++ b/infra/cdk/README.md @@ -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 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 +212,25 @@ 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 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 99d64fa..d6f14e4 100644 --- a/infra/cdk/deploy-dev-stack.ts +++ b/infra/cdk/deploy-dev-stack.ts @@ -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', { @@ -143,83 +129,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.',