This project teaches you how to provision and secure a real Linux server on AWS. Every DevOps engineer needs to know how to set up a server from scratch — configure SSH, lock down firewall rules, create user accounts, and run a web application behind Nginx. This is the foundation that everything else builds on. Before you can deploy containers, manage Kubernetes, or use Terraform, you need to understand how a Linux server actually works. Your Laptop (SSH client) | | Port 22 (SSH - key auth only) v AWS EC2 Instance (Ubuntu 22.04) | +----+----+ | | v v UFW Nginx (firewall) (reverse proxy) | v Node.js App (port 3000 - not exposed) | v Internet users reach the app through Nginx on port 80 (Node.js is never exposed directly)
A freshly launched EC2 instance is not secure. Root login is enabled. Password authentication is allowed. No firewall is configured. Anyone on the internet can attempt to brute-force your server. Within hours of launching an insecure server, you will see thousands of failed login attempts in your logs from automated bots scanning the internet. This project walks you through hardening a server step by step — disabling root login, using SSH keys instead of passwords, configuring a strict firewall, and running your application behind Nginx so the Node.js port is never directly exposed to the internet.
### Step 1: Launch the EC2 Instance ```bash ## Install AWS CLI if not already installed curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" \ -o "awscliv2.zip" unzip awscliv2.zip && sudo ./aws/install aws configure # Enter your access key and secret ## Launch an Ubuntu 22.04 EC2 instance aws ec2 run-instances \ --image-id ami-0f5ee92e2d63afc18 \ --instance-type t2.micro \ --key-name YOUR_KEY_PAIR_NAME \ --security-group-ids sg-xxxxxxxxxx \ --tag-specifications \ 'ResourceType=instance,Tags=[{Key=Name,Value=my-linux-server}]' ## Get the public IP of your instance aws ec2 describe-instances \ --filters "Name=tag:Name,Values=my-linux-server" \ --query \ 'Reservations[0].Instances[0].PublicIpAddress' \ --output text ``` Create the security group to allow SSH and web traffic: ```bash ## Create security group SG_ID=$(aws ec2 create-security-group \ --group-name my-server-sg \ --description "Security group for my Linux server" \ --query 'GroupId' --output text) ## Allow SSH from your IP only (replace with your actual IP) MY_IP=$(curl -s https://checkip.amazonaws.com) aws ec2 authorize-security-group-ingress \ --group-id $SG_ID \ --protocol tcp --port 22 \ --cidr ${MY_IP}/32 ## Allow HTTP from anywhere (web traffic) aws ec2 authorize-security-group-ingress \ --group-id $SG_ID \ --protocol tcp --port 80 \ --cidr 0.0.0.0/0 echo "Security group ID: $SG_ID" ``` > ⚠️ **Security:** Never open port 22 (SSH) to `0.0.0.0/0`. This allows any IP on the internet to attempt SSH connections. Always restrict SSH to your specific IP address using the `/32` CIDR notation. ### Step 2: Connect and Initial Server Setup ```bash ## SSH into your instance using the key pair ssh -i ~/.ssh/YOUR_KEY_PAIR.pem ubuntu@YOUR_EC2_PUBLIC_IP ## The first command to run on any new server — update packages sudo apt update && sudo apt upgrade -y ## Install essential tools sudo apt install -y \ curl \ wget \ git \ htop \ unzip \ vim ## Check system information uname -a # Kernel version df -h # Disk space free -h # Memory available who # Who is logged in last # Recent login history ``` ### Step 3: Create a Dedicated User and Secure SSH Never run your applications as `ubuntu` or `root`. Create a dedicated user with limited permissions. ```bash ## Create a new user (replace 'appuser' with your preferred name) sudo adduser appuser ## Enter a strong password when prompted ## Fill in or skip the optional fields ## Add the user to the sudo group (so they can run admin commands) sudo usermod -aG sudo appuser ## Set up SSH key authentication for the new user sudo mkdir -p /home/appuser/.ssh sudo chmod 700 /home/appuser/.ssh ## Copy your public key to the new user ## (This is the .pub file matching your key pair) sudo cp ~/.ssh/authorized_keys /home/appuser/.ssh/ sudo chown -R appuser:appuser /home/appuser/.ssh sudo chmod 600 /home/appuser/.ssh/authorized_keys ## Test the new user works BEFORE locking down SSH ## Open a NEW terminal window and try: ## ssh -i ~/.ssh/YOUR_KEY.pem appuser@YOUR_EC2_IP ## Make sure this works before proceeding! ``` Now harden SSH configuration: ```bash ## Edit the SSH configuration file sudo vim /etc/ssh/sshd_config ## Find and change these settings: ## PermitRootLogin no <- Disable root login ## PasswordAuthentication no <- Keys only, no passwords ## PubkeyAuthentication yes <- Enable key authentication ## Port 22 <- Keep default or change to custom port ## After editing, restart SSH sudo systemctl restart sshd ## Verify SSH is running sudo systemctl status sshd ``` > 🔴 **Common Mistake:** Changing `PasswordAuthentication no` and then getting locked out because your SSH key was not set up correctly. Always test SSH key login from a NEW terminal window before saving the sshd_config changes. Keep your original SSH session open as a backup while testing. ### Step 4: Configure UFW Firewall UFW (Uncomplicated Firewall) is the beginner-friendly way to manage Linux firewall rules on Ubuntu. ```bash ## Check UFW status (disabled by default) sudo ufw status ## Set default policies: ## Block all incoming connections by default ## Allow all outgoing connections (your server can reach the internet) sudo ufw default deny incoming sudo ufw default allow outgoing ## Allow SSH (CRITICAL: do this before enabling UFW or you get locked out) sudo ufw allow ssh ## Allow HTTP web traffic sudo ufw allow 80/tcp ## Allow HTTPS web traffic (for when you add SSL later) sudo ufw allow 443/tcp ## Enable the firewall sudo ufw enable ## Type 'y' when prompted ## Verify the rules sudo ufw status verbose ## Expected output: ## To Action From ## -- ------ ---- ## 22/tcp ALLOW IN Anywhere ## 80/tcp ALLOW IN Anywhere ## 443/tcp ALLOW IN Anywhere ## Check that port 3000 (Node.js) is NOT listed ## It should be blocked from external access ``` > 💡 **Tip:** The UFW firewall on Ubuntu works alongside the AWS Security Group. Both must allow traffic for it to reach your server. Think of it as two layers of protection — the Security Group is the outer gate, UFW is the inner gate. ### Step 5: Install Node.js and Deploy an Application ```bash ## Install Node.js using NodeSource (always use the official method) curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs ## Verify installation node --version # Expected: v20.x.x npm --version # Expected: 10.x.x ## Create a simple web application mkdir ~/webapp && cd ~/webapp cat > package.json << 'EOF' { "name": "webapp", "version": "1.0.0", "scripts": { "start": "node app.js" }, "dependencies": { "express": "^4.18.2" } } EOF npm install cat > app.js << 'EOF' const express = require('express'); const app = express(); app.get('/', (req, res) => { res.send('<h1>My Linux Server is Running!</h1>'); }); app.get('/health', (req, res) => { res.json({ status: 'healthy', host: require('os').hostname() }); }); app.listen(3000, () => { console.log('App running on port 3000'); }); EOF ## Start the app in the background temporarily to test it node app.js & curl http://localhost:3000 ## Expected: HTML response kill %1 # Stop the background process ``` ### Step 6: Set Up Nginx as a Reverse Proxy Nginx sits in front of your Node.js app. Users connect to Nginx on port 80, and Nginx forwards requests to Node.js on port 3000. Port 3000 never needs to be open to the internet. ```bash ## Install Nginx sudo apt install -y nginx sudo systemctl start nginx sudo systemctl enable nginx ## Test Nginx is running curl http://localhost ## Expected: Nginx welcome page HTML ## Create a configuration file for your application sudo vim /etc/nginx/sites-available/webapp ``` Add this content to the file: ```nginx server { listen 80; server_name YOUR_EC2_PUBLIC_IP; # Log files for debugging access_log /var/log/nginx/webapp-access.log; error_log /var/log/nginx/webapp-error.log; location / { # Forward all requests to Node.js on port 3000 proxy_pass http://localhost:3000; # Pass the original request information proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` ```bash ## Enable the site by creating a symlink sudo ln -s /etc/nginx/sites-available/webapp \ /etc/nginx/sites-enabled/ ## Remove the default Nginx site sudo rm /etc/nginx/sites-enabled/default ## Test the configuration syntax sudo nginx -t ## Expected: syntax is ok, test is successful ## Reload Nginx to apply changes sudo systemctl reload nginx ``` ### Step 7: Run Node.js as a System Service with PM2 PM2 is a process manager for Node.js. It keeps your app running after crashes and restarts it automatically when the server reboots. ```bash ## Install PM2 globally sudo npm install -g pm2 ## Start the application with PM2 cd ~/webapp pm2 start app.js --name webapp ## Verify it is running pm2 status ## Expected: webapp | online pm2 logs webapp # View recent logs ## Configure PM2 to start on server boot pm2 startup ## Copy and run the command that PM2 outputs ## It will look like: sudo env PATH=... pm2 startup systemd pm2 save # Save the current process list ## Test the full stack curl http://YOUR_EC2_PUBLIC_IP ## Expected: HTML from your Node.js app served through Nginx ```
```bash ## 1. Verify SSH only accepts key authentication ssh -o PasswordAuthentication=yes ubuntu@YOUR_EC2_IP ## Expected: Permission denied (publickey) ## This confirms password login is disabled ## 2. Verify root login is disabled ssh root@YOUR_EC2_IP ## Expected: Permission denied — root login blocked ## 3. Check firewall rules are correct sudo ufw status verbose ## Expected: Only ports 22, 80, 443 allowed ## 4. Verify Node.js port is NOT directly accessible curl http://YOUR_EC2_IP:3000 ## Expected: Connection refused or timeout ## Port 3000 is blocked by UFW and Security Group ## 5. Verify the web app works through Nginx curl http://YOUR_EC2_IP ## Expected: HTML from your Node.js app ## 6. Verify PM2 restarts the app on crash pm2 stop webapp ## App is stopped pm2 start webapp ## App restarts immediately ## Simulate a crash kill $(pm2 id webapp | tr -d '[]') pm2 status ## Expected: PM2 restarts the app automatically ## 7. Check for failed login attempts (bots!) sudo grep "Failed password" /var/log/auth.log | tail -20 ## Expected: Many failed attempts from random IPs ## This is normal — bots scan the entire internet constantly ## Your SSH key authentication means none of these attempts succeed ## 8. Verify Nginx logs are working sudo tail -f /var/log/nginx/webapp-access.log ## In another terminal: curl http://YOUR_EC2_IP ## Expected: Request appears in the log echo "Linux server setup and secured successfully" ```
This project teaches you how to provision and secure a real Linux server on AWS. Every DevOps engineer needs to know how...
A freshly launched EC2 instance is not secure. Root login is enabled. Password authentication is allowed. No firewall is...
Step 1: Launch the EC2 Instance Create the security group to allow SSH and web traffic: > ⚠️ Security: Never open port 2...
...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.