shoc-backend/infra/cdk/README.md
2026-07-28 14:30:48 -03:00

193 lines
9.8 KiB
Markdown

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