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)
- 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.
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.
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.
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.
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.
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>
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.
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.
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.
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 ✅
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.
Top comments (0)