Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ const vpc = new SharedVpc("dev-use1", {
natGatewayCount: 3,
enableIpv6: true,
subnetTiers: [
{ name: "public", routeToInternet: true, shareViaRam: false, ipv6Slot: 0 },
{ name: "private", routeToInternet: false, shareViaRam: true, ipv6Slot: 1 },
{ name: "data", routeToInternet: false, shareViaRam: true, ipv6Slot: 2 },
{ name: "public", role: "load-balancers", routeToInternet: true, shareWith: [], ipv6Slot: 0 },
{ name: "private", role: "app-services", routeToInternet: false, shareWith: ["worx-app-dev"], ipv6Slot: 1 },
{ name: "data", role: "data-stores", routeToInternet: false, shareWith: ["worx-app-dev"], ipv6Slot: 2 },
],
flowLogs: { enabled: true, trafficType: "ALL" },
sharedAccounts: {},
sharedAccounts: { "111111111111": "worx-app-dev" },
tags: {},
});

Expand Down
10 changes: 5 additions & 5 deletions docs/security-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,10 +567,10 @@ const vpc = new SharedVpc("care-prd-vpc", {
"kms", // Encryption key management
],
subnetTiers: [
{ name: "public", routeToInternet: true, shareViaRam: false, ipv6Slot: 0 },
{ name: "app", routeToInternet: false, shareViaRam: true, ipv6Slot: 1 },
{ name: "hipaa-data", routeToInternet: false, shareViaRam: true, ipv6Slot: 2 },
{ name: "phi-isolated", routeToInternet: false, shareViaRam: true, cidrBits: 8, ipv6Slot: 3 },
{ name: "public", role: "load-balancers", routeToInternet: true, shareWith: [], ipv6Slot: 0 },
{ name: "app", role: "app-services", routeToInternet: false, shareWith: ["care-app-prd"], ipv6Slot: 1 },
{ name: "hipaa-data", role: "phi-stores", routeToInternet: false, shareWith: ["care-phi-prd"], ipv6Slot: 2 },
{ name: "phi-isolated", role: "phi-isolated", routeToInternet: false, shareWith: [], cidrBits: 8, ipv6Slot: 3 },
],
});
```
Expand Down Expand Up @@ -697,7 +697,7 @@ const privateRt = new aws.ec2.RouteTable({
...args.tags,
Tier: tier.name,
Type: "private",
ShareViaRam: tier.shareViaRam.toString(), // ← Audit visibility
ShareViaRam: String(tierShareTargets(tier).length > 0), // ← Audit visibility (tier.shareWith)
},
});
```
Expand Down
8 changes: 5 additions & 3 deletions packages/iac-aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ Details: [docs/security-implementation.md](https://github.com/AdaptiveWorX/iac-c
### Tier roles, sharing and renames

- `role` names the workloads a tier holds. It is tagged `Role` on the tier's subnets and NACL.
Optional in this release (a tier without it logs a warning), required after.
Required (0.6.0).
- `shareWith` lists the accounts (by their names in `sharedAccounts`) a tier's subnets are shared
with through RAM. A tier sharing with every account goes in the main share; a narrower set gets
a share of its own; `[]` shares with no account. `shareViaRam` is deprecated. Every tier gives
exactly one of the two, and every `shareWith` name must be in `sharedAccounts` (fail closed).
a share of its own; `[]` shares with no account. Required (0.6.0); every name must be in
`sharedAccounts` (fail closed). `shareViaRam` was removed in 0.6.0 and is refused: replace
`shareViaRam: true` with every account in `sharedAccounts`, `false` with `[]`. The default tiers
(`defaultSubnetTiers(sharedAccounts)`) share private and data with every account.
- `formerNames` renames a tier without replacing anything: every resource named after the tier
carries its old logical name as a Pulumi alias, so the preview shows tag updates only. The
standard outputs (`publicSubnetIds`, `privateSubnetIds`, `dataSubnetIds`) and NAT placement
Expand Down
37 changes: 17 additions & 20 deletions packages/iac-aws/src/shared-vpc.ipv6-layout.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import { describe, expect, it } from "vitest";
import {
azIndexOf,
DEFAULT_SUBNET_TIERS,
defaultSubnetTiers,
IPV6_SLOT_COUNT,
ipv6SubnetCidr,
type SubnetTier,
validateIpv6Layout,
} from "./shared-vpc.js";

const DEFAULT_TIERS = defaultSubnetTiers({});

// IPv6 documentation prefix (RFC 3849), never a live block.
const VPC_V6 = "2001:db8:1234:5600::/56";

Expand Down Expand Up @@ -84,25 +86,20 @@ describe("stability: adding an AZ or a tier never renumbers an existing subnet",
}
return out;
};
const before = layout(DEFAULT_SUBNET_TIERS, ["us-east-1a", "us-east-1b", "us-east-1c"]);
const before = layout(DEFAULT_TIERS, ["us-east-1a", "us-east-1b", "us-east-1c"]);

