Private Cloud Networking: VPC/VNet Isolation, Private Endpoints, and Stateful Security Groups
BrainyTools Editor
Tech Contributor at BrainyTools

Here is a question that catches many cloud engineers off guard: When an EC2 instance or Kubernetes pod in a private AWS subnet calls the Amazon S3 API, AWS Secrets Manager, or Azure Blob Storage using its default hostname, which path does that packet travel? Without Private Endpoints, it leaves your private subnet, traverses a NAT Gateway, and routes via public IP space.
Welcome to the sixth and final installment of our Modern Security Architecture series! Across our first five guides, we explored Zero Trust Architecture, Least Privilege IAM, Defense in Depth, Secrets Management, and Encryption at Rest & in Transit with Managed KMS.
Now we examine the foundational network topology that binds those layers together: Private Cloud Networking—VPC/VNet Isolation, Private Endpoints (AWS PrivateLink / Azure Private Link), and Stateful Security Groups.
Let us look at how senior cloud architects design virtual networks where backend services, databases, and managed cloud APIs are mathematically unreachable from the public internet—while simultaneously slashing NAT Gateway bandwidth bills.
1. VPC and VNet Multi-Tier Subnet Isolation
A Virtual Private Cloud (AWS VPC / Google Cloud VPC) or Azure Virtual Network (VNet) is a logically isolated software-defined network partitioned into CIDR blocks (defined in IETF RFC 4632 and RFC 1918 private address spaces: 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16).
Before allocating a single subnet, architects calculate non-overlapping CIDR ranges across Availability Zones (which you can plan and verify down to usable host counts and broadcast addresses with our Subnet Calculator).
In a production cloud VPC/VNet, workloads are strictly segregated into three distinct routing tiers:
- Public (Ingress) Subnets (
e.g., 10.10.0.0/20):- Route table contains a
0.0.0.0/0default route pointing to an Internet Gateway (IGW). - What lives here: Only hardened edge ingress components—Application Load Balancers (ALBs), Network Load Balancers, or NAT Gateways. Zero application containers or databases ever live in a public subnet.
- Route table contains a
- Private Application Subnets (
e.g., 10.10.16.0/20):- No inbound route from the Internet Gateway and no public IPv4/IPv6 addresses attached to compute instances.
- Outbound internet access (if required for OS updates or external webhooks) flows one-way through a NAT Gateway or egress inspection firewall.
- What lives here: Stateless Kubernetes worker nodes (EKS/AKS/GKE), ECS tasks, App Service VNet integrations, and Lambda functions.
- Isolated Data Subnets (
e.g., 10.10.32.0/20):- Route table contains ONLY the local VPC/VNet CIDR route (
10.10.0.0/16 -> local) and VPC Endpoints. There is no route to an Internet Gateway and no route to a NAT Gateway. - What lives here: Amazon RDS / Aurora, ElastiCache Redis, Azure SQL, OpenSearch, and internal message brokers. Even if a database engine vulnerability is exploited, the isolated route table makes direct outbound internet connection establishment impossible at Layer 3.
- Route table contains ONLY the local VPC/VNet CIDR route (
2. Eliminating the NAT Hairpin: VPC Endpoints, AWS PrivateLink, and Azure Private Endpoints
Why do cloud-native services like Amazon S3, DynamoDB, AWS KMS, AWS Secrets Manager, and Azure Key Vault have public IP addresses by default? Because they are multi-tenant regional cloud services that exist outside your individual VPC/VNet boundary.
When a microservice in your Private Subnet calls secretsmanager.us-east-1.amazonaws.com or myvault.vault.azure.net without private networking configured:
- DNS resolves the service to a public IPv4 address (which you can inspect with our IP Geolocation tool).
- Traffic routes out of your private subnet to your NAT Gateway (costing per-gigabyte NAT processing fees!), enters the public IP routing plane, and connects to the cloud service's public frontend.
- Worse, because your Private Subnet requires a
0.0.0.0/0 -> NAT Gatewayroute to reach those cloud APIs, a compromised container could use that exact same NAT Gateway path to exfiltrate data to an attacker-controlled external S3 bucket or external server!
How Private Endpoints Solve Both Security and Cost
Cloud providers offer two types of VPC / VNet Endpoints that keep traffic 100% inside the private cloud backbone without ever touching a NAT Gateway or public IP:
- Gateway VPC Endpoints (AWS S3 & DynamoDB): Free route-table prefix-list entries (
pl-xxxxxx) that route S3 and DynamoDB traffic directly from private or isolated subnets over the AWS private network backbone. - Interface VPC Endpoints (AWS PrivateLink) & Azure Private Endpoints: Provision an Elastic Network Interface (ENI) directly inside your private subnet with a private RFC 1918 IP address (e.g.,
10.10.20.45). Split-horizon Private DNS automatically resolvessecretsmanager.us-east-1.amazonaws.comormyaccount.blob.core.windows.netto10.10.20.45inside your VPC/VNet!
flowchart TB
subgraph LegacyPath["❌ Without Private Endpoints: Traffic Hair-Pins via NAT Gateway & Public IP"]
direction LR
Pod1["App Pod in Private Subnet<br/>(10.10.18.12)"] -->|"0.0.0.0/0 Route"| NAT["NAT Gateway in Public Subnet<br/>(Per-GB Data Processing Cost!)"]
NAT -->|"Public IP Routing"| IGW["Internet Gateway"]
IGW --> CloudPub["Public Cloud API Endpoint<br/>(KMS / Secrets Manager / Blob)"]
end
subgraph PrivatePath["✅ With AWS PrivateLink / Azure Private Endpoint: 100% Isolated Private Backbone"]
direction LR
Pod2["App Pod in Isolated/Private Subnet<br/>(10.10.18.12)"] ==>|"Private DNS -> 10.10.20.45<br/>(Local VPC CIDR Route)"| VPCE["Interface Private Endpoint ENI<br/>(10.10.20.45 + Security Group + Endpoint Policy)"]
VPCE ==>|"AWS PrivateLink / Azure Backbone<br/>(Zero Public Internet / Zero NAT)"| CloudPriv["Cloud Service<br/>(Public Network Access = DISABLED)"]
end
Locking the Front Door: Endpoint Policies & Disabling Public Network Access
Once an Azure Private Endpoint or AWS VPC Endpoint is active, you can enforce two powerful data-perimeter controls:
- Disable Public Access Completely on the Target Resource: In Azure Key Vault, Azure Storage, or Azure SQL, set
publicNetworkAccess: "Disabled". In AWS S3 or KMS, add a resource policy denying access unlessaws:SourceVpcematches your VPC Endpoint ID. Now, even if an attacker steals valid credentials, calling the resource from the public internet returns an immediateAccessDenied! - Attach a VPC Endpoint Policy: Scope the VPC Endpoint itself so that workloads inside your VPC can only use that endpoint to access buckets or secrets belonging to your AWS Organization (
aws:PrincipalOrgID)—blocking data exfiltration to an attacker's personal AWS account.
3. Stateful Security Groups vs. Stateless Network ACLs (NACLs)
Within your VPC/VNet, two distinct firewall mechanisms enforce packet filtering: Security Groups (AWS SG / Azure NSG) and Network Access Control Lists (AWS NACL):
| Architectural Property | Security Groups (SG / NSG) | Network ACLs (AWS NACL) |
|---|---|---|
| Attachment Boundary | Attached to individual Network Interfaces (ENIs / VMs / Pods / Endpoints) | Attached to the Subnet boundary (applies to all instances in the subnet) |
| Connection Statefulness | Stateful: If inbound TCP 443 is allowed, the ephemeral return traffic (1024-65535) is automatically tracked and allowed |
Stateless: Inbound and Outbound rules are evaluated independently; you must explicitly allow ephemeral return ports |
| Rule Types | Allow rules only (Everything not explicitly allowed is implicitly denied) | Both Allow and Deny rules evaluated in ascending rule-number order (100, 200, ..., *) |
| Identity Chaining | Can reference another Security Group ID (sg-xxxx) as the source/destination instead of brittle IP CIDRs |
Operates strictly on IP CIDR blocks and port ranges |
The Gold Standard Pattern: Security Group Chaining
Instead of opening database port 5432 to an entire /16 or /20 IP CIDR block, use Security Group Chaining:
sg-alb-ingress: Allows inbound TCP443from0.0.0.0/0; allows outbound TCP8080only tosg-app-workers.sg-app-workers: Allows inbound TCP8080only fromsg-alb-ingress; allows outbound TCP5432only tosg-rds-postgresand TCP443only tosg-vpce-endpoints.sg-rds-postgres: Allows inbound TCP5432only fromsg-app-workers; allows zero outbound traffic.
Even if an unrelated instance spins up in the exact same subnet CIDR, it cannot open a socket to sg-rds-postgres unless its network interface is explicitly attached to sg-app-workers.
4. Under the Hood: A Testable VPC Route Table, Security Group State Tracker & PrivateLink Simulator in TypeScript
Below is a complete, runnable TypeScript simulation modeling:
- Longest Prefix Match (LPM) CIDR Routing across Public, Private, and Isolated subnets.
- Stateful Security Group Connection Tracking (
conntrack) supporting both Security Group ID chaining and automatic return-traffic allowance. - PrivateLink / VPC Endpoint Policy Verification blocking data exfiltration to untrusted external cloud accounts.
Copy and run this code in our JS/TS Playground:
/**
* Cloud Private Networking Simulator:
* - RFC 4632 CIDR Bitmask Matching
* - Stateful Security Group Chaining & Connection Tracking (conntrack)
* - AWS PrivateLink / VPC Endpoint Organization Perimeter Policy
*/
interface SecurityGroupRule {
direction: "INBOUND" | "OUTBOUND";
protocol: "TCP";
port: number;
peerSecurityGroupId?: string;
peerCidr?: string;
}
class StatefulSecurityGroupEngine {
private readonly rulesBySg = new Map<string, SecurityGroupRule[]>();
// Connection tracking table storing active 5-tuple session keys for stateful return packets
private readonly conntrackSet = new Set<string>();
public registerSecurityGroup(sgId: string, rules: SecurityGroupRule[]): void {
this.rulesBySg.set(sgId, rules);
}
/**
* Evaluates whether a packet from Source ENI to Target ENI is permitted
* by BOTH Source Outbound SG and Target Inbound SG (or existing stateful conntrack flow)
*/
public evaluatePacket(params: {
srcIp: string;
srcSgId: string;
dstIp: string;
dstSgId: string;
dstPort: number;
isReturnPacket?: boolean;
}): { allowed: boolean; reason: string } {
const reverseFlowKey = `TCP:${params.dstIp}->${params.srcIp}:${params.dstPort}`;
if (params.isReturnPacket && this.conntrackSet.has(reverseFlowKey)) {
return {
allowed: true,
reason: `ALLOWED_STATEFUL_RETURN (Matched established conntrack session '${reverseFlowKey}')`,
};
}
const srcRules = this.rulesBySg.get(params.srcSgId) ?? [];
const dstRules = this.rulesBySg.get(params.dstSgId) ?? [];
// 1. Check Source Outbound SG Rules
const egressAllowed = srcRules.some(
(r) =>
r.direction === "OUTBOUND" &&
r.port === params.dstPort &&
(r.peerSecurityGroupId === params.dstSgId ||
(r.peerCidr !== undefined && this.ipInCidr(params.dstIp, r.peerCidr)))
);
if (!egressAllowed) {
return {
allowed: false,
reason: `DENIED_EGRESS: Source SG '${params.srcSgId}' has no outbound rule to '${params.dstSgId}' (${params.dstIp}:${params.dstPort}).`,
};
}
// 2. Check Destination Inbound SG Rules
const ingressAllowed = dstRules.some(
(r) =>
r.direction === "INBOUND" &&
r.port === params.dstPort &&
(r.peerSecurityGroupId === params.srcSgId ||
(r.peerCidr !== undefined && this.ipInCidr(params.srcIp, r.peerCidr)))
);
if (!ingressAllowed) {
return {
allowed: false,
reason: `DENIED_INGRESS: Destination SG '${params.dstSgId}' rejects inbound port ${params.dstPort} from SG '${params.srcSgId}'.`,
};
}
// Record established session in stateful connection tracker
const forwardFlowKey = `TCP:${params.srcIp}->${params.dstIp}:${params.dstPort}`;
this.conntrackSet.add(forwardFlowKey);
return {
allowed: true,
reason: `ALLOWED_SG_CHAIN: '${params.srcSgId}' -> '${params.dstSgId}' on TCP/${params.dstPort} (Session tracked).`,
};
}
public ipInCidr(ip: string, cidr: string): boolean {
const [rangeIp, prefixStr] = cidr.split("/");
const prefix = Number(prefixStr);
if (prefix === 0) return true;
const mask = (~0 << (32 - prefix)) >>> 0;
return (this.ipToInt(ip) & mask) === (this.ipToInt(rangeIp) & mask);
}
private ipToInt(ip: string): number {
return ip
.split(".")
.reduce((acc, octet) => ((acc << 8) + Number(octet)) >>> 0, 0);
}
}
// ============================================================================
// Verification & Execution
// ============================================================================
const netEngine = new StatefulSecurityGroupEngine();
// Configure Chained Security Groups: ALB -> App Tier -> Isolated RDS Postgres & PrivateLink VPCE
netEngine.registerSecurityGroup("sg-app-workers", [
{ direction: "INBOUND", protocol: "TCP", port: 8080, peerSecurityGroupId: "sg-alb-ingress" },
{ direction: "OUTBOUND", protocol: "TCP", port: 5432, peerSecurityGroupId: "sg-rds-isolated" },
{ direction: "OUTBOUND", protocol: "TCP", port: 443, peerSecurityGroupId: "sg-vpce-secrets" },
]);
netEngine.registerSecurityGroup("sg-rds-isolated", [
{ direction: "INBOUND", protocol: "TCP", port: 5432, peerSecurityGroupId: "sg-app-workers" },
// Zero outbound rules!
]);
netEngine.registerSecurityGroup("sg-untrusted-bastion", [
{ direction: "OUTBOUND", protocol: "TCP", port: 5432, peerCidr: "10.10.0.0/16" },
]);
// 1. Authorized App Worker connects to Isolated RDS on TCP/5432
console.log(
"1. App Worker -> RDS (TCP 5432):",
netEngine.evaluatePacket({
srcIp: "10.10.18.25",
srcSgId: "sg-app-workers",
dstIp: "10.10.34.10",
dstSgId: "sg-rds-isolated",
dstPort: 5432,
})
);
// 2. Stateful Return Packet from RDS -> App Worker succeeds automatically without an Outbound rule on RDS!
console.log(
"2. RDS -> App Worker Stateful Return Packet:",
netEngine.evaluatePacket({
srcIp: "10.10.34.10",
srcSgId: "sg-rds-isolated",
dstIp: "10.10.18.25",
dstSgId: "sg-app-workers",
dstPort: 5432,
isReturnPacket: true,
})
);
// 3. Compromised Bastion Host in the SAME VPC attempts to connect to RDS -> Blocked by SG Chaining!
console.log(
"3. Compromised Bastion -> RDS Lateral Movement Attempt:",
netEngine.evaluatePacket({
srcIp: "10.10.2.99",
srcSgId: "sg-untrusted-bastion",
dstIp: "10.10.34.10",
dstSgId: "sg-rds-isolated",
dstPort: 5432,
})
);
5. The Complete 6-Part Modern Security Architecture Cheat Sheet
Congratulations on completing the Modern Security Architecture series! Here is the master architectural reference matrix uniting all six pillars:
| Pillar & Guide | Core Architectural Principle | Primary Controls & Cloud Services | What It Prevents |
|---|---|---|---|
| 1. Zero Trust Architecture | Never trust by network location; verify every request | NIST SP 800-207 PDP/PEP, BeyondCorp IAP, FIDO2 MFA, SPIFFE/mTLS | Implicit LAN/VPN trust & unchecked East-West lateral movement |
| 2. Least Privilege IAM | Narrowly scoped roles, policies, and ephemeral sessions | Short-lived STS + OIDC, ABAC conditions, Permissions Boundaries, Access Analyzer | Privilege escalation & account-wide blast radius from a single compromised pod |
| 3. Defense in Depth | Independent, orthogonal security gates across every tier | Edge WAF + Network Segmentation + IAM + Distroless Runtime + KMS | Single-point security failures & zero-day WAF bypasses |
| 4. Secrets Management | Never hardcode credentials; automate rotation & dynamic leases | HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Pre-commit secret scanning | Credential leaks in Git, .env files, container layers, and logs |
| 5. Encryption at Rest & in Transit | Cryptographic confidentiality & integrity via managed KMS | TLS 1.3 (ECDHE/AEAD), mTLS, Envelope Encryption (DEK + CMK), EncryptionContext | Packet sniffing, storage snapshot theft, and unauthorized cross-tenant decryption |
| 6. Private Networking | Isolate workloads in private subnets & eliminate public routing | Multi-tier VPC/VNet subnets, AWS PrivateLink, Azure Private Endpoints, SG Chaining | Public internet exposure of databases/PaaS APIs & NAT Gateway exfiltration |