01 The Brief
Month one made the account safe. Month two had to make it useful. The brief was simple to state and full of small ways to get it wrong: stand up a real website, on a real domain, served securely, on infrastructure I built and understand end to end.
The tempting shortcut is to launch an instance into the default VPC and click "allow all traffic." I deliberately refused both. If I don't build the network myself, I don't actually understand how a packet gets from a visitor's browser to my web server — and that understanding is the entire point of the project. So the default VPC stayed untouched and I drew my own.
Done meant all of this was true at once:
- A custom VPC with a public subnet, an internet gateway, and routing I wrote myself.
- A single EC2 host running Nginx, reachable at a stable address that survives reboots.
- A domain in Route 53 resolving to that host.
- HTTPS with a valid certificate and a forced redirect from plain HTTP.
- SSH open only to my IP; a scan of the box shows nothing I didn't intend to expose.
02 The Blueprint
The architecture is intentionally minimal — one public subnet, one instance — because the goal is to understand every hop, not to over-engineer. Here's the path a request takes from a visitor to the page and back.
Why these choices
- A custom VPC, not the default. Building the CIDR block, subnet, gateway and route table by hand is the only way to actually learn how traffic is routed — and it's the same muscle every later, bigger network uses.
- An Elastic IP. A normal public IP changes every time the instance stops. Since a DNS A record points at that address, I attached an Elastic IP so the address — and therefore the domain — stays stable across reboots.
- A security group that's almost entirely closed. Only 80 and 443 face the world; SSH (22) is restricted to my own IP. The default posture is "deny," and I open the minimum.
- Let's Encrypt on the box, not ACM. AWS Certificate Manager is lovely, but its certificates only attach to load balancers and CloudFront — not directly to an EC2 instance. For a single self-managed host, Certbot with Let's Encrypt is the correct tool. (ACM behind a load balancer is the scale-up path, noted for later.)
The instance also inherits the least-privilege IAM role I built in Month 01 via an instance profile — so anything the host needs from AWS uses short-lived role credentials, never an access key baked into the box.
03 The Build
Step 1 — Draw the network
I started with an empty 10.0.0.0/16 VPC, carved out a single public
/24 subnet, attached an internet gateway, and — crucially — added a route
sending all outbound traffic through that gateway. A subnet is only "public" because its
route table says so.
# VPC + subnet
VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query Vpc.VpcId --output text)
SUBNET=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.1.0/24 \
--query Subnet.SubnetId --output text)
# Internet gateway + the route that makes the subnet public
IGW=$(aws ec2 create-internet-gateway --query InternetGateway.InternetGatewayId --output text)
aws ec2 attach-internet-gateway --vpc-id $VPC --internet-gateway-id $IGW
RT=$(aws ec2 create-route-table --vpc-id $VPC --query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $RT --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW
aws ec2 associate-route-table --route-table-id $RT --subnet-id $SUBNET
Step 2 — A tight security group
Web ports open to everyone; SSH open only to my current public IP as a /32.
This one decision removes the entire background noise of bots probing port 22.
SG=$(aws ec2 create-security-group --group-name bitwyse-web \
--description "web + restricted ssh" --vpc-id $VPC --query GroupId --output text)
MYIP=$(curl -s https://checkip.amazonaws.com)
aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 22 --cidr ${MYIP}/32
aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $SG --protocol tcp --port 443 --cidr 0.0.0.0/0
Step 3 — Launch and pin the host
I launched an Amazon Linux 2023 instance into the subnet with a key pair and the Month 01 instance profile, then allocated an Elastic IP and associated it so the address never drifts.
ID=$(aws ec2 run-instances --image-id $AL2023_AMI --instance-type t3.micro \
--key-name bitwyse-key --subnet-id $SUBNET --security-group-ids $SG \
--iam-instance-profile Name=bitwyse-ec2-role \
--query Instances[0].InstanceId --output text)
EIP=$(aws ec2 allocate-address --query AllocationId --output text)
aws ec2 associate-address --instance-id $ID --allocation-id $EIP
Step 4 — Nginx and the site
SSH in (key-only — passwords are disabled), install Nginx, drop the site into the web root, and enable the service so it survives reboots.
sudo dnf install -y nginx
sudo systemctl enable --now nginx
# deploy the site and set a sane server_name
sudo cp -r ./site/* /usr/share/nginx/html/
Step 5 — Domain, then the padlock
In Route 53 I created a hosted zone for the domain and an A record pointing at the Elastic IP. Once DNS resolved, I ran Certbot, which proved control of the domain over HTTP, installed a Let's Encrypt certificate straight into the Nginx config, and set up a 301 redirect so every plain-HTTP visitor is bounced to HTTPS. A systemd timer handles renewal automatically.
sudo dnf install -y certbot python3-certbot-nginx
sudo certbot --nginx -d bitwyse.site -d www.bitwyse.site \
--redirect --agree-tos -m hello@bitwyse.site -n
sudo systemctl list-timers | grep certbot # confirm auto-renew is armed
Watching Certbot rewrite my Nginx server block, reload the service, and then loading the domain to a green padlock — with the HTTP version silently redirecting — was the payoff. Every piece from account to packet to certificate was suddenly one working chain.
04 Curveballs
The instance was running and the security group allowed my IP, but the connection
timed out. Cause: the subnet had no route to the internet gateway, so packets
had no way back out. Fix: add the 0.0.0.0/0 → IGW route and associate
the route table with the subnet. A subnet without that route isn't public, no matter
what you named it.
Timeout during connect (likely firewall problem). Let's Encrypt validates by
reaching your server on port 80 — which I'd been about to lock down. Fix:
keep port 80 open in the security group (it's needed both for the challenge and for the
HTTP→HTTPS redirect), and make sure the A record had actually propagated before running
Certbot.
Hitting the Elastic IP directly served the page, but the domain returned nothing. Cause:
a low-TTL A record I'd created earlier was still cached, and Nginx's server_name
didn't include the domain. Fix: correct the A record, wait out the TTL, and set
server_name bitwyse.site www.bitwyse.site; so Nginx answers for both.
05 Proof It Works
"It loads in my browser" isn't evidence — it's an anecdote. I validated the deployment from the outside the way an attacker or an auditor would.
- Redirect + status.
curl -I http://bitwyse.sitereturns a301to the HTTPS URL, andcurl -I https://bitwyse.sitereturns200 OKwith a valid certificate chain. - TLS quality. An SSL Labs scan came back a clean A — modern protocols only, no weak ciphers.
- Attack surface. An
nmapscan from outside my network shows exactly three open ports — 22, 80, 443 — and 22 refuses to even respond unless the scan comes from my IP. - Resilience. Rebooted the instance and confirmed Nginx came back on its own and the Elastic IP kept the domain live — no manual intervention.
The site itself is almost beside the point. What I actually shipped is a mental map of how a request travels from a browser, through DNS and a gateway, into a subnet, past a firewall, and onto a process I configured myself.
06 Takeaways
This build works, and I understand every inch of it — which was the goal. But I finished it very aware of what it isn't: production-grade.
- This host is a pet, not cattle. One instance, hand-configured over SSH. If it dies, so does the site. The next evolution is an Auto Scaling group of interchangeable instances behind a load balancer.
- TLS belongs at the edge. Terminating certificates on a single box is fine for one host; ACM on an Application Load Balancer (or CloudFront) is how you do it at scale — and it removes Certbot from the instance entirely.
- Clicking and typing commands doesn't scale. I built this twice while debugging, by hand, both times. That friction is exactly what makes the case for infrastructure as code.
Which sets up exactly where the logbook goes next: taking everything I did manually here and making it repeatable, reviewable and push-button — automation and infrastructure as code.
Automating the Deploy — retiring manual SSH for a repeatable, push-to-deploy workflow with infrastructure as code. Field notes coming soon.