shoc-backend/infra/cdk
2026-07-29 10:33:31 -03:00
..
app.ts ci: automate dev backend deployment 2026-07-27 19:26:07 -03:00
cdk.json ci: automate dev backend deployment 2026-07-27 19:26:07 -03:00
deploy-dev-stack.ts fix(cdk): allow EB extension verification 2026-07-29 10:33:31 -03:00
package-lock.json ci: automate dev backend deployment 2026-07-27 19:26:07 -03:00
package.json ci: automate dev backend deployment 2026-07-27 19:26:07 -03:00
README.md fix(cdk): allow EB extension verification 2026-07-29 10:33:31 -03:00
tsconfig.json ci: automate dev backend deployment 2026-07-27 19:26:07 -03:00

shoc-backend CDK (dev deployment IAM)

This CDK v2 app owns exactly one thing in the shoc-backend AWS account (396287094661, us-east-1): the GitHub OIDC deploy role used by the dev deployment workflow in .github/workflows/deploy.yml.

Ownership boundary (deliberate)

CDK owns:

  • The IAM role githubdeploy-shoc-backend-dev.
  • Its OIDC trust relationship to arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com scoped to repo:Sea-Haven-Industries/shoc-backend:environment:dev.
  • Its least-privilege inline permissions policy.

CDK does not own, create, import, replace, or modify any of the following. They are referenced by exact identifier only and remain owned by their original provisioning path:

  • Elastic Beanstalk application shoc-backend
  • Elastic Beanstalk environment shoc-backend-dev
  • DNS, VPC, EC2, RDS, and existing service/instance roles
  • S3 bucket elasticbeanstalk-us-east-1-396287094661
  • Environment configuration / option settings
  • Database schema (migrations are applied by Elastic Beanstalk at deploy time, not by CDK)

The role is retained on stack deletion (DeletionPolicy=Retain, UpdateReplacePolicy=Retain) so an accidental teardown cannot orphan the trust or lock out deployments.

Least-privilege policy summary

The role grants only:

  • The three read-only Elastic Beanstalk actions used by deploy, wait, and rollback (DescribeApplicationVersions, DescribeEnvironments, and DescribeEvents). These use Resource: "*" because Elastic Beanstalk describe actions are not reliably constrained by resource ARN.

  • elasticbeanstalk:CreateApplicationVersion on application shoc-backend.

  • 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 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: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 contract even though the workflow sets create-s3-bucket-if-not-exists: "false". That input prevents the action's explicit bucket-creation helper; it does not remove the permission required by the subsequent Elastic Beanstalk update path. A live deployment confirmed this boundary: CreateApplicationVersion succeeded, then UpdateEnvironment was denied because the caller lacked s3:CreateBucket on the service bucket. The permission is scoped to that exact bucket-level ARN only (no object prefix, no wildcard resource), so it cannot create any other bucket.

    s3:PutBucketOwnershipControls was added after a second live deployment (run 30375409934) failed at UpdateEnvironment with AccessDenied for s3:PutBucketOwnershipControls on the same service bucket. That call is emitted by Elastic Beanstalk's UpdateEnvironment path after the source bundle upload succeeds; AWS classifies it as a bucket-level permission, so it is scoped to the same exact bucket-level ARN (no object prefix, no wildcard resource). It does not widen object-prefix permissions, does not grant PutBucketPolicy, PutBucketPublicAccessBlock, or any object-level write, and does not change create-s3-bucket-if-not-exists: "false".

    s3:GetBucketLocation was added after CloudTrail showed that run 30375409934 attempt 4 invoked it as githubdeploy-shoc-backend-dev/GitHubActions and was denied. It is scoped to the exact bucket-level ARN and grants no object access.

  • The six CloudFormation discovery calls observed across the failed OIDC and successful administrator deployments (DescribeStackEvents, DescribeStackResource, DescribeStackResources, DescribeStacks, GetTemplate, and ListStackResources) on the Elastic Beanstalk-managed stack awseb-e-hehnrqjjrt-stack, scoped to arn:aws:cloudformation:us-east-1:396287094661:stack/awseb-e-hehnrqjjrt-stack/*. These read-only calls are emitted by Elastic Beanstalk's UpdateEnvironment path under the GitHub deploy role. GetTemplate was added after run 30375409934 attempt 2 advanced past the S3 ownership-controls step and was denied on the EB-managed stack instance awseb-e-hehnrqjjrt-stack/112f77c0-7718-11f1-a1a9-0e48750aef13. CloudTrail then showed attempt 4 denied DescribeStackResources and ListStackResources on that same stack instance. CloudFormation stack ARNs carry a random GUID instance suffix, so the permission is scoped to that one stack-name prefix (/*) rather than a single instance ARN. The statement grants no CloudFormation mutation, no Resource: "*", and no access to any other stack. CDK does not own or mutate that stack; it is owned by Elastic Beanstalk and referenced by identifier only.

  • ec2:DescribeAvailabilityZones, ec2:DescribeImages, and ec2:DescribeSubnets as read-only account-level discovery queries. CloudTrail identified the GitHub deploy role as the caller during run 30375409934; attempt 5 confirmed the first two denials after DescribeSubnets was allowed. EC2 does not support resource-level constraints for these Describe actions, so IAM requires Resource: "*". No EC2 mutation action is granted.

  • The Auto Scaling discovery calls DescribeAutoScalingGroups and DescribeScalingActivities on Resource: "*" plus PutNotificationConfiguration, ResumeProcesses, and SuspendProcesses on only Auto Scaling groups whose name starts with awseb-e-hehnrqjjrt-stack-. These are the exact calls recorded during the successful administrator deployment. AWS supports resource-level constraints for all three mutations, so replacement ASGs remain covered without granting access to another environment. 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; the only service-wide object grant is read-only ACL metadata.

Prerequisites

  • Node >= 22.22.1 and npm.
  • AWS credentials authorized to create/inspect CloudFormation, IAM roles, and trust policies in account 396287094661.
  • The GitHub OIDC provider arn:aws:iam::396287094661:oidc-provider/token.actions.githubusercontent.com must already exist in the account (created once, outside this stack).

Commands

npm ci                          # install pinned dependencies
npm run build                   # type-check / compile to dist/
npm run synth                   # synthesize the CloudFormation template
npm run diff                    # diff deployed stack vs local (requires AWS)
npm run deploy                  # deploy the stack (requires AWS)

All commands run from infra/cdk/.

CI integration

npm run synth is the deterministic local/CI validation. After synth, inspect cdk.out/shoc-backend-deploy-dev.template.json and verify the synthesized AWS::IAM::Role:

  • 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.

The workflow's AWS_DEPLOY_ROLE_ARN repository secret (environment dev) must hold the ARN output by this stack (GithubDeployRoleArn).

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 the exact immutable version label is active and healthy before smoke testing. Any mismatch fails and invokes rollback. This guard prevents false success; it does not make the unresolved OIDC deployment path release-ready.

The GitHub dev environment is an external release control and must restrict deployments to the dev branch. Required reviewers should be configured when the repository plan supports environment reviewers. The workflow also checks the exact branch before requesting an OIDC token.

Migration and recovery contract

The deployment bundle applies pending EF Core migrations before the new application starts. Migrations must therefore use an expand/contract sequence:

  • Expand changes must remain backward compatible with the previously deployed application version.
  • Destructive contract changes are deployed only after all application versions relying on the old schema have been retired.
  • A failed deployment restores the previous application version only. Database schema is not downgraded, and schema rollback is not claimed.

This contract preserves the usefulness of application-version recovery without misrepresenting it as a tested database downgrade.