A production-ready Linux server setup with Nginx, SSL, and hardened security β built from scratch on AWS EC2.
- Overview
- Requirements
- Architecture
- Step-by-Step Setup
- Endpoints
- Security Checklist
- Verification
- Project Structure
- Author
This project fulfills the HNG Internship 14 β DevOps Track Stage 0 requirements. It involves provisioning a bare Linux server on AWS EC2, configuring Nginx to serve a static homepage and a JSON API endpoint, securing everything with a valid Let's Encrypt SSL certificate, and hardening the server with UFW firewall rules and SSH security best practices.
No Docker. No Compose. No automation tools β just a bare Linux server and raw configuration.
| Requirement | Implementation |
|---|---|
| Linux Server | AWS EC2 β Ubuntu 22.04 LTS (t2.micro) |
| Non-root sudo user | hngdevops with sudo privileges |
| Passwordless sudo (restricted) | /usr/sbin/sshd and /usr/sbin/ufw only |
| Disable root SSH login | PermitRootLogin no in sshd_config |
| Key-based SSH only | PasswordAuthentication no in sshd_config |
| Firewall | UFW β ports 22, 80, 443 only |
| Web Server | Nginx |
Static Homepage (GET /) |
Serves HTML with HNG username visible |
JSON API (GET /api) |
Returns { message, track, username } |
| SSL Certificate | Let's Encrypt via Certbot |
| HTTP β HTTPS Redirect | 301 Permanent Redirect |
Internet
β
βΌ
βββββββββββββββββββββββββββββββ
β AWS EC2 Instance β
β Ubuntu 22.04 LTS β
β β
β βββββββββββββββββββββββββ β
β β UFW β β
β β 22 β
80 β
443 β
β β
β β All others β β β
β ββββββββββββ¬βββββββββββββ β
β β β
β ββββββββββββΌβββββββββββββ β
β β Nginx β β
β β β β
β β :80 β 301 β :443 β β
β β :443 / β HTML β β
β β :443 /api β JSON β β
β ββββββββββββ¬βββββββββββββ β
β β β
β ββββββββββββΌβββββββββββββ β
β β Let's Encrypt SSL β β
β β (Auto-renewing) β β
β βββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββ
β
βΌ
DuckDNS Domain
emmanuel-kabari.duckdns.org
- Log in to AWS Console
- Navigate to EC2 β Instances β Launch Instances
- Configure the instance:
Name: hng-devops-stage0
AMI: Ubuntu Server 22.04 LTS (Free Tier)
Instance Type: t2.micro
Key Pair: Create new β hng-devops-key (RSA, .pem)
- Configure Security Group inbound rules:
| Type | Protocol | Port | Source |
|---|---|---|---|
| SSH | TCP | 22 | Anywhere (0.0.0.0/0) |
| HTTP | TCP | 80 | Anywhere (0.0.0.0/0) |
| HTTPS | TCP | 443 | Anywhere (0.0.0.0/0) |
-
Click Launch Instance
-
Allocate an Elastic IP for a permanent public IP:
- EC2 β Network & Security β Elastic IPs β Allocate
- Actions β Associate β select your instance
- Click Associate
- Go to duckdns.org and log in
- Create a subdomain (e.g.
hng-yourname) - Enter your AWS Elastic IP in the IP field
- Click Update IP
Verify DNS propagation:
nslookup hng-yourname.duckdns.org
# Should return your Elastic IP# Fix key permissions
chmod 400 ~/Downloads/hng-devops-key.pem
# SSH into server
ssh -i ~/Downloads/hng-devops-key.pem ubuntu@YOUR_ELASTIC_IP# Create user
sudo adduser hngdevops
# Add to sudo group
sudo usermod -aG sudo hngdevops
# Configure restricted passwordless sudo
sudo visudo -f /etc/sudoers.d/hngdevopsAdd exactly this line:
hngdevops ALL=(root) NOPASSWD:/usr/sbin/sshd,/usr/sbin/ufw
Verify syntax:
sudo visudo -c -f /etc/sudoers.d/hngdevops
# Expected: parsed okOn your local machine:
# Generate SSH key pair
ssh-keygen -t ed25519 -C "hngdevops" -f ~/.ssh/hngdevops_key
# Press Enter twice (no passphrase)
# View public key (copy this)
cat ~/.ssh/hngdevops_key.pubOn the server:
# Set up SSH directory for hngdevops
sudo mkdir -p /home/hngdevops/.ssh
sudo chmod 700 /home/hngdevops/.ssh
sudo touch /home/hngdevops/.ssh/authorized_keys
sudo chmod 600 /home/hngdevops/.ssh/authorized_keys
sudo chown -R hngdevops:hngdevops /home/hngdevops/.ssh
# Add your public key
sudo nano /home/hngdevops/.ssh/authorized_keys
# Paste your public key, save with Ctrl+X, Y, EnterTest the new SSH login:
ssh -i ~/.ssh/hngdevops_key hngdevops@YOUR_ELASTIC_IP
# Expected: hngdevops@ip-xxx-xx-xx-xx:~$sudo nano /etc/ssh/sshd_configSet the following values:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM no
Apply changes:
sudo systemctl restart sshd
β οΈ Always test SSH login in a new terminal before closing your current session.
# Reset UFW
sudo ufw --force reset
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow required ports only
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw --force enable
# Verify
sudo ufw status verboseExpected output:
Status: active
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
sudo apt update && sudo apt upgrade -y
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginxCreate the static HTML page:
sudo mkdir -p /var/www/hng
sudo nano /var/www/hng/index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HNG DevOps Stage 0</title>
</head>
<body>
<h1>Emmanuel Kabari</h1>
<p>HNG Internship 14 - DevOps Track</p>
<p>Stage 0 Submission</p>
</body>
</html>Create the Nginx server block:
sudo nano /etc/nginx/sites-available/hngserver {
listen 80;
listen [::]:80;
server_name your-domain.duckdns.org;
root /var/www/hng;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /api {
default_type application/json;
return 200 '{"message":"HNGI14 Stage 0","track":"DevOps","username":"your-hng-username"}';
}
}Enable and test:
sudo ln -s /etc/nginx/sites-available/hng /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx# Install Certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtain SSL certificate
sudo certbot --nginx -d your-domain.duckdns.org
# Test auto-renewal
sudo certbot renew --dry-runCertbot automatically:
- Installs the SSL certificate
- Updates Nginx config with SSL settings
- Sets up a 301 redirect from HTTP to HTTPS
- Schedules auto-renewal
https://emmanuel-kabari.duckdns.org/
Returns an HTML page with the HNG username visibly displayed.
https://emmanuel-kabari.duckdns.org/api
Response:
{
"message": "HNGI14 Stage 0",
"track": "DevOps",
"username": "Emmanuel Kabari"
}Headers:
Content-Type: application/json
HTTP Status: 200 OK
| Check | Status |
|---|---|
| Root SSH login disabled | β
PermitRootLogin no |
| Password SSH auth disabled | β
PasswordAuthentication no |
| Key-based SSH only | β
PubkeyAuthentication yes |
| UFW active | β Enabled on startup |
| Only ports 22, 80, 443 open | β All others denied |
| HTTP β HTTPS 301 redirect | β Configured by Certbot |
| Valid SSL certificate | β Let's Encrypt |
| Auto SSL renewal | β Certbot cron job |
hngdevops passwordless sudo (restricted) |
β sshd and ufw only |
| Non-root deployment user | β
hngdevops |
Run these commands to verify the full setup:
# 1. HTTP redirects to HTTPS with 301
curl -I http://your-domain.duckdns.org
# 2. HTTPS homepage returns 200
curl -I https://your-domain.duckdns.org
# 3. API returns correct JSON
curl https://your-domain.duckdns.org/api
# 4. API Content-Type is application/json
curl -I https://your-domain.duckdns.org/api
# 5. UFW is active
sudo ufw status verbose
# 6. Nginx is running
sudo systemctl status nginx
# 7. Passwordless sudo works
sudo -u hngdevops sudo /usr/sbin/sshd -T | head -5
sudo -u hngdevops sudo /usr/sbin/ufw status/
βββ /etc/nginx/
β βββ sites-available/
β βββ hng # Nginx server block config
βββ /var/www/hng/
β βββ index.html # Static homepage
βββ /etc/letsencrypt/
β βββ live/your-domain/
β βββ fullchain.pem # SSL certificate
β βββ privkey.pem # SSL private key
βββ /etc/ssh/
β βββ sshd_config # Hardened SSH config
βββ /etc/sudoers.d/
β βββ hngdevops # Restricted passwordless sudo
βββ /home/hngdevops/
βββ .ssh/
βββ authorized_keys # SSH public keys
Emmanuel Kabari HNG Internship 14 β DevOps Track
Built with π» on AWS EC2 Β· Secured with π Let's Encrypt Β· Served by β‘ Nginx