mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
fix: scope s3:ListBucket to the releases/ prefix (F-1/IAC-04)
The instance role and the GitHub deploy app role granted s3:ListBucket on the whole assets bucket. Every caller (deploy.sh, the publish/rollback scripts) only ever lists under releases/, so add a StringLike s3:prefix=releases/* condition. GetBucketLocation has no s3:prefix in its request context, so it moves to its own unconditioned statement. Also document the accepted F-2 cross-env existence-oracle residual on BatchGetSecretValue.
This commit is contained in:
parent
e4bee74c53
commit
789c59cfdf
3 changed files with 103 additions and 2 deletions
|
|
@ -150,10 +150,22 @@ export class GithubDeployRoles extends Construct {
|
|||
resources: [`arn:aws:s3:::open-swe-${envName}-assets/releases/*`],
|
||||
}),
|
||||
);
|
||||
// ListBucket is constrained to the releases/ prefix (F-1/IAC-04): the
|
||||
// publish/rollback scripts only ever list under releases/, so a leaked CI
|
||||
// token cannot enumerate anything else in the bucket. GetBucketLocation
|
||||
// carries no s3:prefix, so it stays a separate, unconditioned statement.
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ListArtifactBucket",
|
||||
actions: ["s3:ListBucket", "s3:GetBucketLocation"],
|
||||
actions: ["s3:ListBucket"],
|
||||
resources: [`arn:aws:s3:::open-swe-${envName}-assets`],
|
||||
conditions: { StringLike: { "s3:prefix": ["releases/*"] } },
|
||||
}),
|
||||
);
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "GetArtifactBucketLocation",
|
||||
actions: ["s3:GetBucketLocation"],
|
||||
resources: [`arn:aws:s3:::open-swe-${envName}-assets`],
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -43,10 +43,23 @@ export class InstanceRole extends Construct {
|
|||
resources: [`arn:aws:s3:::${p}-assets/releases/*`],
|
||||
}),
|
||||
);
|
||||
// ListBucket is constrained to the releases/ prefix (F-1/IAC-04) — the box
|
||||
// only ever lists release artifacts, so a compromised box cannot enumerate
|
||||
// any other object that might land in the bucket. GetBucketLocation has no
|
||||
// s3:prefix in its request context, so it stays a separate, unconditioned
|
||||
// statement (the condition would otherwise AccessDeny it).
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ListArtifactBucket",
|
||||
actions: ["s3:ListBucket", "s3:GetBucketLocation"],
|
||||
actions: ["s3:ListBucket"],
|
||||
resources: [`arn:aws:s3:::${p}-assets`],
|
||||
conditions: { StringLike: { "s3:prefix": ["releases/*"] } },
|
||||
}),
|
||||
);
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "GetArtifactBucketLocation",
|
||||
actions: ["s3:GetBucketLocation"],
|
||||
resources: [`arn:aws:s3:::${p}-assets`],
|
||||
}),
|
||||
);
|
||||
|
|
@ -85,6 +98,11 @@ export class InstanceRole extends Construct {
|
|||
// holds. The win that DID survive: fetch-config uses `--secret-id-list` (explicit
|
||||
// names, no name filter), so `secretsmanager:ListSecrets` is NOT needed and is
|
||||
// intentionally omitted — the box cannot enumerate secret names account-wide.
|
||||
// F-2 (accepted residual): because the grant is `*`, a caller naming a secret
|
||||
// in ANOTHER env's prefix learns whether that name EXISTS (an existence oracle
|
||||
// via the per-secret AccessDenied-vs-not signal) even though the VALUE stays
|
||||
// gated by the prefix-scoped GetSecretValue above. Accepted within Sea Haven's
|
||||
// single-tenant account 328440206208 — cross-env VALUE isolation is preserved.
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "BatchGetSecretValues",
|
||||
|
|
|
|||
71
infra/test/s3-list-prefix.test.ts
Normal file
71
infra/test/s3-list-prefix.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Match, Template } from "aws-cdk-lib/assertions";
|
||||
import { OpenSweIamStack } from "../lib/open-swe-iam-stack";
|
||||
import { OpenSweStack } from "../lib/open-swe-stack";
|
||||
|
||||
const ENV = { account: "328440206208", region: "us-east-1" };
|
||||
|
||||
// F-1 / IAC-04: s3:ListBucket must be constrained to the releases/ prefix so a
|
||||
// compromised box / leaked CI token cannot enumerate the rest of the bucket.
|
||||
const RELEASES_PREFIX_CONDITION = { StringLike: { "s3:prefix": ["releases/*"] } };
|
||||
|
||||
describe("S3 ListBucket prefix scoping (F-1/IAC-04)", () => {
|
||||
it("instance role ListBucket is constrained to releases/*", () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new OpenSweStack(app, "OpenSweDevStack", {
|
||||
stackName: "open-swe-dev",
|
||||
env: ENV,
|
||||
envName: "dev",
|
||||
});
|
||||
Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", {
|
||||
PolicyDocument: Match.objectLike({
|
||||
Statement: Match.arrayWith([
|
||||
Match.objectLike({
|
||||
Sid: "ListArtifactBucket",
|
||||
Action: "s3:ListBucket",
|
||||
Condition: RELEASES_PREFIX_CONDITION,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("github deploy app role ListBucket is constrained to releases/*", () => {
|
||||
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: "ListArtifactBucket",
|
||||
Action: "s3:ListBucket",
|
||||
Condition: RELEASES_PREFIX_CONDITION,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("GetBucketLocation stays a separate, unconditioned statement", () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new OpenSweStack(app, "OpenSweDevStack", {
|
||||
stackName: "open-swe-dev",
|
||||
env: ENV,
|
||||
envName: "dev",
|
||||
});
|
||||
Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", {
|
||||
PolicyDocument: Match.objectLike({
|
||||
Statement: Match.arrayWith([
|
||||
Match.objectLike({
|
||||
Sid: "GetArtifactBucketLocation",
|
||||
Action: "s3:GetBucketLocation",
|
||||
Condition: Match.absent(),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue