mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
210 lines
11 KiB
Markdown
210 lines
11 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`, and `s3:GetObjectVersion`, which the pinned
|
|
official deployment action requires to validate the
|
|
`CreateApplicationVersion` source bundle after upload.
|
|
- `s3:PutObject` 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` and removes
|
|
that temporary copy after the version is registered. Attempts 1 and 2 of run
|
|
`30448885838` exposed the exact source, destination, and cleanup denial after
|
|
the earlier ACL denial was resolved. The grant does not cover another
|
|
environment, another application, source bundles, object versions, bucket
|
|
ACLs, object ACLs, tags, retention, or reads.
|
|
- `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
|
|
|
|
```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 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.
|