AWS re/Start Lab · Networking

Configuring a VPC with Bastion and NAT Gateway

Built a Virtual Private Cloud from scratch with a public and private subnet, an Internet Gateway, a NAT Gateway, and a bastion server. The goal was to design a network where private instances can reach the internet for updates without being directly exposed, and where administrators connect to them through a controlled jump host.

Lab Summary

A single VPC (10.0.0.0/16) was carved into two subnets with distinct roles. The public side hosts the entry point — an internet-facing bastion. The private side hosts workloads that need outbound internet without being reachable inbound. The plumbing in between is two route tables and two gateways: an Internet Gateway for the public path, and a NAT Gateway for the private path's one-way exit.

Public Side

Subnet 10.0.0.0/24 with auto-assigned public IPs. Its route table sends 0.0.0.0/0 to the Internet Gateway. The bastion server lives here and is the only thing reachable from outside.

Private Side

Subnet 10.0.2.0/23 with no public IPs. Its route table sends 0.0.0.0/0 to the NAT Gateway, which lives in the public subnet. Outbound internet works; inbound from the internet is impossible.

Network Architecture

The full set of resources created in this lab. Note the deliberate CIDR sizing: the private subnet is twice the size of the public one because real workloads usually outnumber bastions and load balancers.

Resource Name / CIDR Role
VPC Lab VPC
10.0.0.0/16
Root network. DNS hostnames enabled so EC2 instances get resolvable internal DNS names.
Public Subnet Public Subnet
10.0.0.0/24 (256 IPs)
Hosts bastion + NAT Gateway. Auto-assign public IPv4 enabled.
Private Subnet Private Subnet
10.0.2.0/23 (512 IPs)
Hosts workload instances. No public IPs. Twice the size of the public subnet.
Internet Gateway Lab IGW Attached to the VPC. Provides bidirectional internet access for the public subnet.
NAT Gateway Lab NAT gateway
+ Elastic IP
Lives in the public subnet. Provides outbound-only internet for the private subnet.
Public Route Table Public Route Table Associated with the public subnet. Default route → IGW.
Private Route Table Private Route Table Associated with the private subnet. Default route → NAT Gateway.
Bastion Server Bastion Server
t3.micro · Amazon Linux 2023
EC2 instance in the public subnet. SSH entry point for the entire VPC.

Why is the private subnet /23 and not /24?

A /23 gives 512 addresses vs /24's 256. In real deployments, the private subnet usually holds far more compute (app servers, databases, queues) than the public one (which only needs bastions, NAT, and load balancers). Sizing reflects that asymmetry.

Why does the public subnet need "auto-assign public IPv4"?

An IGW only routes traffic for resources that actually have a public IP. Without auto-assign enabled (or a manually attached Elastic IP), instances launched in the subnet would be unreachable from the internet even with a correct route table.

Route Tables: The Heart of the Routing

The single most important difference between a "public" and a "private" subnet is the route table it's associated with. Same VPC, same kind of subnet — the route table is what changes the behavior.

Public Route Table

Internet-facing

10.0.0.0/16 → local
0.0.0.0/0 → Lab IGW

Default route points to the Internet Gateway. Combined with instances having public IPs, this enables bidirectional internet traffic — inbound (SSH to bastion) and outbound (package downloads, etc).

Private Route Table

Outbound-only via NAT

10.0.0.0/16 → local
0.0.0.0/0 → Lab NAT gateway

Default route points to the NAT Gateway in the public subnet. The NAT Gateway then forwards traffic out through the IGW. Return traffic finds its way back via NAT state tables. No inbound paths from the internet exist.

Reminder: The local route in every VPC route table is implicit and cannot be deleted. It guarantees that any two subnets inside the same VPC can talk to each other regardless of public/private classification.

The Bastion Server (Jump Host)

A bastion is the only instance with a public IP and inbound SSH from the internet. Every administrative connection to a private instance hops through it first — reducing the attack surface to a single hardenable host.

[ Internet ] ─SSH/22──> [ Bastion 52.27.201.115 ] ─SSH/22──> [ Private Instance 10.0.2.x ]

Bastion Configuration

  • AMI: Amazon Linux 2023
  • Type: t3.micro
  • Subnet: Public Subnet (auto-assign public IP)
  • Security Group: Bastion Security Group — inbound SSH (22) from 0.0.0.0/0
  • IMDSv2: Required (instance metadata hardening)

Private Instance Security Group

The private instance accepts SSH only from the VPC CIDR 10.0.0.0/16. This is the lock that makes the bastion mandatory — even if someone learned the private IP, they couldn't reach it from the internet because the security group rejects any source outside the VPC.

