Set Up and Secure a Linux Server on AWS EC2 from Scratch

Launch an AWS EC2 Ubuntu server, configure SSH security, set up a firewall, create user accounts, and deploy a web application with Nginx as a reverse proxy.

Domains & Technologies

Domains
AWS-CLOUD-ENGINEERING
Technologies
AWSLINUXNGINX

Blueprint Walkthrough

Architecture Overview

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.

◈ DIAGRAM
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)
Problem Solved

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-by-Step Implementation Guide

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:

Bash
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
Validation & Testing
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"