AWS NAT Instance Guide for Multiple Private EC2 Servers

This guide shows how to set up one EC2 instance as a NAT (Network Address Translation) instance so that several other EC2 instances without public IPv4 addresses can still reach the external internet for package installs, OS updates, Docker image pulls, API calls, and Cloudflare Tunnel outbound connections.

It is written for the common pattern:

Important: AWS still recommends NAT Gateway for better availability, bandwidth, and lower admin effort. A NAT instance is appropriate when you want lower fixed cost and you accept the operational work and single-instance risk.


Quick definitions: NAT and VPC

Before going deeper, it helps to define the two main terms used throughout this guide.

What is a VPC?

A VPC (Virtual Private Cloud) is your private network inside AWS.

Think of it as the top-level network boundary that contains:

In other words, the VPC is the network container, and the subnets are smaller network segments inside it.

What is a subnet?

A subnet is a smaller IP network range inside a VPC.

You use subnets to divide the VPC into separate network segments so you can place resources in different areas with different routing behavior.

In AWS, the common distinction is:

In this guide, the NAT instance lives in a public subnet, and the application servers live in private subnets.

What is NAT?

NAT (Network Address Translation) is a networking technique that rewrites IP addresses in packets as traffic passes through a device.

In this guide, the NAT instance does this for your private EC2 servers:

This lets private servers reach the internet for outbound connections without giving each server its own public IPv4 address.

How they relate in AWS

In this design:


1. What problem this solves

When an EC2 instance has no public IPv4 address, it normally cannot reach the internet through an Internet Gateway by itself. To give it outbound internet access, you need a NAT device.

A NAT instance:

AWS documents this behavior for NAT devices and NAT instances.


2. Target architecture

Basic single-AZ layout

flowchart LR
    Internet[(Internet)] --> IGW[Internet Gateway]
    IGW --> NAT[NAT Instance\nPublic Subnet\nPublic IPv4/EIP]
    NAT --> RT[Private Route Table\n0.0.0.0/0 -> NAT Instance]
    RT --> App1[Private EC2 A\nNo Public IP]
    RT --> App2[Private EC2 B\nNo Public IP]
    RT --> App3[Private EC2 C\nNo Public IP]

Packet flow

sequenceDiagram
    participant App as Private EC2
    participant NAT as NAT Instance
    participant IGW as Internet Gateway
    participant Site as External Site

    App->>NAT: Packet to 1.2.3.4:443
    NAT->>IGW: Source NAT to NAT public IPv4
    IGW->>Site: Forwarded packet
    Site-->>IGW: Response to NAT public IPv4
    IGW-->>NAT: Return traffic
    NAT-->>App: De-NAT and forward to private EC2

Production caution: one NAT for many servers is simple but fragile

flowchart TB
    subgraph VPC
      subgraph PublicSubnet[Public Subnet]
        NAT1[NAT Instance]
      end
      subgraph PrivateSubnets[Private Subnets]
        A[App Server 1]
        B[App Server 2]
        C[App Server 3]
        D[App Server 4]
      end
    end

    A --> NAT1
    B --> NAT1
    C --> NAT1
    D --> NAT1
    NAT1 --> Internet[(Internet)]

    note1[Single point of failure\nIf NAT1 stops, all private servers lose egress]

3. When this design is a good fit

Use a NAT instance when:

Prefer NAT Gateway when:


4. Prerequisites

Before you start, make sure you have:

0.0.0.0/0 -> igw-xxxxxxxx

5. Important AWS constraints

According to AWS documentation:

  1. A NAT instance must be in a public subnet.
  2. It must have a public IP address or an Elastic IP.
  3. You must disable source/destination check on the NAT instance.
  4. The private subnet route table must send 0.0.0.0/0 to the NAT instance.
  5. AWS notes that the old prebuilt NAT AMI is based on an old Amazon Linux release, so if you use NAT instances, you should create your own from a current OS such as Amazon Linux 2023 or Amazon Linux 2.

6. Network design checklist

Public subnet route table

Your NAT instance’s subnet must have a route like:

Destination    Target
10.0.0.0/16    local
0.0.0.0/0      igw-xxxxxxxx

Private subnet route table

Every private subnet that should use this NAT instance needs a route like:

Destination    Target
10.0.0.0/16    local
0.0.0.0/0      i-xxxxxxxxxxxxxxxxx

Where the target is the instance ID of the NAT instance.

If you have multiple private subnets, you can associate them with the same private route table, or create separate route tables that all point to the same NAT instance.

How to check which route table an EC2 instance is using

In AWS, a route table is associated with a subnet, not directly with an EC2 instance.

That means the correct way to inspect routing for an instance is:

  1. find the instance’s subnet ID
  2. find the route table associated with that subnet
  3. inspect the 0.0.0.0/0 route target

Check in the AWS Console

  1. Open the EC2 console.
  2. Select the instance you want to inspect.
  3. In the instance details, open the Networking tab and note the Subnet ID and VPC ID.
  4. Click the Subnet ID link to open that subnet in the VPC console.
  5. In the subnet details, open the associated Route table.
  6. Review the Routes section.

Interpret the route table like this:

A subnet is considered public or private based on its routing, not just its name. An instance in a public subnet still needs a public IPv4 or Elastic IP if you want direct internet connectivity.

Check with the AWS CLI

First, get the instance’s subnet and VPC:

aws ec2 describe-instances \
  --instance-ids i-0123456789abcdef0 \
  --query 'Reservations[0].Instances[0].{InstanceId:InstanceId,SubnetId:SubnetId,VpcId:VpcId,PrivateIp:PrivateIpAddress,PublicIp:PublicIpAddress}' \
  --output table

Then check whether that subnet has an explicitly associated route table:

aws ec2 describe-route-tables \
  --filters Name=association.subnet-id,Values=subnet-0123456789abcdef0 \
  --query 'RouteTables[].{RouteTableId:RouteTableId,Routes:Routes[*].{Destination:DestinationCidrBlock,GatewayId:GatewayId,InstanceId:InstanceId,NatGatewayId:NatGatewayId,NetworkInterfaceId:NetworkInterfaceId}}' \
  --output json

If this returns no route table, the subnet is usually using the VPC’s main route table. In that case, list the route tables in the VPC and find the one with "Main": true:

aws ec2 describe-route-tables \
  --filters Name=vpc-id,Values=vpc-0123456789abcdef0 \
  --query 'RouteTables[].{RouteTableId:RouteTableId,Associations:Associations[*].{SubnetId:SubnetId,Main:Main},Routes:Routes[*].{Destination:DestinationCidrBlock,GatewayId:GatewayId,InstanceId:InstanceId,NatGatewayId:NatGatewayId,NetworkInterfaceId:NetworkInterfaceId}}' \
  --output json

Fast rule for public vs private

Use this quick check when reading route tables:

For the architecture in this guide:


7. Security group design

AWS’s NAT-instance walkthrough recommends allowing HTTP and HTTPS from the private subnet CIDR to the NAT instance, plus SSH from your admin network if you need shell access.

Example NAT instance security group

Inbound

Source Protocol Ports Purpose
10.0.10.0/24 TCP 80 Outbound web traffic from private subnet via NAT
10.0.10.0/24 TCP 443 Outbound HTTPS traffic from private subnet via NAT
your-admin-ip/32 TCP 22 Optional admin SSH

Outbound

Destination Protocol Ports Purpose
0.0.0.0/0 TCP 80 Internet egress
0.0.0.0/0 TCP 443 Internet egress
0.0.0.0/0 UDP/TCP DNS if needed Optional if using direct DNS egress

Practical note

If your private servers need more than just web access, you may broaden rules carefully. Examples:

Keep the security group as narrow as your workloads allow.


The most reliable approach is:

  1. Launch a temporary EC2 instance in the public subnet
  2. Configure it for NAT
  3. Create an AMI from it
  4. Launch the final NAT instance from that AMI
  5. Disable source/destination check
  6. Point private subnet route tables to it

This mirrors AWS’s current NAT-instance process.


9. Step-by-step setup

Step 1: Launch the NAT builder instance

Launch an EC2 instance with:

You can later create an AMI from this configured instance and relaunch it as your long-lived NAT instance.


Step 2: Install NAT components and enable forwarding

On the instance, configure iptables and IPv4 forwarding.

Commands based on AWS’s current NAT-instance procedure

sudo yum install iptables-services -y
sudo systemctl enable iptables
sudo systemctl start iptables

Create a sysctl file:

sudo tee /etc/sysctl.d/custom-ip-forwarding.conf > /dev/null <<'EOF'
net.ipv4.ip_forward=1
EOF

Apply it:

sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf

Find the primary interface name:

netstat -i

Common names are eth0, ens5, or enX0.

Now configure NAT. Replace eth0 below if your primary interface has a different name.

sudo /sbin/iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
sudo /sbin/iptables -F FORWARD
sudo service iptables save

In practice, you usually want to explicitly allow forwarding instead of only flushing the chain.

sudo iptables -A FORWARD -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -o eth0 -j ACCEPT
sudo service iptables save

If you already manage host firewall policy in another way, adapt these rules carefully.


In the EC2 console:

This gives you a reusable NAT image so you can relaunch quickly if the instance is replaced.


Step 4: Launch the final NAT instance

Launch from the configured AMI with:

Mermaid view:

flowchart LR
    Build[Temporary Builder EC2] --> Config[Install iptables\nEnable IP forwarding\nConfigure masquerade]
    Config --> AMI[Create AMI]
    AMI --> NAT[Launch Final NAT Instance]
    NAT --> Route[Point private subnet routes to NAT]

Step 5: Disable source/destination check

This is mandatory.

In the EC2 console:

Without this, the NAT instance will drop transit traffic because EC2 instances normally only accept traffic where they are the source or destination.


Step 6: Update private subnet route tables

For each private subnet that should use this NAT instance:

0.0.0.0/0 -> i-xxxxxxxxxxxxxxxxx

If several private subnets should share the same NAT instance, make sure each private subnet is associated with a route table that points default traffic to that NAT instance.

Example with three private subnets

flowchart TB
    subgraph Public
      NAT[NAT Instance]
    end

    subgraph PrivateA[Private Subnet A]
      A1[Server A1]
      A2[Server A2]
    end

    subgraph PrivateB[Private Subnet B]
      B1[Server B1]
    end

    subgraph PrivateC[Private Subnet C]
      C1[Server C1]
    end

    RTA[Route Table A\n0.0.0.0/0 -> NAT]
    RTB[Route Table B\n0.0.0.0/0 -> NAT]
    RTC[Route Table C\n0.0.0.0/0 -> NAT]

    A1 --> RTA
    A2 --> RTA
    B1 --> RTB
    C1 --> RTC
    RTA --> NAT
    RTB --> NAT
    RTC --> NAT
    NAT --> Internet[(Internet)]

Step 7: Configure the private servers

On the private EC2 instances:

To test:

curl -I https://aws.amazon.com
curl -I https://google.com
ping -c 3 8.8.8.8
getent hosts github.com

For your earlier Cloudflare Tunnel case, this is a good test too:

curl -I https://region1.v2.argotunnel.com

If outbound HTTPS works, cloudflared should be able to establish its tunnel unless another firewall or DNS issue exists.


10. Optional: bootstrap the NAT instance with user data

Instead of configuring by hand, you can launch the NAT instance with user data.

Review and adapt interface names before using this in production.

#!/bin/bash
set -euxo pipefail

yum install -y iptables-services net-tools
systemctl enable iptables
systemctl start iptables

cat >/etc/sysctl.d/custom-ip-forwarding.conf <<'EOF'
net.ipv4.ip_forward=1
EOF
sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf

IFACE=$(ip route show default | awk '/default/ {print $5; exit}')

iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE
iptables -A FORWARD -i "$IFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -o "$IFACE" -j ACCEPT
service iptables save

11. Validation checklist

Use this after setup.

On the NAT instance

ip addr
ip route
sysctl net.ipv4.ip_forward
sudo iptables -t nat -S
sudo iptables -S FORWARD
curl -I https://aws.amazon.com

Expected:

On each private server

ip addr
ip route
curl -I https://aws.amazon.com
curl -I https://registry-1.docker.io

Expected:


12. Troubleshooting

Symptom: private servers still cannot reach the internet

Check these in order:

A. NAT instance has no public egress

From the NAT instance:

curl -I https://aws.amazon.com

If this fails:

0.0.0.0/0 -> igw-xxxxxxxx

B. Source/destination check is still enabled

This is one of the most common issues.

Verify it is disabled on the NAT instance in the EC2 console.

C. Private subnet route table is wrong

Make sure the private subnet route table uses the NAT instance as target:

0.0.0.0/0 -> i-xxxxxxxxxxxxxxxxx

Not:

D. Security groups are too restrictive

E. Host firewall rules are incomplete

Verify:

sudo iptables -t nat -S
sudo iptables -S FORWARD

You need at minimum:

F. Wrong interface name in iptables

If you hardcoded eth0 but the primary interface is ens5, NAT will not work.

Check with:

ip route show default
netstat -i

G. DNS works poorly or not at all

If curl https://1.1.1.1 works but curl https://aws.amazon.com fails, you likely have a DNS problem.

Check:

cat /etc/resolv.conf
getent hosts aws.amazon.com

13. High-availability and scaling considerations

One NAT instance serving many servers is usually fine for low-cost environments, but keep these caveats in mind:

Single point of failure

If the NAT instance stops, crashes, or is terminated, all private servers lose outbound internet access.

Cross-AZ traffic

If private subnets in multiple Availability Zones all route through one NAT instance in a single AZ:

Better pattern per AZ

A stronger design is one NAT device per AZ, with each private subnet using the NAT instance in the same AZ.

flowchart LR
    subgraph AZ1
      NAT1[NAT Instance AZ1]
      P1[Private Subnet AZ1 Servers]
      P1 --> NAT1
    end

    subgraph AZ2
      NAT2[NAT Instance AZ2]
      P2[Private Subnet AZ2 Servers]
      P2 --> NAT2
    end

    NAT1 --> Internet[(Internet)]
    NAT2 --> Internet

Monitoring suggestions

At minimum, monitor:


14. Security and hardening tips

Examples:

These can reduce how much traffic needs to traverse the NAT instance.


15. Cost and tradeoff summary

NAT Instance

Pros:

Cons:

NAT Gateway

Pros:

Cons:

Public IP on every server

Pros:

Cons:


16. Practical recommendation for your use case

If you have a few servers and you want:

then this is a reasonable design:

  1. one small NAT instance in a public subnet
  2. all private app servers route default IPv4 traffic to it
  3. app servers keep no public IPv4
  4. use Cloudflare Tunnel or a load balancer separately for inbound access
  5. add VPC endpoints for AWS-native services where possible

If the environment becomes more important or spans multiple AZs, move toward:


17. Quick implementation summary

flowchart TD
    A[Create/verify IGW] --> B[Create public and private subnets]
    B --> C[Launch NAT instance in public subnet]
    C --> D[Attach public IP or EIP]
    D --> E[Enable IP forwarding and iptables masquerade]
    E --> F[Disable source/destination check]
    F --> G[Point private route tables 0.0.0.0/0 to NAT instance]
    G --> H[Launch app servers without public IP]
    H --> I[Test curl, package installs, Cloudflare Tunnel]

18. References

Official AWS references used to assemble this guide:


19. Final sanity check

For this design to work, all of the following must be true at the same time:

If any one of these is missing, private servers will usually fail to reach the external internet.