Designed and deployed a custom AWS Virtual Private Cloud with public and private subnets across two availability zones, an Internet Gateway, route tables, and a hardened bastion host as the sole SSH entry point into the private network.
// 01
Traffic from the public internet enters via the Internet Gateway, hits the public subnet's route table, and reaches the bastion host. The private subnet has no route to the IGW — the only way in is SSH through the bastion.
// 02
Create the VPC
Created a custom VPC with CIDR block 10.0.0.0/16 — giving 65,536 IP addresses to carve up across subnets. DNS hostnames and DNS resolution were both enabled so EC2 instances receive resolvable hostnames automatically.
Create public and private subnets
Created two subnets in separate availability zones for realistic multi-AZ awareness. The public subnet (10.0.1.0/24) hosts the bastion. The private subnet (10.0.2.0/24) hosts any resources that must not be directly internet-accessible.
Attach an Internet Gateway
Created an Internet Gateway and attached it to the VPC. Without this, no traffic can leave or enter from the public internet — even a public subnet's EC2 instance would be unreachable.
Configure route tables
Created a custom route table for the public subnet with a 0.0.0.0/0 → IGW route, then associated it with the public subnet. The private subnet uses the VPC's default route table which only has the local route — no path to the internet.
Configure Security Groups
Created two security groups. The bastion SG allows inbound SSH (port 22) only from my specific IP. The private instance SG allows inbound SSH only from the bastion's security group ID — not a CIDR, an SG reference — so even if the bastion's IP changes, the rule remains correct.
Launch EC2 instances
Launched the bastion host in the public subnet with a public IP assigned, Amazon Linux 2, and the bastion SG. Launched the private instance in the private subnet with no public IP — only the bastion can reach it.
SSH through the bastion
Used SSH agent forwarding to hop from the local machine into the bastion, then from the bastion into the private instance — without copying the private key onto the bastion. This is the correct, secure pattern.
// 03
VPC Design & CIDR Planning
A VPC is your own isolated slice of the AWS network. Choosing a CIDR like /16 at the VPC level gives you flexibility to carve out many subnets without running out of IPs — important to get right before deploying anything.
Internet Gateway vs NAT Gateway
An IGW enables bi-directional internet access for public subnets. A NAT Gateway allows private subnet instances to initiate outbound internet traffic (e.g. for updates) without exposing them to inbound connections. This project uses an IGW; adding a NAT Gateway is the next step.
SAA-C03: Design Resilient ArchitecturesRoute Tables
Every subnet is associated with exactly one route table. The public subnet's table routes 0.0.0.0/0 to the IGW. The private subnet's table has only a local route — traffic stays inside the VPC. This is what makes a subnet "public" or "private."
Security Groups vs NACLs
Security Groups are stateful, instance-level firewalls — return traffic is automatically allowed. NACLs are stateless, subnet-level — you must explicitly allow both inbound and outbound rules. Security Groups are the primary defense; NACLs add a second layer at the subnet boundary.
SAA-C03: Design Secure ArchitecturesBastion Host Pattern
A bastion (jump host) is the single hardened entry point into a private network. It sits in the public subnet with a restricted SG (SSH from known IPs only). Private instances only allow SSH from the bastion's SG — never from the open internet. This dramatically shrinks your attack surface.
SAA-C03: Design Secure ArchitecturesAvailability Zones
Subnets live in a single AZ. Spreading resources across AZs (e.g. public in us-east-1a, private in us-east-1b) is the foundation of high-availability AWS architectures. If one AZ goes down, workloads in the other AZ continue running.
SAA-C03: Design Resilient ArchitecturesSSH Agent Forwarding
Instead of copying the private key to the bastion (a security anti-pattern), SSH agent forwarding (ssh -A) lets the local SSH agent authenticate on behalf of the client when the bastion connects to private instances. The key never leaves the local machine.
Principle of Least Privilege
Each security group rule is as narrow as possible: the bastion SG allows port 22 only from my IP (/32), and the private SG references the bastion SG as the source rather than a CIDR. No resource has more access than it needs.
Route tables, not the IGW, are what define public vs private. Attaching an IGW to a VPC doesn't automatically make subnets public — you have to explicitly route 0.0.0.0/0 through it in that subnet's route table. Spent time debugging this before it clicked.
Security Group chaining is cleaner than CIDR rules for internal traffic. Referencing one SG as the source in another SG's rule is more robust and idiomatic than using IP ranges — it survives instance replacements and IP changes.
NACLs require explicit outbound rules. My first attempt blocked all traffic because I forgot NACLs are stateless — return traffic from an inbound connection is not automatically allowed. Had to add outbound ephemeral port rules (1024–65535) explicitly.
This is the foundation for almost every real AWS architecture. Load balancers, RDS, ECS clusters — they all sit inside a VPC using this exact public/private split. Understanding this at the network layer makes every higher-level service make more sense.