DEV Community

Cover image for AWS VPC
Obidiegwu Onyedikachi Henry
Obidiegwu Onyedikachi Henry

Posted on

AWS VPC

VPC is the foundation everything else in AWS sits on. EC2, RDS, ECS, Lambda in a VPC, EKS, all of it lives inside a VPC. Most engineers use VPCs without understanding them. They click through the wizard, accept the defaults, and move on. Then a security audit happens, or a cross-account connection fails, or a NAT Gateway bill arrives, and the gaps show up.

This guide builds VPC understanding from the ground up. Every concept is introduced with the problem it solves. By the end you will be able to design a production-grade multi-account, multi-region network architecture from scratch, and explain every decision.


Table of Contents

  1. What a VPC Is - The Core Abstraction
  2. CIDR Notation - The Language of IP Addressing
  3. Subnets - Dividing the Network
  4. Route Tables - Where Traffic Goes
  5. Internet Gateway - Public Internet Access
  6. NAT Gateway - Outbound-Only Internet for Private Resources
  7. Security Groups - Stateful Instance-Level Firewall
  8. Network ACLs - Stateless Subnet-Level Firewall
  9. VPC Endpoints - Private Access to AWS Services
  10. VPC Peering - Connecting Two VPCs
  11. Transit Gateway - Hub-and-Spoke Networking at Scale
  12. AWS Direct Connect - Dedicated Private Connectivity
  13. Site-to-Site VPN - Encrypted Tunnel to On-Premises
  14. Client VPN - Remote Access for Individuals
  15. DHCP Option Sets - Customising Network Configuration
  16. Elastic IPs and ENIs - IP Address Management
  17. Flow Logs - Network Traffic Visibility
  18. DNS in VPC - Route 53 Resolver
  19. PrivateLink - Exposing Services Privately Across Accounts
  20. RAM - Resource Access Manager for Cross-Account Sharing
  21. Production VPC Design - The Reference Architecture

What a VPC Is - The Core Abstraction

The Problem

When you launch a resource in AWS, an EC2 instance, an RDS database, a Lambda function, it needs a network to live in. Without network isolation, every resource in every AWS account in the world would be on the same flat network. Any resource could reach any other resource. Security would be impossible. Routing would be chaos.

What a VPC Is

A Virtual Private Cloud (VPC) is a logically isolated network that you define within AWS. It behaves like a traditional data centre network, with IP address ranges, subnets, routing tables, and firewalls, but it is software-defined. There is no physical hardware to rack. No cables to run. The entire network is configuration.

Within a VPC, you control:

  • The IP address range
  • How the network is divided into subnets
  • Which subnets can reach the internet
  • Which resources can communicate with each other
  • What traffic is allowed in and out

A VPC is regional. It exists within one AWS region and spans all Availability Zones in that region. Your subnets live in specific AZs within the VPC.

Default VPC vs Custom VPC

Every AWS account comes with a default VPC in every region. The default VPC:

  • Has a 172.31.0.0/16 CIDR block
  • Has a subnet in every AZ with public IP assignment enabled
  • Has an Internet Gateway attached
  • Has a default route to the Internet Gateway

The default VPC is designed for convenience, not production. It exists so you can launch an EC2 instance immediately without configuring networking. For anything beyond experimentation, you build a custom VPC.

Production VPCs are never the default. They have carefully planned CIDR ranges, private subnets for sensitive resources, controlled internet access, and explicit security boundaries.


CIDR Notation - The Language of IP Addressing

What CIDR Is

Before designing a VPC, you need to understand CIDR (Classless Inter-Domain Routing) notation. It is the language used to define IP address ranges.

A CIDR block looks like this: 10.0.0.0/16

The number after the slash is the prefix length, the number of bits that are fixed. The remaining bits define the host range.

An IPv4 address is 32 bits. 10.0.0.0/16 means the first 16 bits are fixed (10.0), and the remaining 16 bits are available for hosts and subnets.

Calculating Host Count

/16 prefix = 32 - 16 = 16 variable bits = 2^16 = 65,536 addresses
/24 prefix = 32 - 24 = 8 variable bits  = 2^8  = 256 addresses
/28 prefix = 32 - 28 = 4 variable bits  = 2^4  = 16 addresses
Enter fullscreen mode Exit fullscreen mode

AWS reserves 5 addresses in every subnet:

  • .0 - Network address
  • .1 - VPC router
  • .2 - AWS DNS
  • .3 - Reserved for future use
  • .255 - Broadcast address (not used, but reserved)

So a /28 subnet (16 addresses) gives you 16 - 5 = 11 usable addresses.

Choosing a VPC CIDR

RFC 1918 defines private IP ranges. Use these for VPCs:

Range CIDR Total Addresses
10.0.0.0 – 10.255.255.255 10.0.0.0/8 16.7 million
172.16.0.0 – 172.31.255.255 172.16.0.0/12 1 million
192.168.0.0 – 192.168.255.255 192.168.0.0/16 65,536

Planning rules:

  • Do not overlap CIDRs between VPCs you will ever connect. If VPC A is 10.0.0.0/16 and VPC B is 10.0.0.0/16, you can never peer them. Their address spaces overlap.
  • Plan for growth. A /24 VPC (256 addresses) feels fine until you need 300 resources.
  • Use consistent schemes across accounts. A common pattern: allocate a /8 or /10 to your organisation, then carve it into /16 per environment or account.

Example allocation for a multi-account organisation:

10.0.0.0/8  - Organisation total space

10.0.0.0/16  - Production account, us-east-1
10.1.0.0/16  - Production account, eu-west-1
10.2.0.0/16  - Staging account, us-east-1
10.3.0.0/16  - Development account, us-east-1
10.4.0.0/16  - Shared services account, us-east-1
10.5.0.0/16  - Security account, us-east-1
Enter fullscreen mode Exit fullscreen mode

Every VPC in the organisation has a unique, non-overlapping CIDR. They can be connected to each other without conflicts.


Subnets - Dividing the Network

What Subnets Are

A subnet is a range of IP addresses within your VPC. Every resource in your VPC, EC2 instances, RDS databases, Lambda functions, load balancers, is placed in a subnet.

Subnets serve two purposes:

  1. AZ placement - A subnet exists in exactly one Availability Zone. Placing resources in subnets across multiple AZs gives you resilience.
  2. Traffic control - Subnets are associated with route tables. The route table determines where traffic from that subnet can go.

Public vs Private Subnets

A subnet is public or private based on its route table, specifically, whether it has a route to an Internet Gateway.

Public subnet - Has a route to an Internet Gateway. Resources in a public subnet can communicate with the internet (if they have a public IP). The internet can initiate connections to resources with public IPs.

Private subnet - No route to an Internet Gateway. Resources in a private subnet cannot be reached from the internet and cannot initiate direct connections to the internet.

Private does not mean isolated. Resources in private subnets can still communicate with each other, with resources in other subnets via VPC routing, and with the internet via a NAT Gateway. They just cannot be directly reached from the internet.

The Standard Subnet Design

For a production VPC in 10.0.0.0/16 across three AZs:

VPC: 10.0.0.0/16

Public Subnets (for load balancers, NAT Gateways, bastion hosts):
  10.0.0.0/24   - AZ-a  (251 usable IPs)
  10.0.1.0/24   - AZ-b
  10.0.2.0/24   - AZ-c

Private App Subnets (for EC2, ECS, EKS worker nodes):
  10.0.10.0/23  - AZ-a  (507 usable IPs)
  10.0.12.0/23  - AZ-b
  10.0.14.0/23  - AZ-c

Private Data Subnets (for RDS, ElastiCache, OpenSearch):
  10.0.20.0/24  - AZ-a  (251 usable IPs)
  10.0.21.0/24  - AZ-b
  10.0.22.0/24  - AZ-c
Enter fullscreen mode Exit fullscreen mode

Three tiers, three AZs, nine subnets. Each tier has a distinct purpose and distinct security controls.

CloudFormation

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: production-vpc

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: false    # Never auto-assign public IPs
      Tags:
        - Key: Name
          Value: public-subnet-a
        - Key: Tier
          Value: public

  PrivateAppSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.10.0/23
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: private-app-subnet-a
        - Key: Tier
          Value: private-app

  PrivateDataSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.20.0/24
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: private-data-subnet-a
        - Key: Tier
          Value: private-data
Enter fullscreen mode Exit fullscreen mode

Notice MapPublicIpOnLaunch: false even on public subnets. Public IPs should be assigned explicitly where needed, not automatically. This forces intentionality.


Route Tables - Where Traffic Goes

What Route Tables Do

A route table is a set of rules, routes, that determine where network traffic is directed. Every subnet is associated with exactly one route table. Every VPC has a main route table that subnets use by default.

A route has two components:

  • Destination - The IP address range the route applies to
  • Target - Where to send traffic destined for that range

How Routing Works

When a resource in a subnet sends traffic, the VPC router looks at the destination IP address and finds the most specific matching route in the subnet's route table.

Route Table for Public Subnet:

Destination       Target
10.0.0.0/16      local         ← All VPC traffic goes to local router
0.0.0.0/0        igw-abc123    ← All other traffic goes to Internet Gateway
Enter fullscreen mode Exit fullscreen mode

The local route is created automatically for every VPC. It covers the entire VPC CIDR and routes traffic within the VPC. You cannot delete it.

The 0.0.0.0/0 route (the default route) sends all traffic not matched by a more specific route to the specified target. For public subnets, the target is the Internet Gateway.

Route Table for Private App Subnet:

Destination       Target
10.0.0.0/16      local
0.0.0.0/0        nat-abc123    ← Internet-bound traffic goes to NAT Gateway
Enter fullscreen mode Exit fullscreen mode

Private subnets have a default route to the NAT Gateway instead of the Internet Gateway. Traffic can get out (via NAT), but nothing from the internet can initiate a connection in.

Route Table for Private Data Subnet:

Destination       Target
10.0.0.0/16      local
                              ← No default route. No internet access at all.
Enter fullscreen mode Exit fullscreen mode

The data tier has no default route. Database servers have no path to or from the internet. They can only talk to resources within the VPC.

CloudFormation

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: public-route-table

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable

  PrivateAppRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: private-app-route-table-a

  PrivateAppDefaultRouteA:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateAppRouteTableA
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGatewayA

  PrivateAppSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateAppSubnetA
      RouteTableId: !Ref PrivateAppRouteTableA
Enter fullscreen mode Exit fullscreen mode

One private route table per AZ for the app tier. Why not one shared route table for all three private subnets? Because each private subnet routes internet traffic to the NAT Gateway in its own AZ. If AZ-a goes down, the NAT Gateway in AZ-a goes down too. Private subnets in AZ-b and AZ-c should route to their own NAT Gateways, not AZ-a's. One route table per AZ enables this.


Internet Gateway - Public Internet Access

What It Is

An Internet Gateway (IGW) is a horizontally scaled, redundant, highly available VPC component that enables communication between your VPC and the internet. It does two things:

  1. Provides a target in route tables for internet-routable traffic
  2. Performs Network Address Translation (NAT) for resources with public IP addresses, translating between the private IP inside the VPC and the public IP visible on the internet

How It Works

A resource in a public subnet with a public IP address:

  • Sends a packet to 8.8.8.8 (Google's DNS)
  • The VPC router matches 0.0.0.0/0 and sends the packet to the IGW
  • The IGW sees the source private IP (10.0.0.25) and the associated public IP (52.90.100.200)
  • The IGW translates the source to the public IP and forwards the packet
  • The response comes back to the public IP
  • The IGW translates back to the private IP and delivers it to the instance

This translation is called one-to-one NAT. One private IP maps to one public IP. The instance itself does not know its public IP, it only sees its private IP.

What Would Happen Without It

Without an Internet Gateway attached to the VPC, no subnet can reach the internet, regardless of route table configuration. The IGW is the gateway. The route table just tells traffic to go there.

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: production-igw

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
Enter fullscreen mode Exit fullscreen mode

An IGW can only be attached to one VPC at a time. A VPC can only have one IGW.


NAT Gateway - Outbound-Only Internet for Private Resources

The Problem

Your application servers are in private subnets. They need to download OS patches, pull container images from ECR, or call external APIs. But they must not be directly reachable from the internet.

An Internet Gateway handles public subnets, resources with public IPs. Private resources have no public IPs. They need a way to initiate outbound connections to the internet without having an inbound-accessible public address.

What NAT Gateway Is

A NAT Gateway (Network Address Translation Gateway) sits in a public subnet and provides outbound internet access for private resources. Private resources send traffic to the NAT Gateway, which translates their private IP to its own Elastic IP (public IP) and forwards the traffic. Responses come back to the NAT Gateway, which translates back and delivers to the private resource.

The critical property: this translation is one-directional. The internet can respond to connections initiated from inside, but cannot initiate new connections to the private resources. There is no public IP on the private resource for the internet to target.

Architecture

Private EC2 (10.0.10.15)
    ↓ sends packet to 8.8.8.8
Private Route Table: 0.0.0.0/0 → nat-abc123
    ↓
NAT Gateway (10.0.0.5, EIP: 52.90.100.201) in Public Subnet
    ↓ translates source 10.0.10.15 → 52.90.100.201
Internet Gateway
    ↓
8.8.8.8
    ↓ response to 52.90.100.201
NAT Gateway
    ↓ translates back to 10.0.10.15
Private EC2
Enter fullscreen mode Exit fullscreen mode

High Availability

A NAT Gateway is AZ-specific. If the AZ it is in fails, the NAT Gateway fails. For high availability, deploy one NAT Gateway per AZ and configure each AZ's private route table to use its own NAT Gateway.

  EIPA:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NATGatewayA:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIPA.AllocationId
      SubnetId: !Ref PublicSubnetA
      Tags:
        - Key: Name
          Value: nat-gateway-a

  EIPB:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NATGatewayB:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIPB.AllocationId
      SubnetId: !Ref PublicSubnetB
      Tags:
        - Key: Name
          Value: nat-gateway-b
Enter fullscreen mode Exit fullscreen mode

The Cost Reality

NAT Gateway costs money in two ways: hourly ($0.045/hour per gateway) and per GB processed ($0.045/GB). In a three-AZ design, three NAT Gateways cost approximately $97/month in idle costs before any data is processed.

For development environments, one NAT Gateway across all AZs is acceptable. For production, the HA cost is justified.

NAT Gateway is also a frequent source of unexpected AWS bills. A Lambda function in a VPC that calls an external API thousands of times per second generates significant NAT Gateway data processing costs. VPC Endpoints (section 9) eliminate this cost for AWS service traffic.

NAT Gateway vs NAT Instance

AWS used to recommend NAT Instances, EC2 instances configured to perform NAT. NAT Gateway replaced them. NAT Gateway is managed, scales automatically, is highly available within an AZ, and requires no administration. NAT Instance requires patching, scaling, and management. Always use NAT Gateway.


Security Groups - Stateful Instance-Level Firewall

What Security Groups Are

A Security Group is a virtual firewall that controls inbound and outbound traffic at the resource level, EC2 instances, RDS databases, Lambda functions in a VPC, load balancers.

Security Groups are stateful. If an inbound rule allows traffic in, the response traffic is automatically allowed out, regardless of outbound rules. You do not need matching outbound rules for established connections.

How They Work

A Security Group has inbound rules and outbound rules. Each rule specifies:

  • Protocol - TCP, UDP, ICMP, or All
  • Port range - Single port or range
  • Source/Destination - CIDR block, another Security Group, or prefix list
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for web application servers
      VpcId: !Ref VPC
      SecurityGroupIngress:
        # HTTPS from the load balancer only
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          SourceSecurityGroupId: !Ref ALBSecurityGroup

        # HTTP from the load balancer only (for redirect)
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ALBSecurityGroup

      SecurityGroupEgress:
        # HTTPS to internet (for external API calls)
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0

        # PostgreSQL to database security group
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          DestinationSecurityGroupId: !Ref DatabaseSecurityGroup

        # Redis to cache security group
        - IpProtocol: tcp
          FromPort: 6379
          ToPort: 6379
          DestinationSecurityGroupId: !Ref CacheSecurityGroup

  DatabaseSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for PostgreSQL RDS
      VpcId: !Ref VPC
      SecurityGroupIngress:
        # PostgreSQL only from web servers
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          SourceSecurityGroupId: !Ref WebServerSecurityGroup
      SecurityGroupEgress: []    # No outbound - databases don't initiate

  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Application Load Balancer
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0    # HTTPS from internet
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0    # HTTP from internet (for redirect)
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          DestinationSecurityGroupId: !Ref WebServerSecurityGroup
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          DestinationSecurityGroupId: !Ref WebServerSecurityGroup
Enter fullscreen mode Exit fullscreen mode

Security Group Chaining

The most important pattern: reference Security Groups in rules, not CIDR blocks, wherever possible.

# Instead of this (allows any IP in the 10.0.10.0/23 range):
- IpProtocol: tcp
  FromPort: 5432
  ToPort: 5432
  CidrIp: 10.0.10.0/23

# Do this (allows only resources in the WebServerSecurityGroup):
- IpProtocol: tcp
  FromPort: 5432
  ToPort: 5432
  SourceSecurityGroupId: !Ref WebServerSecurityGroup
Enter fullscreen mode Exit fullscreen mode

When you reference a Security Group, the rule applies to any resource assigned that Security Group, regardless of IP address. It is identity-based, not location-based. A new web server added to the Security Group automatically gets database access. An instance not in the Security Group cannot reach the database, even if it is in the same subnet.

What Would Happen Without Correct Security Groups

The default Security Group in a VPC allows all outbound and all inbound from within the same Security Group. Applied broadly, this means all resources in the same Security Group can talk to each other on any port, a flat network with no segmentation. A compromised web server could reach the database directly. That is not a network architecture. That is one layer of failed perimeter security.


Network ACLs - Stateless Subnet-Level Firewall

What NACLs Are

A Network ACL (Access Control List) is a firewall at the subnet level. Unlike Security Groups (which are resource-level and stateful), NACLs are subnet-level and stateless.

Stateless means: each packet is evaluated independently. If an inbound rule allows a request in, you must also have an outbound rule that allows the response out, the return traffic is not automatically allowed.

NACL Rules

NACLs have numbered rules evaluated in order, lowest first. The first matching rule is applied.

  PublicSubnetNACL:
    Type: AWS::EC2::NetworkAcl
    Properties:
      VpcId: !Ref VPC

  # Inbound rules
  NACLInboundHTTPS:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PublicSubnetNACL
      RuleNumber: 100
      Protocol: 6          # TCP
      RuleAction: allow
      Egress: false
      CidrBlock: 0.0.0.0/0
      PortRange:
        From: 443
        To: 443

  NACLInboundHTTP:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PublicSubnetNACL
      RuleNumber: 110
      Protocol: 6
      RuleAction: allow
      Egress: false
      CidrBlock: 0.0.0.0/0
      PortRange:
        From: 80
        To: 80

  # Ephemeral ports - required for return traffic (stateless!)
  NACLInboundEphemeral:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PublicSubnetNACL
      RuleNumber: 120
      Protocol: 6
      RuleAction: allow
      Egress: false
      CidrBlock: 0.0.0.0/0
      PortRange:
        From: 1024
        To: 65535         # Ephemeral port range for response traffic

  # Deny all other inbound
  NACLInboundDenyAll:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PublicSubnetNACL
      RuleNumber: 32766
      Protocol: -1        # All protocols
      RuleAction: deny
      Egress: false
      CidrBlock: 0.0.0.0/0

  # Outbound rules
  NACLOutboundAll:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PublicSubnetNACL
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: true
      CidrBlock: 0.0.0.0/0
Enter fullscreen mode Exit fullscreen mode

The Ephemeral Port Requirement

This is the most common NACL mistake. TCP responses use ephemeral ports temporary high-numbered ports (1024-65535) chosen by the operating system. When a client connects to port 443, the response goes back to the client's ephemeral port, some random port above 1024.

Because NACLs are stateless, they evaluate the response packet independently. If you have an inbound rule allowing port 443 but no inbound rule allowing ephemeral ports, the response to your own client is blocked. You must explicitly allow inbound ephemeral ports for return traffic from your outbound connections, and outbound ephemeral ports for return traffic to your inbound connections.

Security Groups vs NACLs - When to Use Which

Security Group NACL
Level Resource (ENI) Subnet
State Stateful Stateless
Rules Allow only Allow and Deny
Evaluation All rules evaluated Rules evaluated in order, first match wins
Default Deny all inbound, allow all outbound Allow all inbound and outbound

Use Security Groups as your primary traffic control mechanism. They are more expressive, easier to reason about, and stateful. NACLs are a secondary defence, use them to implement subnet-level deny rules for broad IP ranges (blocking a known malicious IP range) where Security Groups cannot deny.

Security Groups cannot "deny", they can only "allow". If you need to explicitly block traffic from a specific IP range even if a Security Group would allow it, that is a NACL use case.


VPC Endpoints - Private Access to AWS Services

The Problem

Your EC2 instances are in private subnets. They need to access S3, DynamoDB, Secrets Manager, SSM Parameter Store. Without VPC Endpoints, this traffic goes through the NAT Gateway, out of the VPC, across the internet (via AWS's network), and back in. You pay NAT Gateway data processing costs for every byte. The traffic leaves the VPC boundary.

What VPC Endpoints Are

VPC Endpoints allow private connectivity to AWS services without traffic leaving the AWS network. No NAT Gateway. No Internet Gateway. No public IP required. The traffic stays entirely within AWS.

Two types:

Gateway Endpoints

Available only for S3 and DynamoDB. Free. Added as a route in your route table.

  S3GatewayEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PrivateAppRouteTableA
        - !Ref PrivateAppRouteTableB
        - !Ref PrivateAppRouteTableC
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: "*"
            Action:
              - s3:GetObject
              - s3:PutObject
              - s3:DeleteObject
            Resource:
              - !Sub "arn:aws:s3:::${AppBucket}/*"
              - !Sub "arn:aws:s3:::${AppBucket}"

  DynamoDBGatewayEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PrivateAppRouteTableA
        - !Ref PrivateAppRouteTableB
        - !Ref PrivateAppRouteTableC
Enter fullscreen mode Exit fullscreen mode

The endpoint adds a route to the route table: traffic destined for S3 or DynamoDB IP ranges goes to the endpoint, not to the NAT Gateway. No configuration change in your application, the AWS SDK already knows the S3 endpoint address. The routing is transparent.

The endpoint policy is an IAM-like policy that restricts which S3 buckets or DynamoDB tables can be accessed through the endpoint. This is an additional security control, even if an IAM policy allows access to another bucket, the endpoint policy can restrict it to only your application's buckets.

Interface Endpoints

Available for most other AWS services, SSM, Secrets Manager, KMS, SQS, SNS, ECR, CloudWatch, and many more. Uses AWS PrivateLink. Creates an Elastic Network Interface (ENI) in your subnet with a private IP address. Not free ($0.01/hour per AZ plus $0.01/GB).

  SecretsManagerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.secretsmanager"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref PrivateAppSubnetA
        - !Ref PrivateAppSubnetB
        - !Ref PrivateAppSubnetC
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      PrivateDnsEnabled: true    # Override public DNS with private endpoint IP

  SSMEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref PrivateAppSubnetA
        - !Ref PrivateAppSubnetB
        - !Ref PrivateAppSubnetC
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      PrivateDnsEnabled: true

  SSMMessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref PrivateAppSubnetA
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      PrivateDnsEnabled: true

  EC2MessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref PrivateAppSubnetA
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      PrivateDnsEnabled: true
Enter fullscreen mode Exit fullscreen mode

PrivateDnsEnabled: true is critical. It overrides the public DNS for the service with the private endpoint IP. Your application calls secretsmanager.us-east-1.amazonaws.com, with PrivateDnsEnabled, that resolves to the private endpoint IP inside your VPC. No code change required.

The three SSM endpoints (ssm, ssmmessages, ec2messages) are required together to enable AWS Systems Manager Session Manager, the secure shell replacement for EC2 instances in private subnets with no SSH, no bastion host, and no public IP.

The Cost Comparison

Without VPC Endpoints, a Lambda function in a private subnet that reads a secret from Secrets Manager on every invocation, invoked 1 million times per day, processes approximately 5 MB of data per million requests through the NAT Gateway.

NAT Gateway data: 5 MB/million × 1M/day × 30 days = 150 GB/month
Cost: 150 GB × $0.045/GB = $6.75/month
Plus NAT Gateway hourly: $0.045 × 24 × 30 × 3 AZs = $97.20/month

Total: ~$104/month for this one traffic pattern

VPC Interface Endpoint: $0.01/hour × 24 × 30 × 3 AZs = $21.60/month
Data processing: $0.01/GB × 150 GB = $1.50/month

Total: ~$23/month
Enter fullscreen mode Exit fullscreen mode

For high-throughput workloads, VPC Endpoints save significant money beyond the security benefit.


VPC Peering - Connecting Two VPCs

What It Is

VPC Peering creates a private network connection between two VPCs. Traffic routes via AWS's internal network, not through the internet. The two VPCs can be in the same account, different accounts, or different regions.

After peering, resources in VPC A can communicate with resources in VPC B as if they were on the same network, using private IP addresses.

Setup

Peering requires three steps:

  1. Create the peering connection (one VPC initiates, the other accepts)
  2. Add routes in both VPCs pointing to the peering connection
  3. Update Security Groups to allow traffic from the peer VPC's CIDR
# In the requester account/VPC
  PeeringConnection:
    Type: AWS::EC2::VPCPeeringConnection
    Properties:
      VpcId: !Ref VpcA
      PeerVpcId: vpc-0abc1234          # The peer VPC
      PeerOwnerId: "123456789012"      # Peer account ID
      PeerRegion: eu-west-1            # For cross-region peering
      Tags:
        - Key: Name
          Value: prod-to-shared-services

  # Route in VPC A pointing to VPC B
  RouteToVpcB:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateAppRouteTableA
      DestinationCidrBlock: 10.4.0.0/16    # VPC B's CIDR
      VpcPeeringConnectionId: !Ref PeeringConnection

# In the accepter account/VPC - must accept the request and add return routes
  # Route in VPC B pointing back to VPC A
  RouteToVpcA:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref SharedServicesRouteTable
      DestinationCidrBlock: 10.0.0.0/16    # VPC A's CIDR
      VpcPeeringConnectionId: !Ref PeeringConnection
Enter fullscreen mode Exit fullscreen mode

The Transitive Routing Problem

VPC Peering is not transitive. This is the most important limitation to understand.

VPC A ←→ VPC B ←→ VPC C

VPC A cannot reach VPC C through VPC B.
Enter fullscreen mode Exit fullscreen mode

If you want full mesh connectivity between three VPCs, you need three peering connections: A-B, B-C, and A-C. For five VPCs, you need ten peering connections. For ten VPCs, you need 45.

This does not scale. For more than a handful of VPCs, use Transit Gateway.

When VPC Peering Is the Right Choice

  • Two or three VPCs that need to communicate
  • Simple, point-to-point connectivity
  • No complex routing requirements
  • Cost-sensitive (peering has no hourly cost, only data transfer cost)

Transit Gateway - Hub-and-Spoke Networking at Scale

The Problem VPC Peering Cannot Solve

You have 20 VPCs, production, staging, development, one per business unit, shared services, security tooling. Full mesh peering requires 190 peering connections. Managing 190 peering connections, each with its own route table entries and Security Group rules, is operationally unmanageable.

Transit Gateway solves this.

What Transit Gateway Is

A Transit Gateway (TGW) is a regional network hub. VPCs and on-premises networks attach to the TGW. All routing goes through the TGW. Instead of 190 peering connections, you have 20 VPC attachments to one TGW.

           Transit Gateway
          /    |    |    \
     VPC A  VPC B  VPC C  VPC D
                |
           On-premises (via VPN or Direct Connect)
Enter fullscreen mode Exit fullscreen mode

Every VPC that attaches to the TGW can route to every other attached VPC, without individual peering connections. Adding a new VPC means one TGW attachment, not connections to every existing VPC.

TGW Route Tables

Transit Gateway has its own route tables. These are separate from VPC route tables. You create TGW route tables and associate VPC attachments with them, giving you full control over inter-VPC routing.

  TransitGateway:
    Type: AWS::EC2::TransitGateway
    Properties:
      Description: Organisation-wide network hub
      AutoAcceptSharedAttachments: disabled    # Explicit acceptance required
      DefaultRouteTableAssociation: disable    # Use custom route tables
      DefaultRouteTablePropagation: disable
      DnsSupport: enable
      VpnEcmpSupport: enable
      Tags:
        - Key: Name
          Value: org-transit-gateway

  # Attach production VPC to TGW
  ProductionVPCAttachment:
    Type: AWS::EC2::TransitGatewayVpcAttachment
    Properties:
      TransitGatewayId: !Ref TransitGateway
      VpcId: !Ref ProductionVPC
      SubnetIds:
        - !Ref ProductionPrivateSubnetA
        - !Ref ProductionPrivateSubnetB
        - !Ref ProductionPrivateSubnetC
      Tags:
        - Key: Name
          Value: production-vpc-attachment

  # TGW route table for production - can reach shared services, not dev
  ProductionRouteTable:
    Type: AWS::EC2::TransitGatewayRouteTable
    Properties:
      TransitGatewayId: !Ref TransitGateway
      Tags:
        - Key: Name
          Value: production-route-table

  # Associate production attachment with production route table
  ProductionRouteTableAssociation:
    Type: AWS::EC2::TransitGatewayRouteTableAssociation
    Properties:
      TransitGatewayAttachmentId: !Ref ProductionVPCAttachment
      TransitGatewayRouteTableId: !Ref ProductionRouteTable

  # Route: production can reach shared services
  RouteToSharedServices:
    Type: AWS::EC2::TransitGatewayRoute
    Properties:
      TransitGatewayRouteTableId: !Ref ProductionRouteTable
      DestinationCidrBlock: 10.4.0.0/16    # Shared services CIDR
      TransitGatewayAttachmentId: !Ref SharedServicesVPCAttachment

  # Route in production VPC pointing to TGW
  ProductionRouteToTGW:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref ProductionPrivateRouteTableA
      DestinationCidrBlock: 10.4.0.0/16    # Shared services
      TransitGatewayId: !Ref TransitGateway
Enter fullscreen mode Exit fullscreen mode

Network Segmentation with TGW Route Tables

Different TGW route tables enable network segmentation at scale. Production VPCs can reach shared services but not development VPCs. Development VPCs can reach shared services but not production. Inspection VPCs can see all traffic.

Production Route Table:
  → Shared Services VPC (10.4.0.0/16)
  → On-premises (10.100.0.0/16)
  NOT → Development VPC

Development Route Table:
  → Shared Services VPC (10.4.0.0/16)
  NOT → Production VPC
  NOT → On-premises

Shared Services Route Table:
  → Production VPC (10.0.0.0/16)
  → Development VPC (10.3.0.0/16)
  → On-premises (10.100.0.0/16)
Enter fullscreen mode Exit fullscreen mode

This is network segmentation that would require hundreds of peering connections and route table entries to replicate. With TGW, it is route table configuration.

Cross-Account and Cross-Region

TGW supports cross-account attachment via AWS RAM (Resource Access Manager). Share the TGW with other accounts; they attach their VPCs without needing a separate TGW.

TGW Peering connects Transit Gateways in different regions, enabling global network architectures without the internet as the backbone.


AWS Direct Connect - Dedicated Private Connectivity

The Problem

Your on-premises data centre needs to connect to AWS. A Site-to-Site VPN over the internet works, but it has variable latency, limited bandwidth, and the traffic traverses the public internet, which is a compliance concern for some regulated industries.

What Direct Connect Is

AWS Direct Connect is a dedicated, private network connection from your data centre to AWS. It does not use the internet. It is a physical fibre connection to an AWS Direct Connect location (a colocation facility where AWS has equipment).

Traffic on a Direct Connect connection:

  • Does not traverse the internet
  • Has consistent, predictable latency
  • Has higher bandwidth (up to 100 Gbps)
  • Is not subject to internet bandwidth fluctuations

Connection Types

Dedicated connection - A physical 1 Gbps, 10 Gbps, or 100 Gbps port at a Direct Connect location. AWS provisions the port. Your network connects to it.

Hosted connection - A sub-Gbps or 1-10 Gbps connection provided by an AWS Direct Connect Partner. The partner provides the physical infrastructure. Useful if you cannot co-locate equipment at a Direct Connect location.

Virtual Interfaces

Over a Direct Connect connection, you create Virtual Interfaces (VIFs):

Private VIF - Connects to a VPC via a Virtual Private Gateway or Transit Gateway. Traffic goes to your VPC's private IP addresses. Used for accessing private resources.

Public VIF - Connects to AWS public services (S3, DynamoDB, public endpoints) without traversing the internet. Traffic goes to AWS public IP ranges over the private connection.

Transit VIF - Connects to a Transit Gateway. Enables a single Direct Connect connection to reach multiple VPCs via TGW.

Resilience

A single Direct Connect connection is a single point of failure. For production, always configure two connections from different Direct Connect locations (for location redundancy) and use Site-to-Site VPN as a fallback.

On-Premises Router
├── Direct Connect Connection 1 → DX Location A → AWS Region
├── Direct Connect Connection 2 → DX Location B → AWS Region
└── Site-to-Site VPN → Internet → AWS Region (backup)
Enter fullscreen mode Exit fullscreen mode

Traffic prefers Direct Connect when available. If both Direct Connect connections fail, traffic fails over to the VPN automatically via BGP routing.


Site-to-Site VPN - Encrypted Tunnel to On-Premises

What It Is

AWS Site-to-Site VPN creates an encrypted IPsec tunnel between your on-premises network and your AWS VPC over the public internet. It is faster to set up than Direct Connect (minutes vs weeks/months), costs less, but has variable performance and does traverse the internet.

Components

Virtual Private Gateway (VGW) - The AWS-side endpoint of the VPN. Attached to a VPC.

Customer Gateway (CGW) - The on-premises-side endpoint. Represents your physical router or firewall.

VPN Connection - The tunnel between the VGW and CGW. Each connection has two tunnels for redundancy.

  VirtualPrivateGateway:
    Type: AWS::EC2::VPNGateway
    Properties:
      Type: ipsec.1
      Tags:
        - Key: Name
          Value: production-vgw

  VPCGatewayAttachmentVPN:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      VpnGatewayId: !Ref VirtualPrivateGateway

  CustomerGateway:
    Type: AWS::EC2::CustomerGateway
    Properties:
      Type: ipsec.1
      BgpAsn: 65000           # Your on-premises BGP AS number
      IpAddress: 203.0.113.1  # Your on-premises public IP
      Tags:
        - Key: Name
          Value: office-router

  VPNConnection:
    Type: AWS::EC2::VPNConnection
    Properties:
      Type: ipsec.1
      CustomerGatewayId: !Ref CustomerGateway
      VpnGatewayId: !Ref VirtualPrivateGateway
      StaticRoutesOnly: false    # Use BGP for dynamic routing
      Tags:
        - Key: Name
          Value: office-to-production

  # Enable route propagation - VGW automatically adds on-premises routes
  RoutePropagation:
    Type: AWS::EC2::VPNGatewayRoutePropagation
    Properties:
      RouteTableIds:
        - !Ref PrivateAppRouteTableA
        - !Ref PrivateAppRouteTableB
      VpnGatewayId: !Ref VirtualPrivateGateway
Enter fullscreen mode Exit fullscreen mode

Route propagation automatically adds routes for your on-premises network to your VPC route tables. Instead of manually adding a route for each on-premises subnet, the VGW learns routes via BGP and propagates them.

VPN vs Direct Connect

Site-to-Site VPN Direct Connect
Setup time Minutes Weeks to months
Cost ~$36/month + data From ~$216/month + data
Bandwidth Up to 1.25 Gbps Up to 100 Gbps
Latency Variable (internet) Consistent (dedicated)
Reliability Internet-dependent SLA-backed
Traverses internet Yes (encrypted) No
Use case Low cost, quick setup, lower traffic High bandwidth, consistent latency, compliance

Client VPN - Remote Access for Individuals

The Problem

Site-to-Site VPN connects networks. But a developer working from home, a support engineer travelling, or an operations team member needs individual access to private resources in the VPC. Site-to-Site VPN is not for individual users — it is for connecting office networks.

What Client VPN Is

AWS Client VPN is a managed OpenVPN service that allows individual users to connect to your VPC from anywhere. Each user authenticates, establishes an encrypted tunnel, and gains access to specified resources in the VPC.

  ClientVPNEndpoint:
    Type: AWS::EC2::ClientVpnEndpoint
    Properties:
      Description: Developer access to production VPC
      ServerCertificateArn: arn:aws:acm:us-east-1:123456789012:certificate/abc123
      AuthenticationOptions:
        - Type: federated-authentication
          FederatedAuthentication:
            SAMLProviderArn: !Ref SAMLProvider   # SSO via Okta, Azure AD, etc.
      ConnectionLogOptions:
        Enabled: true
        CloudwatchLogGroup: /aws/clientvpn
        CloudwatchLogStream: connections
      ClientCidrBlock: 172.16.0.0/22    # IP range assigned to VPN clients
      SplitTunnel: true                 # Only VPC traffic goes through VPN
      VpcId: !Ref VPC
      SecurityGroupIds:
        - !Ref ClientVPNSecurityGroup
      DnsServers:
        - 10.0.0.2                      # VPC DNS resolver

  ClientVPNNetworkAssociation:
    Type: AWS::EC2::ClientVpnTargetNetworkAssociation
    Properties:
      ClientVpnEndpointId: !Ref ClientVPNEndpoint
      SubnetId: !Ref PrivateAppSubnetA

  # Authorisation rule — which users can access which networks
  ClientVPNAuthRule:
    Type: AWS::EC2::ClientVpnAuthorizationRule
    Properties:
      ClientVpnEndpointId: !Ref ClientVPNEndpoint
      TargetNetworkCidr: 10.0.0.0/16    # Access to entire VPC
      AuthorizeAllGroups: false
      AccessGroupId: "engineers-group-id"  # Only engineers group
      Description: Engineers access to VPC
Enter fullscreen mode Exit fullscreen mode

SplitTunnel: true is important for productivity. With split tunnelling, only traffic destined for the VPC goes through the VPN. Internet traffic (GitHub, Slack, YouTube) uses the user's normal internet connection. Without split tunnelling, all traffic goes through the VPN, adding latency and NAT Gateway costs for traffic that has nothing to do with the VPC.


DHCP Option Sets - Customising Network Configuration

What They Are

When a resource launches in your VPC, it needs network configuration: an IP address, a default gateway, a DNS server, a domain name. DHCP (Dynamic Host Configuration Protocol) provides this automatically. VPCs have DHCP Option Sets that define what the DHCP server tells resources.

The default DHCP Option Set uses AmazonProvidedDNS for DNS and the VPC's domain (region.compute.internal or ec2.internal for us-east-1).

When to Customise

Custom DNS servers - If you run your own DNS servers on-premises and need resources in the VPC to resolve internal hostnames (like db.internal.company.com), point the DHCP Option Set at your DNS servers.

Custom domain name - Change the domain name used for internal hostname resolution. Instead of ip-10-0-10-15.ec2.internal, instances can be named ip-10-0-10-15.production.company.internal.

  CustomDHCPOptions:
    Type: AWS::EC2::DHCPOptions
    Properties:
      DomainName: production.company.internal
      DomainNameServers:
        - 10.0.0.2         # VPC resolver (must include this for Route 53 Private Hosted Zones)
        - 10.100.1.10      # On-premises DNS server
      NtpServers:
        - 169.254.169.123  # AWS NTP service
      Tags:
        - Key: Name
          Value: production-dhcp-options

  DHCPOptionsAssociation:
    Type: AWS::EC2::VPCDHCPOptionsAssociation
    Properties:
      VpcId: !Ref VPC
      DhcpOptionsId: !Ref CustomDHCPOptions
Enter fullscreen mode Exit fullscreen mode

One DHCP Option Set per VPC. Changes apply to new DHCP leases, existing instances do not update immediately unless they release and renew their lease.


Elastic IPs and ENIs - IP Address Management

Elastic IPs

An Elastic IP (EIP) is a static public IPv4 address that you allocate to your account and associate with resources. Unlike auto-assigned public IPs (which change when an instance stops and restarts), an EIP stays the same.

  ElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: nat-gateway-eip

  # Associate with an EC2 instance
  EIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      InstanceId: !Ref MyInstance
      AllocationId: !GetAtt ElasticIP.AllocationId
Enter fullscreen mode Exit fullscreen mode

Use EIPs for:

  • NAT Gateways (required)
  • Resources that external partners whitelist by IP
  • Resources where the IP must be known in advance (firewall rules, DNS)

Unattached EIPs cost money ($0.005/hour). Release them when not in use.

Elastic Network Interfaces

An Elastic Network Interface (ENI) is a virtual network card. Every EC2 instance has at least one ENI, its primary network interface. ENIs have:

  • Private IP address (primary and secondary)
  • Optional public IP or Elastic IP
  • MAC address
  • Security Groups
  • Source/destination check flag

Secondary ENIs can be attached to instances and moved between instances. This enables:

  • Dual-homed instances (connected to two subnets/VPCs)
  • Failover by moving an ENI (with its IP and MAC) to a standby instance
  • Licensing tied to MAC address that must stay consistent across failover
import boto3

ec2 = boto3.client('ec2', region_name='us-east-1')

# Create a secondary ENI
eni = ec2.create_network_interface(
    SubnetId='subnet-0abc1234',
    Description='Secondary interface for failover',
    Groups=['sg-0abc1234'],
    PrivateIpAddresses=[
        {'PrivateIpAddress': '10.0.10.50', 'Primary': True}
    ]
)

ENI_ID = eni['NetworkInterface']['NetworkInterfaceId']

# Attach to an instance
ec2.attach_network_interface(
    NetworkInterfaceId=ENI_ID,
    InstanceId='i-0abc1234',
    DeviceIndex=1    # 0 is primary, 1+ is secondary
)

# Move to another instance during failover
ec2.detach_network_interface(AttachmentId='eni-attach-abc123')
ec2.attach_network_interface(
    NetworkInterfaceId=ENI_ID,
    InstanceId='i-0def5678',    # Standby instance
    DeviceIndex=1
)
Enter fullscreen mode Exit fullscreen mode

Flow Logs - Network Traffic Visibility

What They Are

VPC Flow Logs capture metadata about IP traffic in your VPC. Not the packet contents, the metadata: source IP, destination IP, source port, destination port, protocol, bytes transferred, action (ACCEPT or REJECT), and whether the traffic was accepted or rejected by Security Group or NACL rules.

Flow Logs are the primary visibility tool for network troubleshooting and security investigation in a VPC.

What They Tell You

  • Which resources are communicating
  • Which connections are being rejected (Security Group or NACL blocking)
  • Unusual traffic patterns (potential data exfiltration, port scanning, lateral movement)
  • NAT Gateway and VPC Endpoint utilisation
  • Traffic volume between subnets

Configuration

Flow Logs can be captured at three levels:

  • VPC — All traffic in the VPC
  • Subnet — All traffic in a specific subnet
  • ENI — Traffic on a specific network interface
  FlowLogsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: vpc-flow-logs.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: FlowLogsPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogGroups
                  - logs:DescribeLogStreams
                Resource: "*"

  FlowLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /vpc/flow-logs/production
      RetentionInDays: 90

  VPCFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      ResourceType: VPC
      ResourceId: !Ref VPC
      TrafficType: ALL         # ACCEPT, REJECT, or ALL
      LogDestinationType: cloud-watch-logs
      LogGroupName: !Ref FlowLogGroup
      DeliverLogsPermissionArn: !GetAtt FlowLogsRole.Arn
      LogFormat: >-
        ${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr}
        ${srcport} ${dstport} ${protocol} ${packets} ${bytes}
        ${start} ${end} ${action} ${log-status}
        ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags}
        ${type} ${pkt-srcaddr} ${pkt-dstaddr}
Enter fullscreen mode Exit fullscreen mode

Querying Flow Logs with CloudWatch Insights

# Find all rejected connections (Security Group or NACL blocks)
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, protocol, action
| filter action = "REJECT"
| sort @timestamp desc
| limit 100

# Find the top talkers in the VPC
fields @timestamp, srcAddr, dstAddr, bytes
| stats sum(bytes) as totalBytes by srcAddr
| sort totalBytes desc
| limit 20

# Find connections to a specific port (potential port scanning)
fields @timestamp, srcAddr, dstAddr, dstPort
| filter dstPort = 22 and action = "REJECT"
| stats count() as attempts by srcAddr
| sort attempts desc

# Find traffic leaving to the internet on unusual ports
fields @timestamp, srcAddr, dstAddr, dstPort, bytes
| filter dstAddr not like /^10\./ and dstAddr not like /^172\./ and dstAddr not like /^192\.168\./
| filter dstPort != 443 and dstPort != 80
| sort bytes desc
Enter fullscreen mode Exit fullscreen mode

Flow Logs sent to S3 can be queried with Athena — more powerful for complex queries across long retention periods.


DNS in VPC - Route 53 Resolver

How VPC DNS Works

Every VPC has a DNS resolver at VPC_CIDR_BASE + 2. For 10.0.0.0/16, the resolver is at 10.0.0.2. This resolver handles:

  • Private Hosted Zone queries (your internal domains)
  • AWS service name resolution (EC2 hostnames, service endpoints)
  • Public DNS queries (forwarded to Route 53 public resolvers)

Private Hosted Zones

A Private Hosted Zone is a Route 53 DNS zone visible only within specified VPCs. It provides internal DNS names for your private resources.

  PrivateHostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: internal.company.com
      VPCs:
        - VPCId: !Ref VPC
          VPCRegion: !Ref AWS::Region
      HostedZoneConfig:
        Comment: Private zone for internal services

  DatabaseRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref PrivateHostedZone
      Name: postgres.internal.company.com
      Type: CNAME
      TTL: 60
      ResourceRecords:
        - !GetAtt Database.Endpoint.Address

  CacheRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref PrivateHostedZone
      Name: redis.internal.company.com
      Type: CNAME
      TTL: 60
      ResourceRecords:
        - !GetAtt RedisCluster.ConfigurationEndPoint.Address
Enter fullscreen mode Exit fullscreen mode

Instead of hardcoding RDS endpoint URLs in your application config, which change when you restore from a snapshot or replace the database, use a CNAME like postgres.internal.company.com. When the RDS endpoint changes, update the DNS record. The application does not change.

Route 53 Resolver - Hybrid DNS

For hybrid environments, DNS resolution crosses the on-premises/cloud boundary. On-premises systems need to resolve AWS private hostnames. AWS resources need to resolve on-premises hostnames.

Inbound Resolver Endpoint - On-premises DNS servers forward queries for your AWS domain to this endpoint. Queries enter the VPC and are resolved by the VPC resolver.

Outbound Resolver Endpoint - AWS queries for on-premises domains are forwarded to your on-premises DNS servers through this endpoint.

  InboundResolverEndpoint:
    Type: AWS::Route53Resolver::ResolverEndpoint
    Properties:
      Direction: INBOUND
      IpAddresses:
        - SubnetId: !Ref PrivateAppSubnetA
          Ip: 10.0.10.10    # Fixed IP for on-premises to target
        - SubnetId: !Ref PrivateAppSubnetB
          Ip: 10.0.12.10
      SecurityGroupIds:
        - !Ref ResolverSecurityGroup
      Name: inbound-resolver

  OutboundResolverEndpoint:
    Type: AWS::Route53Resolver::ResolverEndpoint
    Properties:
      Direction: OUTBOUND
      IpAddresses:
        - SubnetId: !Ref PrivateAppSubnetA
        - SubnetId: !Ref PrivateAppSubnetB
      SecurityGroupIds:
        - !Ref ResolverSecurityGroup
      Name: outbound-resolver

  ForwardRule:
    Type: AWS::Route53Resolver::ResolverRule
    Properties:
      DomainName: internal.company.com    # Forward this domain to on-premises
      RuleType: FORWARD
      ResolverEndpointId: !Ref OutboundResolverEndpoint
      TargetIps:
        - Ip: 10.100.1.10               # On-premises DNS server
          Port: 53
        - Ip: 10.100.1.11
          Port: 53
      Name: forward-to-onprem
Enter fullscreen mode Exit fullscreen mode

PrivateLink - Exposing Services Privately Across Accounts

The Problem

You have a service in VPC A that another team's VPC B needs to consume. VPC Peering would expose all of VPC A to VPC B. You only want to expose one specific service endpoint, not the entire network.

What PrivateLink Is

AWS PrivateLink allows you to expose a service in your VPC to other VPCs (in the same or different accounts) without peering, without public internet, and without exposing your VPC. Consumers connect to an Interface VPC Endpoint in their own VPC. The traffic goes through AWS's network to your service.

Consumer VPC                    Provider VPC
     │                               │
Interface Endpoint  ←PrivateLink→  NLB → Your Service
(10.1.0.15)                     (10.0.0.0/16)
Enter fullscreen mode Exit fullscreen mode

The consumer never sees your VPC's address space. They only see the private IP of the Interface Endpoint in their own VPC.

# Provider side - expose your service via an Endpoint Service
  NetworkLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: network
      Subnets:
        - !Ref PrivateAppSubnetA
        - !Ref PrivateAppSubnetB
      Scheme: internal

  EndpointService:
    Type: AWS::EC2::VPCEndpointService
    Properties:
      NetworkLoadBalancerArns:
        - !Ref NetworkLoadBalancer
      AcceptanceRequired: true    # You must approve each connection request
      Tags:
        - Key: Name
          Value: my-service-endpoint

# Consumer side - in the other account
  ServiceEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref ConsumerVPC
      ServiceName: com.amazonaws.vpce.us-east-1.vpce-svc-abc123  # Provider's endpoint service name
      VpcEndpointType: Interface
      SubnetIds:
        - !Ref ConsumerSubnetA
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      PrivateDnsEnabled: true
Enter fullscreen mode Exit fullscreen mode

PrivateLink is also how AWS's own managed services (S3 via Interface Endpoint, Secrets Manager, etc.) work under the hood. Every AWS Interface VPC Endpoint uses PrivateLink.


RAM - Resource Access Manager for Cross-Account Sharing

What It Is

AWS Resource Access Manager (RAM) allows you to share AWS resources across accounts within your AWS Organisation without duplicating them. Instead of each account creating its own Transit Gateway, subnets, or Route 53 Resolver rules, one account creates them and shares via RAM.

What Can Be Shared

  • Transit Gateways (most common)
  • Subnets (in a shared VPC model)
  • Route 53 Resolver rules
  • Prefix Lists
  • License Manager configurations
  • Many other resource types

The Shared VPC Model

Instead of each account having its own VPC, a central networking account creates the VPC with all subnets, route tables, NAT Gateways, and VPC Endpoints. Subnets are shared to workload accounts via RAM. Resources in workload accounts launch into the shared subnets.

# In the networking account - share subnets to workload accounts
  SubnetShare:
    Type: AWS::RAM::ResourceShare
    Properties:
      Name: production-subnet-share
      ResourceArns:
        - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateAppSubnetA}"
        - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateAppSubnetB}"
        - !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${PrivateAppSubnetC}"
      Principals:
        - "123456789012"    # Workload account A
        - "234567890123"    # Workload account B
      AllowExternalPrincipals: false
      Tags:
        - Key: Name
          Value: shared-subnets
Enter fullscreen mode Exit fullscreen mode

Workload accounts can launch EC2 instances, ECS tasks, and RDS databases into the shared subnets. They do not manage the network, the networking account does. This centralises network governance while enabling workload account autonomy.


Production VPC Design - The Reference Architecture

The Complete Architecture

Putting everything together, here is the reference architecture for a production-grade, multi-account, multi-AZ AWS network:

┌─────────────────────────────────────────────────────────────────────┐
│                    AWS Organisation                                 │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │               Networking Account                             │   │
│  │                                                              │   │
│  │  Transit Gateway (shared via RAM to all accounts)            │   │
│  │       │          │          │          │                     │   │
│  │  Direct Connect  VPN    Inspection   Central                 │   │
│  │  Gateway         GW     VPC (FW)     Egress VPC              │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                    │          │          │                          │
│  ┌─────────────────┐  ┌──────────┐  ┌──────────┐                    │
│  │  Production     │  │ Staging  │  │  Dev     │                    │
│  │  Account        │  │ Account  │  │  Account │                    │
│  │                 │  │          │  │          │                    │
│  │  VPC 10.0.0.0/16│  │ 10.2.0.0 │  │ 10.3.0.0 │                    │
│  │  ┌───────────┐  │  │   /16    │  │   /16    │                    │
│  │  │Public /24 │  │  └──────────┘  └──────────┘                    │
│  │  │ALB,NAT GW │  │                                                │
│  │  ├───────────┤  │  ┌──────────────────────────────────────────┐  │
│  │  │Private /23│  │  │           Shared Services Account        │  │
│  │  │App Servers│  │  │                                          │  │
│  │  ├───────────┤  │  │  VPC 10.4.0.0/16                         │  │
│  │  │Private /24│  │  │  Route 53 (Private Hosted Zones)         │  │
│  │  │Databases  │  │  │  VPC Endpoints (SSM, Secrets, ECR)       │  │
│  │  └───────────┘  │  │  Centralised Logging                     │  │
│  └─────────────────┘  └──────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Design Decisions Explained

Centralised Transit Gateway in the Networking Account - One TGW, shared to all accounts. All inter-VPC routing is controlled in one place. The networking team owns the network. Application teams own their workloads.

Three-tier subnet design in every VPC - Public (load balancers, NAT Gateways), Private App (compute), Private Data (databases). Each tier has distinct route tables and Security Groups. Databases are never in subnets with internet access.

One NAT Gateway per AZ - AZ resilience. If AZ-a fails, AZ-b and AZ-c have their own NAT Gateways.

VPC Endpoints for AWS services - S3 and DynamoDB via Gateway Endpoints (free). SSM, Secrets Manager, ECR, CloudWatch, STS via Interface Endpoints. Private subnets never need internet access to reach AWS services.

Inspection VPC for egress control - All internet-bound traffic from private subnets routes through a centralised inspection VPC with AWS Network Firewall or a third-party appliance. Centralised egress filtering, logging, and threat detection.

Shared Services VPC for common infrastructure - Route 53 Resolver endpoints, centralised logging, shared tooling. Every other VPC connects to this one through TGW.

Private Hosted Zones for all internal DNS - No hardcoded endpoints. Database connection strings use DNS names. When the underlying resource changes, update DNS. Applications do not change.

Flow Logs everywhere - VPC-level flow logs to CloudWatch Logs. 90-day retention. Athena queries for security investigations.

No SSH, no bastion hosts - SSM Session Manager provides secure shell access to private EC2 instances with full audit logging, no SSH keys, no open port 22. The SSM endpoint (Interface VPC Endpoint) provides the connectivity.

What Would Happen Without This Design

A flat VPC with one subnet where all resources share the same route table and security group. Every EC2 instance can reach every other EC2 instance. A compromised web server can connect to the database, the cache, and every other internal service. There is no network segmentation. A single breach has unrestricted lateral movement across the entire environment. Compliance audits fail. Security certifications are impossible.

Network architecture is not overhead. It is the foundation of every security control above it.

Written by Onyedikachi Obidiegwu | Cloud Security Engineer

Top comments (0)