DEV Community

Cover image for Building a Secure VPC on AWS: Public & Private Subnets with Bastion Host and NAT Gateway
Javier Seng
Javier Seng

Posted on

1 1 1

Building a Secure VPC on AWS: Public & Private Subnets with Bastion Host and NAT Gateway

Overview

This project simulates a production-ready AWS environment with strong security boundaries, using a custom Virtual Private Cloud (VPC), public/private subnet separation, a bastion host for controlled admin access, and a NAT gateway for outbound-only internet access from private instances.

In real-world infrastructure, it’s common to isolate frontend and backend components at the networking layer. Direct access to critical systems (like databases or internal APIs) is tightly restricted, and secure channels (like bastion hosts or session managers) are used for administrative access. This architecture helps organizations enforce least privilege, reduce their attack surface, and follow zero-trust design principles.

Business Value

This architecture lays the foundation for secure, enterprise-grade infrastructure in the cloud. By enforcing strict network boundaries between public and private resources, leveraging a bastion host for administrative access, and using a NAT Gateway to support outbound communication without exposing backend systems, organizations can meet both operational and compliance requirements. It minimizes exposure to the internet, isolates workloads by function, and aligns with cloud security best practices — making it suitable for any workload involving sensitive data, regulated environments, or production backend systems.

What I Built:

  • Creating a custom VPC (10.0.0.0/16)

Segmenting it into:

  • 1. 2 Public Subnets (for public-facing resources like bastion hosts)
    1. 2 Private Subnets (for sensitive backend servers)
  • Launching a bastion host in a public subnet for secure administrative SSH access

  • Deploying a private EC2 instance that has no direct internet exposure

  • Setting up a NAT Gateway so the private EC2 can still reach the internet for package updates

  • Configuring route tables to enforce proper traffic flow and isolation

Part 1: Creating the VPC and Subnet Architecture

To begin, I created a custom VPC called SecureVPC with a CIDR block of 10.0.0.0/16, which gives us 65,536 IP addresses — more than enough for segregated subnets across multiple availability zones.

Image description

I then divided the network into four /24 subnets (256 IPs each):

  • Two public subnets for internet-facing resources (e.g., the bastion host)
  • Two private subnets for sensitive resources (e.g., backend EC2s, databases)

Subnetting this way helps segment environments by function and prepares us for things like high availability, fault tolerance, and security zoning.

Image description

Step 2: Creating and Attaching the Internet Gateway

To allow outbound traffic from the public subnets, I created an Internet Gateway and attached it to the VPC. This is required for any resource (like a bastion host) to have direct internet access.

This gateway will only be referenced in the route table for public subnets — not private ones.

Image description

Step 3: Making Subnets Truly Public (Auto-Assign IP)
Even if a subnet routes to an Internet Gateway, instances launched inside it won’t be reachable unless they have public IP addresses. So I enabled auto-assign public IPv4 for the public subnets. This ensures EC2s launched there (e.g., bastion host) get a public IP automatically.

Image description

Step 4: Deploying a Bastion Host for Secure Admin Access
Next, I launched an EC2 instance (Amazon Linux 2023) in PublicSubnet1. This bastion host acts as a jump server, allowing me to SSH into private EC2 instances securely.

Security group settings were crucial here:

Inbound Rule: SSH (port 22) allowed only from my public IP

Outbound Rule: Open to all (default)

This setup follows the principle of least privilege and ensures that only authorized admins can reach the private backend network.

Image description

Step 5: SSH Access Test to Bastion
I tested SSH access from my terminal using the PEM key and confirmed that access was restricted correctly.

ssh -i ~/Desktop/AWS_projects/aws-webserver-key.pem ec2-user@<bastion-ip>

Image description

Step 6: Launching a Private EC2 Instance
I then launched a private EC2 instance in PrivateSubnet1. It had no public IP, which means it is not accessible from the internet under any circumstances.

Security group:

Inbound Rule: SSH (port 22) allowed only from the bastion subnet CIDR

Outbound Rule: Open (to allow NAT access)

This is a textbook example of private-by-default security, and it can be applied to database servers, internal APIs, and sensitive workloads.

Image description

Step 7: Configuring the NAT Gateway
To let the private EC2 instance reach the internet (for package updates, outbound requests), I set up a NAT Gateway in PublicSubnet1.

The NAT Gateway uses an Elastic IP and allows outbound internet traffic only — meaning private instances can access the internet, but no one from outside can initiate connections back in.

This is the key to controlled outbound access in private zones.

Image description

Step 8: Updating Private Route Table
I created a new PrivateRouteTable and associated it with PrivateSubnet1. I added the following routes:

10.0.0.0/16 → local (VPC internal communication)

0.0.0.0/0 → NAT Gateway

This ensures private instances route external requests through NAT, and internal traffic stays within the VPC.

Image description

Step 9: Final Internet Access Test
From my Mac:

SSH’d into the bastion ✅
From the bastion:

SSH’d into the private EC2 ✅
From private EC2:

Ran curl ifconfig.me and sudo yum update -y ✅

Image description

This proved that:

  • The EC2 in the private subnet had outbound internet
  • It remained unreachable from the outside world

Key Takeaways

  • Public subnets use an Internet Gateway; private subnets use a NAT Gateway
  • Bastion hosts should always restrict SSH to a trusted admin IP
  • Route tables are the glue that define how traffic flows

This architecture prevents direct public exposure of sensitive resources while preserving needed functionality.

Warp.dev image

Warp is the #1 coding agent.

Warp outperforms every other coding agent on the market, and gives you full control over which model you use. Get started now for free, or upgrade and unlock 2.5x AI credits on Warp's paid plans.

Download Warp

Top comments (0)

Build gen AI apps that run anywhere with MongoDB Atlas

Build gen AI apps that run anywhere with MongoDB Atlas

MongoDB Atlas bundles vector search and a flexible document model so developers can build, scale, and run gen AI apps without juggling multiple databases. From LLM to semantic search, Atlas streamlines AI architecture. Start free today.

Start Free

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay