Ko-fiSupport
Security2026-11-04•11 min read

Private Cloud Networking: VPC/VNet Isolation, Private Endpoints, and Stateful Security Groups

B

BrainyTools Editor

Tech Contributor at BrainyTools

Private Cloud Networking: VPC/VNet Isolation, Private Endpoints, and Stateful Security Groups

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:

  1. Public (Ingress) Subnets (e.g., 10.10.0.0/20):
    • Route table contains a 0.0.0.0/0 default 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.
  2. 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.
  3. 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.

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:

  1. DNS resolves the service to a public IPv4 address (which you can inspect with our IP Geolocation tool).
  2. 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.
  3. Worse, because your Private Subnet requires a 0.0.0.0/0 -> NAT Gateway route 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 resolves secretsmanager.us-east-1.amazonaws.com or myaccount.blob.core.windows.net to 10.10.20.45 inside 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:

  1. 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 unless aws:SourceVpce matches your VPC Endpoint ID. Now, even if an attacker steals valid credentials, calling the resource from the public internet returns an immediate AccessDenied!
  2. 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 TCP 443 from 0.0.0.0/0; allows outbound TCP 8080 only to sg-app-workers.
  • sg-app-workers: Allows inbound TCP 8080 only from sg-alb-ingress; allows outbound TCP 5432 only to sg-rds-postgres and TCP 443 only to sg-vpce-endpoints.
  • sg-rds-postgres: Allows inbound TCP 5432 only from sg-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:

  1. Longest Prefix Match (LPM) CIDR Routing across Public, Private, and Isolated subnets.
  2. Stateful Security Group Connection Tracking (conntrack) supporting both Security Group ID chaining and automatic return-traffic allowance.
  3. 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

Related Videos

Enjoyed this article?

Check out our suite of free online developer tools to boost your productivity even further. 100% Privacy Focused.

Explore Tools