it("adding an AZ (in any list position) keeps every existing /64", () => {
const after = layout(DEFAULT_SUBNET_TIERS, [
"us-east-1d",
"us-east-1a",
"us-east-1b",
"us-east-1c",
]);
const after = layout(DEFAULT_TIERS, ["us-east-1d", "us-east-1a", "us-east-1b", "us-east-1c"]);
for (const [key, cidr] of before) {
expect(after.get(key)).toBe(cidr);
}
});

it("inserting a tier (anywhere in the array) keeps every existing /64", () => {
const [publicTier, privateTier, dataTier] = DEFAULT_SUBNET_TIERS;
const [publicTier, privateTier, dataTier] = DEFAULT_TIERS;
const tiers = [
publicTier,
{ name: "dmz", routeToInternet: true, shareViaRam: false, ipv6Slot: 3 },
{ name: "dmz", routeToInternet: true, role: "test", shareWith: [], ipv6Slot: 3 },
privateTier,
dataTier,
] as SubnetTier[];
Expand All @@ -117,37 +114,37 @@ describe("validateIpv6Layout", () => {
const azs = ["us-east-1a", "us-east-1b"];

it("accepts the default tiers (slots 0, 1, 2)", () => {
expect(DEFAULT_SUBNET_TIERS.map(t => t.ipv6Slot)).toEqual([0, 1, 2]);
expect(() => validateIpv6Layout(DEFAULT_SUBNET_TIERS, azs)).not.toThrow();
expect(DEFAULT_TIERS.map(t => t.ipv6Slot)).toEqual([0, 1, 2]);
expect(() => validateIpv6Layout(DEFAULT_TIERS, azs)).not.toThrow();
});

it("rejects a tier without an explicit ipv6Slot (no array-position default)", () => {
const tiers: SubnetTier[] = [
{ name: "public", routeToInternet: true, shareViaRam: false, ipv6Slot: 0 },
{ name: "private", routeToInternet: false, shareViaRam: true },
{ name: "public", routeToInternet: true, role: "test", shareWith: [], ipv6Slot: 0 },
{ name: "private", routeToInternet: false, role: "test", shareWith: ["shared"] },
];
expect(() => validateIpv6Layout(tiers, azs)).toThrow(/'private' has no ipv6Slot/);
});

it("rejects duplicate, out-of-range and non-integer slots", () => {
const dup: SubnetTier[] = [
{ name: "a", routeToInternet: true, shareViaRam: false, ipv6Slot: 1 },
{ name: "b", routeToInternet: false, shareViaRam: true, ipv6Slot: 1 },
{ name: "a", routeToInternet: true, role: "test", shareWith: [], ipv6Slot: 1 },
{ name: "b", routeToInternet: false, role: "test", shareWith: ["shared"], ipv6Slot: 1 },
];
expect(() => validateIpv6Layout(dup, azs)).toThrow(/both declare ipv6Slot 1/);
for (const bad of [16, -1, 1.5]) {
expect(() =>
validateIpv6Layout(
[{ name: "a", routeToInternet: true, shareViaRam: false, ipv6Slot: bad }],
[{ name: "a", routeToInternet: true, role: "test", shareWith: [], ipv6Slot: bad }],
azs
)
).toThrow(/integer 0–15/);
}
});

it("rejects AZs that map to the same index", () => {
expect(() =>
validateIpv6Layout(DEFAULT_SUBNET_TIERS, ["us-east-1a", "us-east-1-bos-1a"])
).toThrow(/same IPv6 AZ index 0/);
expect(() => validateIpv6Layout(DEFAULT_TIERS, ["us-east-1a", "us-east-1-bos-1a"])).toThrow(
/same IPv6 AZ index 0/
);
});
});
118 changes: 86 additions & 32 deletions packages/iac-aws/src/shared-vpc.rename.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,27 @@ async function settle(): Promise<void> {
}

const OLD = (shared: [boolean, boolean, boolean]) => [
{ name: "public", routeToInternet: true, shareViaRam: shared[0], ipv6Slot: 0 },
{ name: "private", routeToInternet: false, shareViaRam: shared[1], ipv6Slot: 1 },
{ name: "data", routeToInternet: false, shareViaRam: shared[2], ipv6Slot: 2 },
{
name: "public",
role: "public",
routeToInternet: true,
shareWith: shared[0] ? ["app-dev"] : [],
ipv6Slot: 0,
},
{
name: "private",
role: "private",
routeToInternet: false,
shareWith: shared[1] ? ["app-dev"] : [],
ipv6Slot: 1,
},
{
name: "data",
role: "data",
routeToInternet: false,
shareWith: shared[2] ? ["app-dev"] : [],
ipv6Slot: 2,
},
];

/**
Expand Down Expand Up @@ -147,23 +165,23 @@ beforeAll(async () => {
name: "edge",
role: "ztna-connectors",
routeToInternet: true,
shareViaRam: true,
shareWith: ["app-dev"],
ipv6Slot: 0,
formerNames: ["public"],
},
{
name: "flow",
role: "flow-hosts",
routeToInternet: false,
shareViaRam: true,
shareWith: ["app-dev"],
ipv6Slot: 1,
formerNames: ["private"],
},
{
name: "analytics",
role: "analytics",
routeToInternet: false,
shareViaRam: true,
shareWith: ["app-dev"],
ipv6Slot: 2,
formerNames: ["data"],
},
Expand Down Expand Up @@ -238,15 +256,21 @@ beforeAll(async () => {
name: "edge",
role: "ztna-connectors",
routeToInternet: true,
shareViaRam: false,
shareWith: [],
ipv6Slot: 0,
},
{ name: "flow", role: "flow-hosts", routeToInternet: false, shareViaRam: true, ipv6Slot: 1 },
{
name: "flow",
role: "flow-hosts",
routeToInternet: false,
shareWith: ["app-dev"],
ipv6Slot: 1,
},
{
name: "analytics",
role: "analytics",
routeToInternet: false,
shareViaRam: true,
shareWith: ["app-dev"],
ipv6Slot: 2,
},
]);
Expand Down Expand Up @@ -388,22 +412,25 @@ describe("sharing is per tier and fails closed", () => {
expect(shares).toEqual(["s6-vpc-share", "s6-vpc-share-analytics-dev", "s6-vpc-share-app-dev"]);
});

it("refuses a tier that gives both shareWith and shareViaRam, or neither", () => {
it("refuses the removed shareViaRam, a tier without shareWith, and a tier without a role", () => {
const legacy = { name: "x", role: "x", routeToInternet: false, shareViaRam: true };
expect(() =>
mod.validateTierSharing(
[{ name: "x", routeToInternet: false, shareViaRam: true, shareWith: [] }],
{}
)
).toThrow(/exactly one/);
expect(() => mod.validateTierSharing([{ name: "x", routeToInternet: false }], {})).toThrow(
/exactly one/
);
mod.validateTierSharing([legacy as unknown as import("./shared-vpc.js").SubnetTier], {})
).toThrow(/shareViaRam, removed in 0.6.0/);
const noShare = { name: "x", role: "x", routeToInternet: false };
expect(() =>
mod.validateTierSharing([noShare as unknown as import("./shared-vpc.js").SubnetTier], {})
).toThrow(/must give shareWith/);
const noRole = { name: "x", routeToInternet: false, shareWith: [] };
expect(() =>
mod.validateTierSharing([noRole as unknown as import("./shared-vpc.js").SubnetTier], {})
).toThrow(/must name its role/);
});

it("refuses an account that is not in sharedAccounts", () => {
expect(() =>
mod.validateTierSharing(
[{ name: "x", routeToInternet: false, shareWith: ["nobody"] }],
[{ name: "x", role: "x", routeToInternet: false, shareWith: ["nobody"] }],
ACCOUNTS
)
).toThrow(/not in sharedAccounts/);
Expand All @@ -413,8 +440,14 @@ describe("sharing is per tier and fails closed", () => {
expect(() =>
mod.validateTierSharing(
[
{ name: "edge", routeToInternet: true, shareWith: [], formerNames: ["public"] },
{ name: "public", routeToInternet: true, shareWith: [] },
{
name: "edge",
role: "ztna-connectors",
routeToInternet: true,
shareWith: [],
formerNames: ["public"],
},
{ name: "public", role: "public", routeToInternet: true, shareWith: [] },
],
{}
)
Expand All @@ -425,7 +458,13 @@ describe("sharing is per tier and fails closed", () => {
describe("helpers", () => {
it("tierNamed finds a tier by its former name", () => {
const tiers = [
{ name: "flow", routeToInternet: false, shareWith: [], formerNames: ["private"] },
{
name: "flow",
role: "flow-hosts",
routeToInternet: false,
shareWith: [],
formerNames: ["private"],
},
];
expect(mod.tierNamed(tiers, "private")?.name).toBe("flow");
expect(mod.tierNamed(tiers, "data")).toBeUndefined();
Expand All @@ -434,33 +473,48 @@ describe("helpers", () => {
it("legacyRamAssociationNames counts subnets across the old shared tiers", () => {
const tier = {
name: "flow",
role: "flow-hosts",
routeToInternet: false,
shareWith: ["a"],
formerNames: ["private"],
};
expect(
mod.legacyRamAssociationNames("dev", ["public", "private", "data"], [tier], tier, 3, 6)
mod.legacyRamAssociationNames(
"dev",
["public", "private", "data"],
[tier],
tier,
3,
6,
ACCOUNTS
)
).toEqual(["dev-ram-private-9"]);
expect(mod.legacyRamAssociationNames("dev", ["data"], [tier], tier, 0, 6)).toEqual([]);
expect(mod.legacyRamAssociationNames("dev", ["data"], [tier], tier, 0, 6, ACCOUNTS)).toEqual(
[]
);
});

it("without a layout, a renamed shareViaRam tier gets its index under every name", () => {
it("without a layout, a renamed tier in the main share gets its index under every name", () => {
const edge = {
name: "edge",
role: "ztna-connectors",
routeToInternet: true,
shareViaRam: false,
shareWith: [],
formerNames: ["public"],
};
const flow = {
name: "flow",
role: "flow-hosts",
routeToInternet: false,
shareViaRam: true,
shareWith: ["app-dev"],
formerNames: ["private"],
};
expect(mod.legacyRamAssociationNames("dev", undefined, [edge, flow], flow, 1, 6)).toEqual([
"dev-ram-flow-1",
"dev-ram-private-1",
]);
expect(mod.legacyRamAssociationNames("dev", undefined, [edge, flow], edge, 0, 6)).toEqual([]);
const accounts = { "111111111111": "app-dev" };
expect(
mod.legacyRamAssociationNames("dev", undefined, [edge, flow], flow, 1, 6, accounts)
).toEqual(["dev-ram-flow-1", "dev-ram-private-1"]);
expect(
mod.legacyRamAssociationNames("dev", undefined, [edge, flow], edge, 0, 6, accounts)
).toEqual([]);
});
});
Loading
Loading