mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, dev]
|
branches: [main, dev]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
build-and-test:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@main
|
name: Build and test
|
||||||
with:
|
runs-on: ubuntu-latest
|
||||||
dotnet-version: "8.0.x"
|
timeout-minutes: 20
|
||||||
solution: "SeaHavenIndustries.sln"
|
concurrency:
|
||||||
run-tests: true
|
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.
|
- Trust policy `StringEquals` matches the exact audience and subject above.
|
||||||
- The inline policy contains no `Resource: "*"` mutation action and no service
|
- The inline policy contains no `Resource: "*"` mutation action and no service
|
||||||
outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2` /
|
outside `elasticbeanstalk` / `s3` / `cloudformation` / `ec2` /
|
||||||
`autoscaling`. CloudFormation discovery is limited to the single EB-managed
|
`elasticloadbalancing` / `autoscaling`. CloudFormation discovery and
|
||||||
stack prefix. EC2 and Auto Scaling discovery use `Resource: "*"` only where
|
mutations are limited to the single EB-managed stack prefix. EC2, Elastic
|
||||||
the IAM resource model requires it; Auto Scaling mutations are limited to
|
Load Balancing, and Auto Scaling discovery use `Resource: "*"` only where the
|
||||||
this environment's ASG name pattern.
|
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
|
The workflow's `AWS_DEPLOY_ROLE_ARN` repository secret (environment `dev`) must
|
||||||
hold the ARN output by this stack (`GithubDeployRoleArn`).
|
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
|
reported a generic `s3:GetObjectAcl` denial outside the account-owned source
|
||||||
prefix. AWS Support case `178526484500047` subsequently confirmed that
|
prefix. AWS Support case `178526484500047` subsequently confirmed that
|
||||||
`UpdateEnvironment` checks objects in AWS-owned Elastic Beanstalk buckets
|
`UpdateEnvironment` checks objects in AWS-owned Elastic Beanstalk buckets
|
||||||
using the initiating role and requires the `elasticbeanstalk-*/*` resource
|
using the initiating role and requires the service-wide
|
||||||
namespace. This policy adds only the denied ACL-read action on that namespace;
|
`elasticbeanstalk-*` bucket/object namespaces.
|
||||||
it intentionally does not copy the managed
|
|
||||||
`AdministratorAccess-AWSElasticBeanstalk` policy's broad `s3:Get*`,
|
The July 30 deployment of backend PR #41 then reached `UpdateEnvironment` and
|
||||||
`s3:Put*`, or `s3:Delete*` grants. Any later denial must be evaluated and
|
failed on `ec2:DescribeVpcs`. Elastic Beanstalk performs this read-only network
|
||||||
granted independently.
|
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
|
The pinned deployment action can return success after Elastic Beanstalk emits a
|
||||||
fatal deployment event. The following workflow step therefore verifies that
|
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_ID = 'e-hehnrqjjrt';
|
||||||
const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`;
|
const ENVIRONMENT_STACK_NAME = `awseb-${ENVIRONMENT_ID}-stack`;
|
||||||
const REPO = 'Sea-Haven-Industries/shoc-backend';
|
const REPO = 'Sea-Haven-Industries/shoc-backend';
|
||||||
const BUCKET_NAME = `elasticbeanstalk-${REGION}-${ACCOUNT_ID}`;
|
|
||||||
|
|
||||||
export class DeployDevStack extends cdk.Stack {
|
export class DeployDevStack extends cdk.Stack {
|
||||||
constructor(scope: Construct, id: string, props: cdk.StackProps = {}) {
|
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 applicationArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:application/${APPLICATION_NAME}`;
|
||||||
const environmentArn = `arn:aws:elasticbeanstalk:${REGION}:${ACCOUNT_ID}:environment/${APPLICATION_NAME}/${ENVIRONMENT_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 oidcProviderArn = `arn:aws:iam::${ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com`;
|
||||||
|
|
||||||
const deployRole = new iam.Role(this, 'GithubDeployRole', {
|
const deployRole = new iam.Role(this, 'GithubDeployRole', {
|
||||||
|
|
@ -57,9 +43,12 @@ export class DeployDevStack extends cdk.Stack {
|
||||||
new iam.PolicyStatement({
|
new iam.PolicyStatement({
|
||||||
effect: iam.Effect.ALLOW,
|
effect: iam.Effect.ALLOW,
|
||||||
actions: [
|
actions: [
|
||||||
|
'autoscaling:Describe*',
|
||||||
|
'ec2:Describe*',
|
||||||
'elasticbeanstalk:DescribeEnvironments',
|
'elasticbeanstalk:DescribeEnvironments',
|
||||||
'elasticbeanstalk:DescribeApplicationVersions',
|
'elasticbeanstalk:DescribeApplicationVersions',
|
||||||
'elasticbeanstalk:DescribeEvents',
|
'elasticbeanstalk:DescribeEvents',
|
||||||
|
'elasticloadbalancing:Describe*',
|
||||||
],
|
],
|
||||||
resources: ['*'],
|
resources: ['*'],
|
||||||
}),
|
}),
|
||||||
|
|
@ -94,6 +83,8 @@ export class DeployDevStack extends cdk.Stack {
|
||||||
'cloudformation:DescribeStackResources',
|
'cloudformation:DescribeStackResources',
|
||||||
'cloudformation:DescribeStacks',
|
'cloudformation:DescribeStacks',
|
||||||
'cloudformation:ListStackResources',
|
'cloudformation:ListStackResources',
|
||||||
|
'cloudformation:CancelUpdateStack',
|
||||||
|
'cloudformation:UpdateStack',
|
||||||
],
|
],
|
||||||
resources: [
|
resources: [
|
||||||
`arn:aws:cloudformation:${REGION}:${ACCOUNT_ID}:stack/${ENVIRONMENT_STACK_NAME}/*`,
|
`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(
|
deployRole.addToPolicy(
|
||||||
new iam.PolicyStatement({
|
new iam.PolicyStatement({
|
||||||
effect: iam.Effect.ALLOW,
|
effect: iam.Effect.ALLOW,
|
||||||
|
|
@ -141,82 +109,31 @@ export class DeployDevStack extends cdk.Stack {
|
||||||
deployRole.addToPolicy(
|
deployRole.addToPolicy(
|
||||||
new iam.PolicyStatement({
|
new iam.PolicyStatement({
|
||||||
effect: iam.Effect.ALLOW,
|
effect: iam.Effect.ALLOW,
|
||||||
actions: [
|
actions: ['s3:Delete*', 's3:Get*', 's3:Put*'],
|
||||||
's3:ListBucket',
|
// AWS Support case 178526484500047 confirmed that UpdateEnvironment
|
||||||
's3:CreateBucket',
|
// reads, writes, versions, ACL-checks, and removes objects in both the
|
||||||
's3:PutBucketOwnershipControls',
|
// account bucket and AWS-owned Elastic Beanstalk service buckets.
|
||||||
'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.
|
|
||||||
resources: ['arn:aws:s3:::elasticbeanstalk-*/*'],
|
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', {
|
new cdk.CfnOutput(this, 'GithubDeployRoleArn', {
|
||||||
value: deployRole.roleArn,
|
value: deployRole.roleArn,
|
||||||
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',
|
description: 'ARN of the GitHub OIDC deploy role for shoc-backend dev.',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue