# 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`, `s3:GetObjectAcl`, and `s3:GetObjectVersion`, which the pinned official deployment action and Elastic Beanstalk require to validate the `CreateApplicationVersion` source bundle after upload. `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. - `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. - `s3:GetObjectAcl` on only the active Elastic Beanstalk platform version prefix `elasticbeanstalk-platform-assets-us-east-1/stalks/eb_dotnet8_amazon_linux_2023_1.0.2939.0_20260625064821/*`. The live environment stack references its platform configuration, hooks, user-data script, and sample bundle under that exact AWS-owned prefix. An audited account-bucket data-event trail captured source-bundle ACL reads but did not capture the remaining denied ACL read, which rules out the previous account-owned runtime-prefix inference. The grant therefore covers only ACL metadata for the environment's currently deployed platform version; it grants no platform object content read or write, no adjacent platform version, and no account-owned runtime 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. ## 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 ```bash 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 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.