13 KiB
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.comscoped torepo: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, andDescribeEvents). These useResource: "*"because Elastic Beanstalk describe actions are not reliably constrained by resource ARN. -
elasticbeanstalk:CreateApplicationVersionon applicationshoc-backend. -
elasticbeanstalk:UpdateEnvironmenton environmentshoc-backend-devonly. -
s3:ListBucketands3:GetBucketLocationonelasticbeanstalk-us-east-1-396287094661(the official action's ownership-safe bucket checks),s3:GetBucketPolicyfor the policy inspection observed in attempt 11 of run30448885838, pluss3:CreateBucketands3:PutBucketOwnershipControlson the same bucket-level ARN. Under theshoc-backend/object prefix only:s3:PutObject,s3:GetObject, ands3:GetObjectVersion, which the pinned official deployment action requires to validate theCreateApplicationVersionsource bundle after upload. -
s3:PutObject,s3:GetObject,s3:GetObjectVersionAcl,s3:PutObjectVersionAcl, ands3:DeleteObjecton onlyelasticbeanstalk-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 duringUpdateEnvironment, verifies it withHeadObject(authorized bys3:GetObject), and removes the temporary copy after the version is registered. Attempts 1 through 4 of run30448885838exposed the exact source, destination, cleanup, and verification operations after the earlier ACL denial was resolved. CloudTrail recorded the exacts3:GetObjectdenial 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:PutObjecton only the two embedded-extension prefixeselasticbeanstalk-us-east-1-396287094661/resources/_runtime/_embedded_extensions/shoc-backend/*andelasticbeanstalk-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 run30448885838showed 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:GetObjecton only the environment-specific embedded-extension prefix above. Attempt 10 showed that Elastic Beanstalk verifies the materialized environment copy withHeadObject, which S3 authorizes throughs3:GetObject. The shared embedded-extension prefix remains write-only. -
s3:GetObjectands3:PutObjecton onlyelasticbeanstalk-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:GetObjectAclon objects under the service-widearn:aws:s3:::elasticbeanstalk-*/*namespace. AWS Support case178526484500047confirmed thatUpdateEnvironmentuses 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, orPassRolecapability.s3:CreateBucketis part of the pinnedaws-actions/aws-elastic-beanstalk-deploy@cfad3e5e...(v1.0.6) IAM contract even though the workflow setscreate-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:CreateApplicationVersionsucceeded, thenUpdateEnvironmentwas denied because the caller lackeds3:CreateBucketon 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:PutBucketOwnershipControlswas added after a second live deployment (run 30375409934) failed atUpdateEnvironmentwithAccessDeniedfors3:PutBucketOwnershipControlson the same service bucket. That call is emitted by Elastic Beanstalk'sUpdateEnvironmentpath 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 grantPutBucketPolicy,PutBucketPublicAccessBlock, or any object-level write, and does not changecreate-s3-bucket-if-not-exists: "false".s3:GetBucketLocationwas added after CloudTrail showed that run30375409934attempt 4 invoked it asgithubdeploy-shoc-backend-dev/GitHubActionsand 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, andListStackResources) on the Elastic Beanstalk-managed stackawseb-e-hehnrqjjrt-stack, scoped toarn:aws:cloudformation:us-east-1:396287094661:stack/awseb-e-hehnrqjjrt-stack/*. These read-only calls are emitted by Elastic Beanstalk'sUpdateEnvironmentpath under the GitHub deploy role.GetTemplatewas added after run30375409934attempt 2 advanced past the S3 ownership-controls step and was denied on the EB-managed stack instanceawseb-e-hehnrqjjrt-stack/112f77c0-7718-11f1-a1a9-0e48750aef13. CloudTrail then showed attempt 4 deniedDescribeStackResourcesandListStackResourceson 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, noResource: "*", 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, andec2:DescribeSubnetsas read-only account-level discovery queries. CloudTrail identified the GitHub deploy role as the caller during run30375409934; attempt 5 confirmed the first two denials afterDescribeSubnetswas allowed. EC2 does not support resource-level constraints for these Describe actions, so IAM requiresResource: "*". No EC2 mutation action is granted. -
The Auto Scaling discovery calls
DescribeAutoScalingGroupsandDescribeScalingActivitiesonResource: "*"plusPutNotificationConfiguration,ResumeProcesses, andSuspendProcesseson only Auto Scaling groups whose name starts withawseb-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 orPassRole, 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.commust 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
StringEqualsmatches the exact audience and subject above. - The inline policy contains no
Resource: "*"mutation action and no service outsideelasticbeanstalk/s3/cloudformation/ec2/autoscaling. CloudFormation discovery is limited to the single EB-managed stack prefix. EC2 and Auto Scaling discovery useResource: "*"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.