mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
fix: isolate dev CDK deploys on their own bootstrap qualifier (B-1/OSWE-IAC-01) (#55)
* fix: isolate dev CDK deploys on their own bootstrap qualifier (B-1) Dev synthesizes against the oswedev qualifier and the dev infra deploy role is scoped to cdk-oswedev-* — it can no longer assume the default hnb659fds bootstrap roles whose admin cfn-exec-role deploys prod, closing the cross-env escalation (OSWE-IAC-01). Prod stays on the default qualifier. * test: assert per-env bootstrap qualifier isolation + document (B-1)
This commit is contained in:
parent
a33aaec495
commit
e9499e49b8
5 changed files with 116 additions and 15 deletions
|
|
@ -66,6 +66,32 @@ never defaults to PascalCase; resource names follow `open-swe-<env>-*`.
|
|||
The GitHub OIDC provider already exists account-wide (created for seahaven-site);
|
||||
it is referenced by ARN, never re-created.
|
||||
|
||||
## CDK bootstrap qualifiers — per-env deploy isolation (B-1 / OSWE-IAC-01)
|
||||
|
||||
Each env's infra deploy role may assume **only its own bootstrap qualifier's**
|
||||
roles, so a dev-branch token can never assume the bootstrap roles whose admin
|
||||
`cfn-exec-role` deploys prod (closing the cross-env escalation that bypassed
|
||||
prod's Environment approval gate). Mapping lives in `config.ts:bootstrapQualifier`:
|
||||
|
||||
| Env | Qualifier | Toolkit stack | Infra role assumes |
|
||||
|---|---|---|---|
|
||||
| dev | `oswedev` | `CDKToolkit-oswedev` | `cdk-oswedev-*` |
|
||||
| prod | `hnb659fds` (default) | `CDKToolkit` | `cdk-hnb659fds-*` |
|
||||
|
||||
The dev stack synthesizes with `DefaultStackSynthesizer({ qualifier: "oswedev" })`
|
||||
(`bin/app.ts`); prod uses the default. Bootstrap a new env qualifier with:
|
||||
|
||||
```bash
|
||||
npx cdk bootstrap --qualifier <qual> --toolkit-stack-name CDKToolkit-<qual> \
|
||||
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
|
||||
aws://328440206208/us-east-1
|
||||
```
|
||||
|
||||
**Deploy order matters** when changing an env's qualifier: bootstrap the new
|
||||
qualifier and deploy the env stack onto it **before** re-scoping that env's infra
|
||||
role in `open-swe-iam` — otherwise a pipeline deploy with the re-scoped role would
|
||||
fail to assume the not-yet-targeted bootstrap roles.
|
||||
|
||||
## Kebab-case naming Aspect
|
||||
|
||||
`KebabNamingAspect` (applied app-wide in `bin/app.ts`) fails synth via
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
import "source-map-support/register";
|
||||
import * as cdk from "aws-cdk-lib";
|
||||
import { ACCOUNT, REGION } from "../lib/config";
|
||||
import { ACCOUNT, REGION, bootstrapQualifier } from "../lib/config";
|
||||
import { OpenSweIamStack } from "../lib/open-swe-iam-stack";
|
||||
import { OpenSweStack } from "../lib/open-swe-stack";
|
||||
import { KebabNamingAspect } from "../lib/aspects/kebab-naming-aspect";
|
||||
|
|
@ -17,10 +17,18 @@ new OpenSweIamStack(app, "OpenSweIamStack", {
|
|||
|
||||
// The two env stacks — explicit kebab-case stackName (never let CDK default to
|
||||
// PascalCase), env-parameterised so resources are `open-swe-<env>-*`.
|
||||
//
|
||||
// B-1 / OSWE-IAC-01: dev synthesizes against its OWN bootstrap qualifier
|
||||
// (`oswedev`), so it deploys via the cdk-oswedev-* roles the dev infra role is
|
||||
// scoped to — and NOT the default hnb659fds bootstrap roles that deploy prod.
|
||||
// Prod stays on the default qualifier (no synthesizer override).
|
||||
new OpenSweStack(app, "OpenSweDevStack", {
|
||||
stackName: "open-swe-dev",
|
||||
env,
|
||||
envName: "dev",
|
||||
synthesizer: new cdk.DefaultStackSynthesizer({
|
||||
qualifier: bootstrapQualifier("dev"),
|
||||
}),
|
||||
});
|
||||
|
||||
new OpenSweStack(app, "OpenSweProdStack", {
|
||||
|
|
|
|||
|
|
@ -28,16 +28,28 @@ export const prefix = (env: EnvName): string => `open-swe-${env}`;
|
|||
* Each env gets its OWN infra + app role (githubdeploy-open-swe-{infra,app}-<env>)
|
||||
* so a dev token cannot reach prod. Exact subject → StringEquals (no `*`).
|
||||
*
|
||||
* Residual (documented): CDK's single account-wide `cfn-exec-role` means the dev
|
||||
* INFRA role can still technically `cdk deploy open-swe-prod`; the workflow only
|
||||
* ever targets its own env stack, and prod's environment-gated role is the
|
||||
* approved path. Per-env bootstrap qualifiers would close this fully (future).
|
||||
* Cross-env deploy isolation is enforced at the bootstrap layer too — see
|
||||
* `bootstrapQualifier`: dev runs on its own qualifier so the dev infra role
|
||||
* cannot assume the bootstrap roles that deploy prod.
|
||||
*/
|
||||
export const oidcSubject = (env: EnvName): string =>
|
||||
env === "prod"
|
||||
? `repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:prod`
|
||||
: `repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/dev`;
|
||||
|
||||
/**
|
||||
* Per-env CDK bootstrap qualifier (B-1 / OSWE-IAC-01 fix). Dev runs on its OWN
|
||||
* qualifier `oswedev` (bootstrapped into the `CDKToolkit-oswedev` stack), so the
|
||||
* dev infra deploy role only assumes `cdk-oswedev-*` and can NO LONGER assume the
|
||||
* default `cdk-hnb659fds-*` set whose admin `cfn-exec-role` deploys prod. Prod
|
||||
* stays on the default qualifier. This closes the cross-env escalation where a
|
||||
* dev-branch token could `cdk deploy open-swe-prod` via the shared bootstrap
|
||||
* roles, bypassing prod's Environment approval gate.
|
||||
*/
|
||||
export const DEFAULT_BOOTSTRAP_QUALIFIER = "hnb659fds";
|
||||
export const bootstrapQualifier = (env: EnvName): string =>
|
||||
env === "dev" ? "oswedev" : DEFAULT_BOOTSTRAP_QUALIFIER;
|
||||
|
||||
/**
|
||||
* The GitHub Actions OIDC provider already exists account-wide (created for
|
||||
* seahaven-site; see .github/oidc-deploy-roles.yaml `CreateOIDCProvider=false`).
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
EnvName,
|
||||
GITHUB_OIDC_PROVIDER_ARN,
|
||||
REGION,
|
||||
bootstrapQualifier,
|
||||
oidcSubject,
|
||||
} from "../config";
|
||||
|
||||
|
|
@ -62,20 +63,19 @@ export class GithubDeployRoles extends Construct {
|
|||
// permissions are exercised by the bootstrap `cfn-exec-role`, whose scope is
|
||||
// owned by the CDKToolkit stack — NOT granted directly here.
|
||||
//
|
||||
// T4 BLOCK#1: GPT-4.1 flagged the `cdk-hnb659fds-*` wildcard and recommended
|
||||
// enumerating the four exact ARNs. ACCEPTED EXCEPTION (Adam, 2026-06-26): kept
|
||||
// as the verified org-wide convention (githubdeploy-seahaven-account-baseline
|
||||
// uses the identical wildcard). Only `cdk bootstrap` creates roles with this
|
||||
// prefix, so practical escalation risk is low.
|
||||
// T5 residual (OSWE-IAC-02): the single account-wide cfn-exec-role means the
|
||||
// dev infra role can technically deploy any stack; per-env trust gates WHO can
|
||||
// assume, and the prod role requires the environment:prod approval. Per-env
|
||||
// bootstrap qualifiers would close the residual fully (future hardening).
|
||||
// B-1 / OSWE-IAC-01 fix: scope the assume to THIS env's bootstrap qualifier.
|
||||
// Dev uses `oswedev` (its own CDKToolkit-oswedev bootstrap), prod uses the
|
||||
// default `hnb659fds`. The dev infra role can therefore no longer assume the
|
||||
// bootstrap roles whose admin cfn-exec-role deploys prod — closing the prior
|
||||
// cross-env escalation (a dev-branch token could `cdk deploy open-swe-prod`
|
||||
// via the shared account-wide bootstrap roles, bypassing prod's Environment
|
||||
// approval gate). The qualifier wildcard still matches only the handful of
|
||||
// roles `cdk bootstrap` creates for that qualifier.
|
||||
this.infraRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "AssumeCdkBootstrapRoles",
|
||||
actions: ["sts:AssumeRole"],
|
||||
resources: [`arn:aws:iam::${ACCOUNT}:role/cdk-hnb659fds-*`],
|
||||
resources: [`arn:aws:iam::${ACCOUNT}:role/cdk-${bootstrapQualifier(envName)}-*`],
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
|||
55
infra/test/bootstrap-qualifier.test.ts
Normal file
55
infra/test/bootstrap-qualifier.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Match, Template } from "aws-cdk-lib/assertions";
|
||||
import { OpenSweIamStack } from "../lib/open-swe-iam-stack";
|
||||
import { bootstrapQualifier } from "../lib/config";
|
||||
|
||||
const ENV = { account: "328440206208", region: "us-east-1" };
|
||||
|
||||
// B-1 / OSWE-IAC-01: each env's infra deploy role may assume ONLY its own
|
||||
// bootstrap qualifier's roles. Dev runs on `oswedev`, so a dev-branch token can
|
||||
// no longer assume the default `hnb659fds` bootstrap roles whose admin
|
||||
// cfn-exec-role deploys prod. Prod stays on the default qualifier.
|
||||
describe("Per-env CDK bootstrap qualifier isolation (B-1/OSWE-IAC-01)", () => {
|
||||
it("maps dev -> oswedev and prod -> hnb659fds", () => {
|
||||
expect(bootstrapQualifier("dev")).toBe("oswedev");
|
||||
expect(bootstrapQualifier("prod")).toBe("hnb659fds");
|
||||
});
|
||||
|
||||
it("dev infra deploy role assumes only cdk-oswedev-* bootstrap roles", () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new OpenSweIamStack(app, "OpenSweIamStack", {
|
||||
stackName: "open-swe-iam",
|
||||
env: ENV,
|
||||
});
|
||||
Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", {
|
||||
PolicyDocument: Match.objectLike({
|
||||
Statement: Match.arrayWith([
|
||||
Match.objectLike({
|
||||
Sid: "AssumeCdkBootstrapRoles",
|
||||
Action: "sts:AssumeRole",
|
||||
Resource: "arn:aws:iam::328440206208:role/cdk-oswedev-*",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("prod infra deploy role stays on the default cdk-hnb659fds-* bootstrap roles", () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new OpenSweIamStack(app, "OpenSweIamStack", {
|
||||
stackName: "open-swe-iam",
|
||||
env: ENV,
|
||||
});
|
||||
Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", {
|
||||
PolicyDocument: Match.objectLike({
|
||||
Statement: Match.arrayWith([
|
||||
Match.objectLike({
|
||||
Sid: "AssumeCdkBootstrapRoles",
|
||||
Action: "sts:AssumeRole",
|
||||
Resource: "arn:aws:iam::328440206208:role/cdk-hnb659fds-*",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue