Free NAT Gateway on AWS

2026-02-23

I recently decided to move my AWS EC2 instances to IPv6-only on AWS. The goal was simple: reduce cost.

It mostly worked. Until I tried to run:

git clone https://github.com/...

And it failed. I also couldn't setup tools like nvm which rely on Github to install (auto-installer).

Turns out, GitHub still requires IPv4, except maybe for Github Pages. My IPv6-only instance had no way to talk to the IPv4 internet, and I really needed to talk to Github from my EC2 instances.

This seems to be a well-known issue, and the default solution is to just assign a public IPv4 address to the EC2 instance, or use a NAT Gateway. But public IPv4 addresses cost money. And NAT Gateways on AWS cost even more money. For small projects or internal tools, that monthly charge adds up fast.

AWS gives you one free IPv4 address to play with, though, and here's how I used it to setup my own NAT Gateway. A cheap (or free) NAT instance that uses the one free public IPv4 AWS gives you, while keeping all my app servers IPv6-only and publicly accessible.

This guide walks through the full setup, but assumes you already know AWS fundamentals like VPC, subnets, EC2 etc.

The Architecture

Here's what we're building:

VPC (Dual Stack)
|
├── Public NAT Subnet
│     └── NAT Instance
│           - Public IPv4 (free)
│           - IPv6
│
└── App Subnet
        └── IPv6-only App Instances
            - Public IPv6
            - Private IPv4
            - No public IPv4

Traffic flow:

  • Inbound traffic hits app servers over IPv6 directly
  • Outbound IPv6 goes straight to the Internet Gateway
  • Outbound IPv4 goes through the NAT instance
  • No NAT Gateway involved

Step 1: Create a Dual Stack VPC

Create a VPC with:

  • IPv4 CIDR, for example 10.0.0.0/16
  • Add an Amazon-provided IPv6 CIDR block

Create an internet gateway if you haven't already, and attach it to the VPC.

Step 2: Create Two Subnets

NAT Subnet

  • IPv4 CIDR 10.0.1.0/24.
  • Enable auto-assign public IPv4, or associate an Elastic IP later.
  • Enable IPv6.

This is where the NAT instance lives.

App Subnet

  • IPv4 CIDR 10.0.2.0/24.
  • Disable auto-assign public IPv4.
  • Enable IPv6 and auto-assign public IPv6.e

Even though the instances we spin up in this subnet would be IPv6-first, they still need a private IPv4 address to route IPv4 traffic to the NAT instance over the local network. They just won't have public IPv4.

Step 3: Route Tables

NAT Subnet Route Table

10.0.0.0/16    → local
VPC_IPv6_CIDR  → local
0.0.0.0/0      → Internet Gateway
::/0           → Internet Gateway

The VPC_IPv6_CIDR placeholder would contain the actual IPv6 CIDR that AWS allocated when you created the VPC. The local routes should already be present by default.

App Subnet Route Table

10.0.0.0/16    → local
VPC_IPv6_CIDR  → local
::/0           → Internet Gateway
0.0.0.0/0      → NAT Instance

The VPC_IPv6_CIDR placeholder would contain the actual IPv6 CIDR that AWS allocated when you created the VPC. The local routes should already be present by default.

That IPv4 default route (0.0.0.0/0) pointing to the NAT instance is what sends the IPv4 traffic from the app instances to the NAT instance.

Step 4: Launch the NAT Instance

I used Ubuntu Server 24.04 LTS, but you can use what you like.

A relatively small instance would do, because we would use this only for outbound IPv4 traffic. My apps typically don't do a lot of outbound traffic unless I'm installing dependencies or similar. t4g.nano works fine for light workloads, but I used a t4g.micro because it was free-tier eligible anyway.

Make sure:

  • It's in the public subnet
  • It gets a public IPv4: should be auto-assigned if you followed the steps, but you can also preferably associate an Elastic IP to this instance.
  • It gets IPv6
  • Source and destination check is disabled

Step 5: Enable IPv4 Forwarding

SSH into the NAT instance, and then:

sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-nat.conf
sudo sysctl --system

Verify:

cat /proc/sys/net/ipv4/ip_forward

It should return 1.

Step 6: Configure nftables for NAT

Ubuntu 24 should have nftables by default, but if it doesn't, then install and enable it:

sudo apt update
sudo apt install nftables -y
sudo systemctl enable nftables

Find your network interface name:

ip a

This is usually something like ens5.

Now edit the nftables config...

sudo vi /etc/nftables.conf

...so that it looks something like this:

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0;
        policy accept;
    }

    chain forward {
        type filter hook forward priority 0;
        policy drop;

        ip saddr 10.0.2.0/24 accept
        ct state established,related accept
    }

    chain output {
        type filter hook output priority 0;
        policy accept;
    }
}

table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100;
        oifname "ens5" masquerade
    }
}

You could also policy drop and then add explicit rules. If you do this, please make sure you don't lock yourself out of the instance (allow SSH from your IP, for example).

Now restart nftables:

sudo systemctl restart nftables

IPv4 forwarding should be working now. To verify that these rules persist, you can reboot the instance:

sudo reboot

Word of caution here: make sure your nftables rules allow you to SSH into the instance. Otherwise, you will be locked out.

Step 7: Security Groups

For the NAT instance:

  • Allow SSH from your IP
  • Allow all outbound

For the app instances:

  • Allow 80 and 443 from ::/0 and 0.0.0.0/0
  • Allow SSH from your IP
  • Allow all outbound

Security groups handle inbound filtering. I kept nftables simple for input to avoid locking myself out.

Step 8: Test Everything

From your IPv6-only app instance:

curl -4 ifconfig.me

If this returns the NAT instance's public IPv4, then you have everything setup correctly, and git clone should work now:

git clone https://github.com/your/repo.git

Inbound over IPv6 works immediately. Just point an AAAA DNS record to your instance's IPv6 address. This IPv6 address that AWS assigns to the instance always survives reboots and stops, so the IPv6 equivalent of an IPv4 Elastic IP is not needed.

Final Thoughts

IPv6-only EC2 instances are totally usable today at little to no extra cost. All I needed was a small IPv4 escape hatch for services that are not fully IPv6-native yet. In the end, I got what I wanted:

  • Public IPv6 app instances
  • Working GitHub access
  • No NAT Gateway bill

I think this should work well for the foreseeable future.


ChatGPT helped write this article.

Pradeep CE

Pradeep CE

I am the author of this blog. I enjoy coding, learning new things and solving problems. This blog is where I share my learnings on running a one-person software business, software development, and life.

Thanks for reading!