PAYCARGO · AI-SANDBOX · PROCESS

3Apply Network (Stages 1-6, alert mode)

The largest substrate block: VPC, three trust-zone subnet tiers, Network Firewall, NAT, 13 VPC endpoints, DNS Firewall, flow logs. Lands in alert mode — logs would-block traffic, drops nothing.
Block: 3 of N · Operator: terraform apply -target=module.network from composition · Cost: ~$425/month standing · Permanence: Permanent
Pending review

State: Network plan generated via terraform plan -target=module.network -out=network.tfplan. Apply not yet executed.

Plan summary: 82 resources to add, 0 changes, 0 destroys. All + create. The largest single block by resource count.

Visual companion: Step.3.diagram.html

Executive Summary

Build the entire network layer the AI platform runs inside — in alert mode, so nothing gets blocked yet.

This is the biggest block in the substrate by resource count (82 resources), cost (~$425/month standing), and operational consequence (the first block where a mistake could affect traffic, not just metadata). It establishes three trust-zone subnets (agent / lockbox / egress), the single egress chokepoint (AWS Network Firewall), the private path to AWS services (13 VPC endpoints), and the dual L4/L7 egress defense (NFW + DNS Firewall). Both firewalls land in alert mode: they observe and log all traffic, drop nothing. Enforcement is deferred to Step 5, after real agent workloads from Step 4 are exercising the network and observability is live to monitor the flip.

StatusPending
Cost~$425/month
EffortPlan: ~4 weeks (already done)
Apply: ~15-20 min
Review: ~1 hour
RiskMedium

If skipped: No VPC for any AWS workload to run inside. No firewall surface to defend egress. No private path to AWS services (everything goes over public internet via NAT). No flow logs, no DNS observability. Every subsequent block (data, observability, lambda-tools, runtime) cannot deploy because it has nowhere to attach.

1. Why this step exists

Bootstrap (Step 1) created the substrate. Identity (Step 2) created the trust zones at the cryptographic and IAM layers. Network (Step 3) creates the trust zones at the physical layer — the actual VPC, subnets, route tables, and firewall surfaces that workloads live in. After this step, every downstream resource (data, observability, lambda-tools, runtime) has somewhere to attach.

The one-line summary

Network is where the platform's egress doctrine becomes structural. Three trust-zone subnets (agent / lockbox / egress) with explicit route table policies make "lockbox cannot reach the internet" a property of the routing layer, not just IAM. The Network Firewall makes "agents can only egress to the 4 SoRs" a property of the data plane, not just policy.

Why this block is the biggest by far

The network surface is where AWS gives you the most knobs — subnet topology, route tables, security groups, NACLs, VPC endpoints, NFW rule groups, DNS Firewall, flow logs, log destinations, encryption keys. Each is a small decision. The 82-resource count reflects how many distinct decisions are encoded in the design, not gratuitous complexity. The Network Module Reference document (_docs/Network Module Reference.html) walks the topology in detail.

2. What this step creates (82 resources, categorized)

LayerResourcesCountPurpose
Core VPCVPC + IGW + NAT GW + EIP4The VPC itself (10.0.0.0/16), the internet attachment, source-NAT for egress traffic, the elastic IP NAT uses
Subnets9 subnets across 3 AZs × 3 tiers9agent-private (/22) × 3, lockbox-private (/24) × 3, egress (/24) × 3
RoutingRoute tables + associations + routes24One route table per subnet (9 + 9 associations), plus 6 explicit routes (defaults, NFW endpoint forwarding)
Security groups4 SGs + 8 rules + default SG lockdown12agent-sg, lockbox-sg, endpoint-sg, egress-sg, plus their ingress/egress rules, plus the default SG locked down to no rules
VPC endpoints13 endpoints132 gateway (S3, DynamoDB, free) + 11 interface (Bedrock×4, KMS, Secrets, STS, ECR API/DKR, Logs, Lambda)
Network FirewallNFW + policy + 2 rule groups + logging config5The firewall resource, its policy, stateless drop-bad rules, stateful FQDN allowlist rules, and the logging destination
DNS FirewallRule group + association + 2 rules + 2 domain lists6The DNS-level second layer of egress defense, with the 4-SoR allowlist + catch-all alert
DNS query loggingQuery log config + association2Captures every DNS query for audit
Flow logsFlow log + 4 CloudWatch log groups + IAM role + inline policy7VPC-wide flow log delivery to CloudWatch, encrypted with the audit CMK from Step 2
Total82

All resources are tagged with Project=ai-sandbox, Owner=platform-team, ManagedBy=terraform, Module=network.

3. The three trust-zone topology

The doctrine: every subnet in the VPC belongs to exactly one of three trust zones, each with a distinct purpose and a distinct internet posture.

ZoneCIDRs (3 AZs)WorkloadsDefault routeEgress to internet
agent-private 10.0.0.0/22
10.0.4.0/22
10.0.8.0/22
AgentCore runtime, Lambda tools (future blocks) 0.0.0.0/0 → NFW endpoint Allowed via NFW → NAT → IGW (4 SoRs only in enforce mode)
lockbox-private 10.0.16.0/24
10.0.17.0/24
10.0.18.0/24
Lockbox reveal Lambda, DDB token table (future) (no default route) Impossible by construction. No IGW route exists in any subnet here.
egress 10.0.32.0/24 (single AZ at POC; 3 AZs in multi) NFW endpoint, NAT GW (no agent workloads) 0.0.0.0/0 → IGW The only subnet with a direct IGW route. NFW post-inspection traffic flows through here.

The egress packet chain

agent ENI → agent-sg (443 outbound only) → agent-private route table
          → NFW endpoint in egress subnet
          → (TLS SNI inspected against 4-SoR allowlist)
          → NAT Gateway in egress subnet
          → (source-NAT to NAT's EIP)
          → IGW
          → SoR API (Salesforce / Monday / GitHub / GitBook)

The AWS-API path (separate from the egress chain)

agent or lockbox ENI → VPC interface endpoint (in same subnet, private DNS)
                     → AWS service (Bedrock / KMS / Secrets / STS / Logs / ECR)

AWS API traffic never traverses NAT, never traverses IGW, never hits NFW. Cheaper, faster, more secure, and keeps the NFW allowlist focused on the 4 third-party SoRs.

4. What "alert mode" means — and why we defer enforcement

Both the Network Firewall and the Route53 DNS Firewall land in alert mode. This is the safe state.

NFW in alert mode (vs. drop mode in Step 5)

AspectAlert mode (this step)Drop mode (Step 5)
Stateful rule group actionLogs match, allows trafficLogs match, drops traffic
What an agent calling httpbin.org seesCall succeeds; alert log entry createdCall hangs / times out; alert log entry created
What an agent calling api.salesforce.com seesCall succeeds; alert log entry createdCall succeeds; no alert (allowlisted)
Operational riskNone — nothing is blockedReal — a missed allowlist entry breaks a workload

DNS Firewall in ALERT mode (vs. BLOCK mode in Step 5)

Same pattern: ALERT logs DNS queries that match the catch-all rule but resolves them anyway. BLOCK responds with NXDOMAIN.

Why we defer the flip to Step 5

  1. Alert log is only meaningful when real workloads exist. Until Step 4 deploys lambda-tools, the agent runtime, and the first agent, the alert log mostly captures AWS-internal noise. The 4-SoR allowlist needs to be validated against actual agent traffic.
  2. Observability has to be live to monitor the flip. CloudTrail, GuardDuty, and Bedrock invocation logging are part of the observability block (Step 4). Without them, flipping NFW to drop is flying blind.
  3. The audit S3 bucket from the data block is a Stage 9 dependency. Network has a "flow logs to S3" stage that needs the data block's audit bucket; doing data first means network can reach final form in one pass.

5. Cost & permanence

Cost (single-AZ POC mode, locked in terraform.tfvars)

ItemMonthlyNote
NFW endpoint (1 AZ)~$284$0.395/hr × 1 AZ × 720h
NAT Gateway (1 AZ)~$32$0.045/hr × 1 AZ × 720h
Interface endpoints (11)~$79$0.01/hr per AZ × 11 endpoints × 1 AZ
DNS Firewall + log ingest~$30~$0.60/M queries + CloudWatch ingest
Flow logs ingest~$5-10POC traffic volume; ~$0.50/GB ingested
Total baseline (single-AZ POC)~$425

Two-AZ graduation (internal app / SaaS replacement tier)

When the platform graduates from POC to supporting internal apps (PayCargo's own SaaS-replacement workloads), the next step is two-AZ resilience — not full three-AZ production. Two AZs adds the cheapest meaningful failover (an AZ-a outage no longer breaks egress); three AZs adds marginal additional resilience at meaningful additional cost and is reserved for true production posture later.

TierUse caseNFWNATEndpoints (11 ×)Baseline
1 AZ (POC, current)Sandbox, prototyping$284$32$79~$425/mo
2 AZ (internal app / SaaS replacement)Replacing existing internal SaaS tools with AI-powered alternatives; not customer-facing yet$569$65$158~$822/mo
3 AZ (full production, future)Customer-facing production, regulated workloads$853$97$237~$1,217/mo

Why 2 AZ is the right next tier (not 3)

Trade-off remaining at 2 AZ

A two-AZ deployment survives an AZ outage by routing traffic through the surviving AZ. The trade-off is that cross-AZ data charges ($0.01/GB) apply for workloads in AZ-c calling endpoints in AZ-a or AZ-b. At any reasonable sandbox or internal-app traffic volume, this is negligible (sub-$10/month). At sustained high-traffic production, it becomes a third-AZ argument.

How to graduate when ready

The single-line change in terraform.tfvars: az_redundancy = "dual". Terraform plan will show ~22 resources to add (one NFW endpoint, one NAT + EIP, 11 interface endpoint ENIs in the second AZ, route table updates). Apply takes the same ~15-20 minutes (NFW endpoint creation dominates). Rollback is symmetric: revert the var, plan, apply — resources are torn down cleanly.

Note: the current az_redundancy variable supports single and multi; a follow-up module change adds dual as the explicit two-AZ option so it is not conflated with eventual three-AZ.

Variable cost on top of standing

NFW data processing ($0.065/GB), NAT data processing ($0.045/GB), interface endpoint data ($0.01/GB), cross-AZ data ($0.01/GB). At POC traffic volumes (sub-1GB/day combined), variable cost is well under $20/month.

Permanence

Permanent. Tearing down the network breaks every downstream module that has any workload attached. NAT and EIP have a few-second creation time but a sticky deletion (release EIP, then delete NAT, then delete subnet routing). Don't terraform destroy network casually.

6. How to apply

Preconditions

Commands

cd substrate/environments/ai-sandbox

# Plan ONLY the network module (already done; re-plan if anything's changed)
terraform plan -target=module.network -out=network.tfplan

# READ the plan. Expected:
#   Plan: 82 to add, 0 to change, 0 to destroy.
# All resource lines should be + create. NFW + VPC endpoints dominate the count.

terraform apply network.tfplan

# Capture outputs for downstream blocks (data, observability, lambda-tools)
terraform output > /tmp/network-outputs.txt

Expected apply duration breakdown

PhaseTimeWhat's happening
VPC + IGW + subnets + route tables~30sFoundational layer, parallel
Security groups + rules~10sParallel
EIP + NAT GW~3-4 minNAT is the slowest single resource here
VPC endpoints (13)~3-5 minCreated in parallel; bound by AWS provisioning speed
Network Firewall + policy + rule groups~10-15 minNFW is the longest-creating AWS resource in this entire build
DNS Firewall + logging~30sQuick
Flow logs + IAM + log groups~15sQuick
Total~15-20 minMost of it waiting for NFW

7. Verification

# 1. VPC exists with the right CIDR
AWS_PROFILE=ai-sandbox aws ec2 describe-vpcs \
  --filters "Name=tag:Project,Values=ai-sandbox" \
  --query 'Vpcs[].{VpcId:VpcId,Cidr:CidrBlock,State:State}' --output table

# 2. 9 subnets exist (3 AZs x 3 tiers)
AWS_PROFILE=ai-sandbox aws ec2 describe-subnets \
  --filters "Name=tag:Project,Values=ai-sandbox" \
  --query 'length(Subnets)' --output text
# Expected: 9

# 3. NFW is live and the firewall policy is attached
AWS_PROFILE=ai-sandbox aws network-firewall describe-firewall \
  --firewall-name ai-sandbox-nfw \
  --query '{Status:FirewallStatus.Status, Policy:Firewall.FirewallPolicyArn}'

# 4. NFW is in ALERT mode (stateful default action)
AWS_PROFILE=ai-sandbox aws network-firewall describe-firewall-policy \
  --firewall-policy-name ai-sandbox-nfw-policy \
  --query 'FirewallPolicy.StatefulDefaultActions' --output text
# Expected: aws:alert_strict (or similar alert-mode default)

# 5. DNS Firewall rule group association is in ALERT mode
AWS_PROFILE=ai-sandbox aws route53resolver list-firewall-rules \
  --firewall-rule-group-id "$(aws route53resolver list-firewall-rule-groups \
    --query 'FirewallRuleGroups[?Name==\`ai-sandbox-dns-fw\`].Id' --output text)" \
  --query 'FirewallRules[].{Domain:FirewallDomainListId,Action:Action}' --output table
# Expected: catch-all rule has Action=ALERT (not BLOCK)

# 6. 13 VPC endpoints (2 gateway + 11 interface)
AWS_PROFILE=ai-sandbox aws ec2 describe-vpc-endpoints \
  --filters "Name=tag:Project,Values=ai-sandbox" \
  --query 'length(VpcEndpoints)' --output text
# Expected: 13

# 7. Flow logs are configured and writing to CloudWatch (audit CMK)
AWS_PROFILE=ai-sandbox aws ec2 describe-flow-logs \
  --filter "Name=tag:Project,Values=ai-sandbox" \
  --query 'FlowLogs[].{LogDest:LogDestination, FlowLogStatus:FlowLogStatus}' --output table
# Expected: FlowLogStatus=ACTIVE, LogDest contains "ai-sandbox" log group

8. How to view the work in AWS Console

This is the step where the visual story really helps. The VPC has nine subnets, three trust zones, a firewall, two NATs, and 14 endpoints — reading about it in text is one thing; seeing the topology in the AWS Console builds the mental model that makes the doctrine obvious. The "Resource map" view is especially worth a minute.

What to seeWhere to lookWhat confirms success (and what's visible doctrine)
VPC topology (the visual) VPC → Your VPCs → click ai-sandbox-vpcResource map tab The whole VPC as a diagram. 9 subnets arranged by AZ. IGW attached. NAT Gateway in us-east-1a. Subnets color-coded by route table. The lockbox subnets visibly do not connect to the IGW — that's the structural isolation, in pixels. Click any subnet to see its route table.
The 9 subnets VPC → Subnets → filter by VPC vpc-0ed3a16a8b82f7a34 3 agent-private (10.0.0.0/22, 10.0.4.0/22, 10.0.8.0/22), 3 lockbox-private (10.0.16.0/24, 10.0.17.0/24, 10.0.18.0/24), 3 egress (10.0.32.0/24, 10.0.33.0/24, 10.0.34.0/24). Click a lockbox subnet, scroll down to Route table, confirm no 0.0.0.0/0 entry.
Lockbox isolation, visible VPC → Route tables → filter ai-sandbox-rt-lockbox Three route tables, one per AZ. Open any. The Routes tab shows: 10.0.0.0/16 → local, S3 prefix list → gateway endpoint, DDB prefix list → gateway endpoint. That's all. No internet route. See Step.3.routing-tables.html for the full walkthrough.
Agent default route to NFW VPC → Route tables → filter ai-sandbox-rt-agent → click one Routes tab shows 0.0.0.0/0 → vpce-0119843d5454e1ed0 (the NFW endpoint). All outbound from agent workloads goes through the firewall first. Compare to the egress route table where 0.0.0.0/0 → igw-*.
Network Firewall live + alert mode VPC → Network Firewall → Firewalls → click ai-sandbox-nfw Status: Ready. Sync state: In sync. Policy: ai-sandbox-firewall-policy. Click the policy → Stateful rule evaluation order: Strict. Default stateful actions: aws:alert_strict — alert mode confirmed visually.
NFW SoR allowlist (the 4 SoRs) VPC → Network Firewall → Rule groups → click ai-sandbox-stateful-sor-allowlist Type: Stateful. Rule type: Domain list. Targets: lists the 4 SoRs (Salesforce subdomains, Monday, GitHub, GitBook). Target type: TLS_SNI, HTTP_HOST. Mode tag: alert. This is the allowlist, as data.
DNS Firewall in ALERT mode Route 53 → Resolver → DNS Firewall rule groups → click ai-sandbox-dns-fw Two rules: priority 1 = allow the SoR domain list; priority 100 = catch-all wildcard with action ALERT (not BLOCK). Query logging: enabled. Associated to VPC.
VPC endpoints (14 total) VPC → Endpoints → filter by VPC 14 rows: 2 Gateway (S3, DynamoDB), 11 Interface (Bedrock×4, ECR×2, KMS, Secrets, STS, Logs, Lambda), 1 GatewayLoadBalancer (the NFW endpoint, AWS-auto-created). Click any interface endpoint → Subnets tab shows the AZ-bound ENI.
NAT Gateway + EIP VPC → NAT gateways → ai-sandbox-nat-us-east-1a State: Available. Subnet: egress-us-east-1a. Elastic IP attached. This is the single egress source-IP for the whole VPC (single-AZ mode).
Internet Gateway VPC → Internet gateways → ai-sandbox-igw State: Attached to vpc-0ed3a16a8b82f7a34. Note: only the egress route tables reference it. Agent and lockbox subnets do not.
VPC flow logs (active) VPC → click ai-sandbox-vpc → Flow logs tab Status: Active. Destination: /aws/vpc/ai-sandbox/flow-logs CloudWatch log group. Encryption: audit CMK. Log format: custom 28-field. Capturing all (ACCEPT + REJECT).
NFW alert log (where blocked-or-not traffic shows up) CloudWatch → Log groups → /aws/network-firewall/ai-sandbox/alert Log group exists, encrypted with audit CMK. Probably empty right now (no workloads to generate traffic). Once Step 4 deploys agents, alert entries appear here for any non-SoR destination — this is what's reviewed before flipping NFW to drop in Step 5.
VPC flow log entries (data in motion) CloudWatch → Log groups → /aws/vpc/ai-sandbox/flow-logs → click any log stream Per-AZ streams. Each line is a flow record: source IP, dest IP, ports, packets, bytes, action (ACCEPT/REJECT), AWS service tags. Even without workloads, AWS-internal probes generate some entries — confirms the flow log pipeline is end-to-end functional.

Recommended sequence for the first viewing (~15 minutes total)

  1. Start with the Resource map — gives you the visual gestalt. Spend 3-5 minutes just looking at the topology. Click around. Note which subnets connect to which.
  2. Open a lockbox route table — confirm no 0.0.0.0/0. This is the doctrine.
  3. Open the NFW firewall policy — see aws:alert_strict. This is the safe-mode posture.
  4. Open the NFW rule group — see the 4 SoRs as data. This is the allowlist.
  5. Open the audit CMK key policy (Step 2) in a new tab and verify the flow log group's encryption references it. Cross-step doctrine connection visible.

The visual is the doctrine

Steps 1-3 had to be read to be understood. Step 3 can be seen. The trust zones, the routing isolation, the chokepoint topology — they exist as pixels in the AWS Console exactly the way they exist in the doctrine. When future engineers (or auditors) come through, the Resource map is the artifact that explains the design without anyone having to read the Terraform.

9. Anticipated gotchas

Gotcha A — -target warning (anticipated, ignored)

Same as Step 2. Terraform will warn about -target being non-routine. Ignore.

Gotcha B — NFW creation timeout perception

The Network Firewall resource takes 10-15 minutes to create. During this window, terraform apply appears to hang on a single line:

aws_networkfirewall_firewall.main: Still creating... [11m20s elapsed]

Not a fix — just wait. AWS provisions NFW endpoint ENIs across the configured AZs and that takes real time. If the apply errors after 30+ minutes, then there's a real problem (check CloudTrail for NFW API errors).

Gotcha C — SSO token expiry mid-apply

20-minute apply, 12-hour SSO token. If you started the session 11h45m ago, the token will expire mid-apply with ExpiredToken.

Fix: re-run aws sso login --profile ai-sandbox before starting if you're near the boundary. If it dies mid-apply, re-auth and re-run terraform apply network.tfplan — Terraform is idempotent.

Gotcha D — VPC endpoint service availability

11 interface endpoints, all for AWS services. Almost all are available in us-east-1. Bedrock-specific endpoints (bedrock-runtime, bedrock-agent, bedrock-agent-runtime) require the service to be active in the region; us-east-1 has them all, but if AWS changes that, an endpoint may fail to create.

Fix: remove the unavailable endpoint from the network module's endpoint list, re-plan, re-apply. The endpoint can be added back when AWS launches it.

Gotcha E — default SG already locked down (or not)

The module includes an aws_default_security_group resource that strips the default SG of all rules. If a previous apply (or manual console action) modified the default SG, Terraform may try to bring it under management and the diff might look surprising.

Verify: the plan output for aws_default_security_group.no_rules should show no ingress/egress rules. If it shows any, manual cleanup may be needed in console first.

Gotcha F — IAM role propagation for flow logs

The flow log delivery role is created in the same apply. AWS IAM is eventually consistent (~5-10s). If the flow log resource is created before IAM has propagated the role, it errors with InvalidParameter: Unable to assume role. Rare with current AWS performance.

Fix: wait 30 seconds, re-run apply. Terraform's depends_on reduces but does not eliminate this race.

9. Go / no-go gates

Pass all four gates before terraform apply network.tfplan.

1
Identity gate

aws sts get-caller-identity returns Account 959228203854 with SSO admin ARN. Token is fresh (re-auth if >10h old).

2
Plan gate

terraform plan -target=module.network shows 82 to add, 0 to change, 0 to destroy. All + create. Any drift on a freshly-deployed account is a no-go.

3
Safety toggle gate

substrate/environments/ai-sandbox/terraform.tfvars has all four safety toggles in safe positions: nfw_enforcement_mode = "alert", dns_firewall_action = "ALERT", flow_logs_destination = "cloudwatch", az_redundancy = "single". Verify with grep -E "(nfw_enforcement|dns_firewall|flow_logs|az_redundancy)" substrate/environments/ai-sandbox/terraform.tfvars.

4
Time gate

You have an uninterrupted 20-minute window. NFW creation alone is 10-15 minutes; total apply is 15-20 minutes. Not a good time to start if you have a meeting in 10 minutes.

If all four pass, run terraform apply network.tfplan.

10. Doctrine implications

Structural enforcement at the routing layer

The lockbox subnets have no default route. This is the strongest enforcement possible: the internet is not unreachable because of a policy decision, it is unreachable because there is no route for the packet to take. Removing the route is a structural property of the subnet, not a state of a firewall. Even if NFW is misconfigured, even if SGs are wide open, even if IAM grants every permission — a packet from a lockbox workload destined for the internet has nowhere to go.

Two independent egress defense layers

NFW operates at L4 with stateful TLS SNI inspection. DNS Firewall operates at L7 with query-time hostname filtering. They are independent — a gap in one is covered by the other. Both are configured with the same 4-SoR allowlist, but they enforce it via different mechanisms. Defense in depth.

The egress chokepoint is identity-blind

NFW does not know which IAM role originated a packet — it sees TLS handshakes and TCP/IP headers only. That is intentional: the chokepoint enforces "this destination is allowed for any workload" rather than "this destination is allowed for this role." Role-level scoping happens at the application layer (IAM policies on tier roles), and the network layer enforces the floor that no role can bypass.

Alert-first deployment is a doctrine, not a quirk

Every traffic-affecting block in this build lands in observation mode first. Alert mode for NFW, ALERT for DNS Firewall, log-only for any future WAF. Enforcement is a separate, deliberate operator decision, gated on observed traffic patterns. The doctrine: never let an automation make the first enforcement decision.

11. What comes next

After network apply succeeds and verification passes:

  1. Mark Step 3 complete in Step.3.html and update index.html.
  2. Stay in alert mode. Do not flip nfw_enforcement_mode or dns_firewall_action yet. They flip in Step 5, after Step 4 deploys real workloads.
  3. Begin Step 4 — the workload layer. Six sub-blocks: data, observability, guardrails, memory, gateway, lambda-tools, first agent. Each will have its own Step.5.x.html as we get there.

Network is now the substrate the rest of the platform attaches to. Every downstream module references at least one of network's outputs (vpc_id, agent_subnet_ids, lockbox_subnet_ids, agent_security_group_id, endpoint_security_group_id, etc.).