Why a bastion instead of opening SSH on every instance? One hardened host with a public IP is dramatically easier to monitor and patch than dozens of private workloads each exposed to the internet. Logs, MFA, and session recording all collapse onto one box.

NAT Gateway: Controlled Outbound

Private instances often need to reach the internet — to pull OS updates, fetch container images, call third-party APIs. They must not be reachable from the internet. The NAT Gateway delivers exactly that asymmetry.

[ Private Instance 10.0.2.x ] ─> Private RT 0.0.0.0/0 ─> [ NAT GW + EIP ] ─> [ IGW ] ─> Internet

Why does the NAT Gateway need an Elastic IP?

The NAT Gateway translates private source IPs to a single public IP visible on the internet. That public IP must be stable (so firewalls and allow-lists on remote services keep working), which is exactly what an Elastic IP provides — a static, account-owned public IPv4.

IGW vs NAT GW — what's the actual difference?

IGW: bidirectional, free, routes packets when source/destination has a public IP.
NAT GW: outbound-only (from the VPC's perspective), billed per hour + per GB, translates private IPs to its EIP. NAT is built on top of an IGW — it always needs one.

Cost note: Unlike an Internet Gateway, the NAT Gateway is a managed, billable resource (hourly charge + data processing). In production accounts, an unused NAT Gateway running 24/7 is one of the more common surprise charges on a VPC bill.

Optional Challenge — Reference Commands

The official guide ends with an optional challenge: launch a private EC2 instance, SSH into it through the bastion, and prove outbound internet works via the NAT Gateway. The base lab was completed; this section documents the reference commands for that challenge. Not executed in this run, so there is no visual evidence — only the procedure.

ref

User-data script for the private instance

When launching the private instance, this user-data script enables password-based SSH so the connection from the bastion can use a password (vockey isn't pre-installed there). In a real environment you'd push the key with ssh-copy-id through the bastion instead.

#!/bin/bash
echo 'lab-password' | passwd ec2-user --stdin
sed -i 's|[#]*PasswordAuthentication no|PasswordAuthentication yes|g' /etc/ssh/sshd_config
systemctl restart sshd.service
  • Launch Amazon Linux 2023, t3.micro, in the Private Subnet.
  • No auto-assign public IP (and the subnet doesn't grant one anyway).
  • Security Group: Private Instance SG — inbound SSH (22) from 10.0.0.0/16 only.
  • Paste the script above into Advanced details » User data at launch time.
ref

Connect through the bastion and verify NAT

Once both instances are running, use EC2 Instance Connect (or SSH) to reach the bastion, then SSH from there into the private instance using its private IP. From inside, a ping to an internet host proves the NAT Gateway path is working.

# 1. Connect to the bastion (from EC2 console: "Connect" → EC2 Instance Connect)

# 2. From the bastion, jump to the private instance:
ssh ec2-user@10.0.2.X
# (use the password set by user-data: lab-password)

# 3. Verify outbound internet via NAT Gateway:
ping -c 3 amazon.com
  • Successful pings = the Private Route Table is correctly forwarding 0.0.0.0/0 to the NAT Gateway, the NAT GW reached the IGW, and return packets made it back.
  • Failed pings would point at one of: missing NAT route, NAT GW in the wrong subnet, security group blocking egress, or NACL issues.
Why password auth instead of keys? The lab is intentionally simple. In production you copy the bastion's public key (or use SSM Session Manager / AWS Systems Manager) to avoid passwords on Linux entirely.

Key Learnings

What Was Learned

How to build a two-tier VPC manually (public + private subnet) instead of using the wizard.
Public vs Private subnet is defined by the route table, not by any subnet flag.
The role of an Internet Gateway (bidirectional, free) vs a NAT Gateway (outbound-only, billed).
Why a NAT Gateway requires an Elastic IP — a stable public address for outbound traffic.
The bastion pattern: one hardened public host as the SSH entry point for an entire VPC.
CIDR sizing logic — why the private subnet is intentionally larger than the public one.

Technical Conclusion

This lab consolidated the previous networking series (labs 263, 264, 267) into a single real-world topology: a VPC where the public/private boundary is enforced by route tables and gateways, and where administrative access is funneled through a bastion.

The two recurring lessons are worth restating. First, routing is the contract: a subnet is "public" if and only if its route table points 0.0.0.0/0 at an IGW. Second, NAT is one-way by design: it grants the private subnet outbound internet without ever creating an inbound path, which is exactly the asymmetry production workloads need.