shoc-backend/infra/cdk/README.md

191 lines
9.6 KiB
Markdown
Raw Normal View History

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`, `s3:GetObjectAcl`, and
`s3:GetObjectVersion`, which the pinned official deployment action and
Elastic Beanstalk require to validate the `CreateApplicationVersion` source
bundle after upload.
2026-07-27 19:26:07 -03:00
`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
`resources/environments/e-hehnrqjjrt/_runtime/*` in the same bucket. A
successful break-glass deployment showed that EB copies each immutable
application version, its embedded extensions, and its manifest under that
exact environment runtime prefix. The failed OIDC path already allowed the
source object, so the remaining ACL validation is limited to that proven
runtime boundary. This grant exposes ACL metadata only; it does not grant
runtime content reads or writes.
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.
2026-07-27 19:26:07 -03:00
## 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.
2026-07-27 19:26:07 -03:00
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.