mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
fix(cdk): allow Beanstalk VPC discovery (#44)
* fix(cdk): allow Beanstalk VPC discovery * chore(ci): clarify backend check name * fix(eb): allow temporary object cleanup * fix(cdk): allow managed environment stack update * fix(cdk): allow Beanstalk template read * fix(cdk): apply supported Beanstalk S3 policy * fix(cdk): allow load balancer discovery and cancel * fix(cdk): allow Beanstalk resource discovery
This commit is contained in:
parent
83ed6a1790
commit
47c3fff4ba
3 changed files with 101 additions and 126 deletions
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
|
|
@ -1,11 +1,34 @@
|
|||
name: CI
|
||||
name: Backend CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@main
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
solution: "SeaHavenIndustries.sln"
|
||||
run-tests: true
|
||||
build-and-test:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
concurrency:
|
||||
group: backend-ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore SeaHavenIndustries.sln
|
||||
|
||||
- name: Build
|
||||
run: dotnet build SeaHavenIndustries.sln --no-restore --configuration Release
|
||||
|
||||
- name: Test
|
||||
run: dotnet test SeaHavenIndustries.sln --no-build --configuration Release
|
||||
|
|
|
|||
|
|
@ -191,10 +191,11 @@ All commands run from `infra/cdk/`.
|
|||
- 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.
|
||||
`elasticloadbalancing` / `autoscaling`. CloudFormation discovery and
|
||||
mutations are limited to the single EB-managed stack prefix. EC2, Elastic
|
||||
Load Balancing, 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`).
|
||||
|
|
@ -203,12 +204,46 @@ 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.
|
||||
using the initiating role and requires the service-wide
|
||||
`elasticbeanstalk-*` bucket/object namespaces.
|
||||
|
||||
The July 30 deployment of backend PR #41 then reached `UpdateEnvironment` and
|
||||
failed on `ec2:DescribeVpcs`. Elastic Beanstalk performs this read-only network
|
||||
discovery using the initiating role, so the CDK policy includes that action
|
||||
alongside the existing EC2 describe permissions. It remains resource `*`
|
||||
because `DescribeVpcs` does not support resource-level permissions.
|
||||
|
||||
Successive exact reruns then reached S3 cleanup, the delegated CloudFormation
|
||||
update, and the CloudFormation template fetch. The observed failures were
|
||||
`s3:DeleteObject`, `cloudformation:UpdateStack`, and finally an opaque
|
||||
CloudFormation `S3 error: Access Denied` after narrower object reads had been
|
||||
added. Because AWS does not expose the AWS-owned bucket/key or exact internal
|
||||
S3 read in that final error, the CDK now uses AWS Support's authoritative
|
||||
UpdateEnvironment S3 set:
|
||||
|
||||
- `s3:Delete*`, `s3:Get*`, and `s3:Put*` on
|
||||
`arn:aws:s3:::elasticbeanstalk-*/*`.
|
||||
- `s3:GetBucket*`, `s3:ListBucket`, `s3:PutBucketPolicy`,
|
||||
`s3:PutBucketPublicAccessBlock`, and `s3:PutBucketOwnershipControls` on
|
||||
`arn:aws:s3:::elasticbeanstalk-*`.
|
||||
|
||||
`s3:CreateBucket` remains excluded because this workflow targets an existing
|
||||
application/environment and explicitly disables bucket creation. No S3 access
|
||||
is granted to non-Elastic-Beanstalk bucket names. The CloudFormation mutation
|
||||
remains limited to the single existing `shoc-backend-dev` managed stack ARN; it
|
||||
cannot create stacks or update another stack.
|
||||
|
||||
The next rerun cleared S3 and then required the read-only
|
||||
`elasticloadbalancing:DescribeLoadBalancers` discovery action. Its failed
|
||||
managed-stack update also required `cloudformation:CancelUpdateStack`; the
|
||||
cancel action is scoped to the same single stack ARN as `UpdateStack`.
|
||||
|
||||
The subsequent rerun progressed into Auto Scaling and required
|
||||
`autoscaling:DescribeLaunchConfigurations`. Because Elastic Beanstalk's
|
||||
managed update workflow performs variable resource discovery, the role follows
|
||||
the documented read-only discovery families for EC2, Elastic Load Balancing,
|
||||
and Auto Scaling (`Describe*`). These grants expose metadata across the account
|
||||
but do not authorize any mutation; write actions remain separately scoped.
|
||||
|
||||
The pinned deployment action can return success after Elastic Beanstalk emits a
|
||||
fatal deployment event. The following workflow step therefore verifies that
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ const ENVIRONMENT_NAME = 'shoc-backend-dev';
|
|||
const ENVIRONMENT_ID = 'e-hehnrqjjrt';
|
||||
const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`;
|
||||
const REPO = 'Sea-Haven-Industries/shoc-backend';
|
||||
const BUCKET_NAME = `elasticbeanstalk-${REGION}-${ACCOUNT_ID}`;
|
||||
|
||||
export class DeployDevStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) {
|
||||
|
|
@ -17,19 +16,6 @@ export class DeployDevStack extends cdk.Stack {
|
|||
|
||||
const applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`;
|
||||
const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_NAME}`;
|
||||
const bucketArn = `arn:aws:s3:::${BUCKET_NAME}`;
|
||||
const runtimeVersionArn =
|
||||
`${bucketArn}/resources/environments/${ENVIRONMENT_ID}` +
|
||||
`/_runtime/_versions/${APPLICATION_NAME}/*`;
|
||||
const embeddedExtensionArn =
|
||||
`${bucketArn}/resources/_runtime/_embedded_extensions/` +
|
||||
`${APPLICATION_NAME}/*`;
|
||||
const environmentEmbeddedExtensionArn =
|
||||
`${bucketArn}/resources/environments/${ENVIRONMENT_ID}` +
|
||||
`/_runtime/_embedded_extensions/${APPLICATION_NAME}/*`;
|
||||
const runtimeManifestArn =
|
||||
`${bucketArn}/resources/environments/${ENVIRONMENT_ID}` +
|
||||
'/_runtime/versions/*';
|
||||
const oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`;
|
||||
|
||||
const deployRole = new iam.Role(this, 'GithubDeployRole', {
|
||||
|
|
@ -57,9 +43,12 @@ export class DeployDevStack extends cdk.Stack {
|
|||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'autoscaling:Describe*',
|
||||
'ec2:Describe*',
|
||||
'elasticbeanstalk:DescribeEnvironments',
|
||||
'elasticbeanstalk:DescribeApplicationVersions',
|
||||
'elasticbeanstalk:DescribeEvents',
|
||||
'elasticloadbalancing:Describe*',
|
||||
],
|
||||
resources: ['*'],
|
||||
}),
|
||||
|
|
@ -94,6 +83,8 @@ export class DeployDevStack extends cdk.Stack {
|
|||
'cloudformation:DescribeStackResources',
|
||||
'cloudformation:DescribeStacks',
|
||||
'cloudformation:ListStackResources',
|
||||
'cloudformation:CancelUpdateStack',
|
||||
'cloudformation:UpdateStack',
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/${ENVIRONMENT_STACK_NAME}/*`,
|
||||
|
|
@ -101,29 +92,6 @@ export class DeployDevStack extends cdk.Stack {
|
|||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'ec2:DescribeAvailabilityZones',
|
||||
'ec2:DescribeImages',
|
||||
'ec2:DescribeSubnets',
|
||||
],
|
||||
resources: ['*'],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'autoscaling:DescribeAutoScalingGroups',
|
||||
'autoscaling:DescribeScalingActivities',
|
||||
],
|
||||
resources: ['*'],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
|
|
@ -141,82 +109,31 @@ export class DeployDevStack extends cdk.Stack {
|
|||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
's3:ListBucket',
|
||||
's3:CreateBucket',
|
||||
's3:PutBucketOwnershipControls',
|
||||
's3:GetBucketLocation',
|
||||
's3:GetBucketPolicy',
|
||||
],
|
||||
resources: [bucketArn],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['s3:GetObject', 's3:GetObjectVersion', 's3:PutObject'],
|
||||
resources: [`${bucketArn}/${APPLICATION_NAME}/*`],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
's3:DeleteObject',
|
||||
's3:GetObject',
|
||||
's3:GetObjectVersionAcl',
|
||||
's3:PutObject',
|
||||
's3:PutObjectVersionAcl',
|
||||
],
|
||||
// UpdateEnvironment copies the uploaded source bundle into this
|
||||
// environment-specific runtime prefix, verifies the temporary copy,
|
||||
// preserves its version ACL, and removes it after registration.
|
||||
resources: [runtimeVersionArn],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['s3:PutObject'],
|
||||
// UpdateEnvironment materializes the application's embedded-extension
|
||||
// manifest under the shared and environment-specific runtime prefixes.
|
||||
resources: [embeddedExtensionArn, environmentEmbeddedExtensionArn],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['s3:GetObject'],
|
||||
// Elastic Beanstalk verifies the environment copy with HeadObject.
|
||||
resources: [environmentEmbeddedExtensionArn],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['s3:GetObject', 's3:PutObject'],
|
||||
// UpdateEnvironment reads the prior environment version manifest and
|
||||
// writes its replacement under this environment-only runtime prefix.
|
||||
resources: [runtimeManifestArn],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['s3:GetObjectAcl'],
|
||||
// UpdateEnvironment also checks objects in AWS-owned Elastic Beanstalk
|
||||
// buckets. AWS Support case 178526484500047 confirmed that the caller's
|
||||
// identity policy must cover the service-wide bucket namespace.
|
||||
actions: ['s3:Delete*', 's3:Get*', 's3:Put*'],
|
||||
// AWS Support case 178526484500047 confirmed that UpdateEnvironment
|
||||
// reads, writes, versions, ACL-checks, and removes objects in both the
|
||||
// account bucket and AWS-owned Elastic Beanstalk service buckets.
|
||||
resources: ['arn:aws:s3:::elasticbeanstalk-*/*'],
|
||||
}),
|
||||
);
|
||||
|
||||
deployRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
's3:GetBucket*',
|
||||
's3:ListBucket',
|
||||
's3:PutBucketOwnershipControls',
|
||||
's3:PutBucketPolicy',
|
||||
's3:PutBucketPublicAccessBlock',
|
||||
],
|
||||
// This is AWS Support's bucket-level UpdateEnvironment set, excluding
|
||||
// CreateBucket because the workflow deploys only to an existing
|
||||
// application/environment and disables bucket creation.
|
||||
resources: ['arn:aws:s3:::elasticbeanstalk-*'],
|
||||
}),
|
||||
);
|
||||
|
||||
new cdk.CfnOutput(this, 'GithubDeployRoleArn', {
|
||||
value: deployRole.roleArn,
|
||||
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue