Linux Server Hardening Guide for Developers — SSH, Firewall, and Fail2Ban (2026)
W
Writer
Published: 16 Jun 2026
13 min read
Step-by-step hardening of a fresh Ubuntu VPS — disable root SSH login, key-only auth, UFW firewall, Fail2Ban brute-force protection, automatic security updates, and auditd kernel logging.
A Fresh VPS Is Already Being Attacked
Spin up a new Ubuntu VPS on DigitalOcean, Hetzner, or AWS and check your auth logs after 10 minutes. You will find hundreds of failed SSH login attempts from IPs across the globe. Automated bots scan the entire IPv4 address space continuously, probing every new IP for default credentials and open ports. The default Ubuntu configuration is not designed to survive this.
This guide walks through hardening a fresh Ubuntu 24.04 LTS server from default to production-ready — disabling root SSH login, enforcing key-only authentication, configuring UFW firewall, installing Fail2Ban, enabling automatic security updates, and setting up basic audit logging. Every command is tested on Ubuntu 24.04 and works on Debian 12 with minor path differences.
Complete this guide immediately after provisioning your server — before deploying any application. Run every step in order. Do not close your SSH session until you have verified that your new sudo user can log in with key-based auth. Locking yourself out is a real risk if steps are skipped.
Step 1 — Create a Non-Root Sudo User
Root has unrestricted access to everything on the system. A mistake or compromised session as root is catastrophic — there is no safety net. Create a dedicated user with sudo access for all administrative tasks, then disable root SSH login entirely.
bash
Run as root on fresh server
# Create new user (replace 'deploy' with your preferred username)
adduser deploy
# Add to sudo group
usermod -aG sudo deploy
# Verify sudo access
su - deploy
sudowhoami# should print 'root'exit
Step 2 — SSH Key-Based Authentication
SSH keys are cryptographically stronger than any password. A 4096-bit RSA key or an Ed25519 key cannot be brute-forced — it would take longer than the age of the universe. Once keys are in place, password authentication is disabled entirely, eliminating the entire class of brute-force attacks.
Generate SSH Key (on your local machine)
bash
Run on your LOCAL machine
# Ed25519 is modern, smaller, and faster than RSA
ssh-keygen -t ed25519 -C "deploy@yourserver" -f ~/.ssh/yourserver_ed25519
# Or RSA 4096 if Ed25519 is not supported by your client
ssh-keygen -t rsa -b 4096 -C "deploy@yourserver" -f ~/.ssh/yourserver_rsa
# Copy public key to server (run from local machine)
ssh-copy-id -i ~/.ssh/yourserver_ed25519.pub deploy@YOUR_SERVER_IP
# Test key-based login before proceeding
ssh -i ~/.ssh/yourserver_ed25519 deploy@YOUR_SERVER_IP
Harden SSH Configuration
bash
Run on server as deploy (sudo)
# Back up original config before editingsudocp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo nano /etc/ssh/sshd_config
bash
/etc/ssh/sshd_config — Key Changes
# Change default port (obscurity, not security — but reduces log noise significantly)
Port 2222
# Disable root login entirely
PermitRootLogin no
# Disable password authentication
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
# Disable empty passwords
PermitEmptyPasswords no
# Only allow specific users to SSH
AllowUsers deploy
# Restrict to specific IP (optional — if you have a static IP)# AllowUsers deploy@YOUR_HOME_IP# Use only modern key exchange algorithms
KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512,hmac-sha2-256
# Disconnect idle sessions after 10 minutes
ClientAliveInterval 300
ClientAliveCountMax 2
# Reduce login grace time
LoginGraceTime 30
# Disable X11 forwarding (not needed for servers)
X11Forwarding no
bash
Apply SSH Changes
# Test config syntax before restartingsudo sshd -t
# Restart SSH servicesudo systemctl restart ssh
# IMPORTANT: Open a second terminal and verify login still works# ssh -p 2222 -i ~/.ssh/yourserver_ed25519 deploy@YOUR_SERVER_IP# Do NOT close the current session until new session confirms working
Always Keep a Backup Session Open
When changing SSH configuration, always keep your current session open and test the new config in a second terminal window. If you lock yourself out, you will need console access via your VPS provider's web panel (DigitalOcean Droplet Console, Hetzner VNC, AWS Systems Manager) to recover.
Step 3 — Configure UFW Firewall
UFW (Uncomplicated Firewall) is the standard iptables front-end on Ubuntu. The principle is simple: deny all inbound traffic by default, then explicitly allow only what your application needs. This limits the attack surface to exactly the ports your app uses.
bash
UFW Setup
# Check current statussudo ufw status
# Set default policies: deny incoming, allow outgoingsudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH on the custom port you set (use your actual port)sudo ufw allow 2222/tcp comment 'SSH'# Allow HTTP and HTTPS for your web appsudo ufw allow 80/tcp comment 'HTTP'sudo ufw allow 443/tcp comment 'HTTPS'# If running a Node.js API directly (not behind nginx)# sudo ufw allow 3000/tcp comment 'Node API'# Enable firewallsudo ufw enable# Verify rulessudo ufw status numbered
bash
Expected UFW Status Output
Status: active
To Action From
-- ------ ----
[ 1] 2222/tcp ALLOW IN Anywhere # SSH
[ 2] 80/tcp ALLOW IN Anywhere # HTTP
[ 3] 443/tcp ALLOW IN Anywhere # HTTPS
Port 22 Is Still Closed
After this setup, the default SSH port 22 is blocked. All SSH connections must use port 2222 (or whatever port you chose). Update your ~/.ssh/config on your local machine to avoid typing -p 2222 every time.
bash
~/.ssh/config (local machine)
Host myserver
HostName YOUR_SERVER_IP
User deploy
Port 2222
IdentityFile ~/.ssh/yourserver_ed25519
IdentitiesOnly yes# Usage: ssh myserver (no flags needed)
Step 4 — Install and Configure Fail2Ban
Even with key-based auth, bots still probe SSH ports and generate log noise. Fail2Ban reads authentication logs and automatically bans IPs that exceed a threshold of failed attempts — typically after 5 failures within 10 minutes. It works for SSH, Nginx, and any other service that writes to a log file.
[DEFAULT]# Ban for 1 hourbantime = 3600# Window for counting failuresfindtime = 600# Failures before banmaxretry = 5# Your server's IP (never ban yourself)ignoreip = 127.0.0.1/8[sshd]enabled = trueport = 2222# match your SSH portlogpath = /var/log/auth.log
maxretry = 3# stricter for SSH[nginx-http-auth]enabled = trueport = http,https
logpath = /var/log/nginx/error.log
[nginx-limit-req]enabled = trueport = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
bash
Start and Verify Fail2Ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Check status of all jailssudo fail2ban-client status
# Check SSH jail specificallysudo fail2ban-client status sshd
# View currently banned IPssudo fail2ban-client status sshd | grep "Banned IP"# Manually unban an IP (if you ban yourself)sudo fail2ban-client set sshd unbanip YOUR_IP
Step 5 — Automatic Security Updates
Most server compromises exploit known vulnerabilities that have patches available. Unattended-upgrades automatically installs security updates, closing the window between a patch release and your server being updated. It only applies security updates by default — not version upgrades that might break your app.
bash
Configure Automatic Security Updates
sudo apt install -y unattended-upgrades
# Enable and configuresudo dpkg-reconfigure -plow unattended-upgrades
# Select 'Yes' when prompted# Verify configurationcat /etc/apt/apt.conf.d/20auto-upgrades
// Automatically reboot if required (e.g., kernel update)
// Set to a time when traffic is low
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
// Email notification on upgrade (optional)
Unattended-Upgrade::Mail "your@email.com";
Unattended-Upgrade::MailReport "on-change";
Step 6 — System Audit Logging with auditd
auditd is the Linux audit framework — it logs system calls at the kernel level, capturing file access, privilege escalation, and network connections. For compliance (PCI-DSS, SOC 2) and incident investigation, audit logs are essential. For a production server, even basic auditd rules provide invaluable forensic data.
bash
Install and Configure auditd
sudo apt install -y auditd audispd-plugins
# Basic rules for common threatssudo nano /etc/audit/rules.d/hardening.rules
bash
/etc/audit/rules.d/hardening.rules
# Delete all existing rules first
-D
# Buffer size
-b 8192
# Failure mode: 1=printk, 2=panic
-f 1
# Monitor changes to user/group files
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity
# Monitor SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
# Monitor cron jobs
-w /etc/cron.d/ -p wa -k cron
-w /etc/crontab -p wa -k cron
# Log all sudo commands
-w /usr/bin/sudo -p x -k sudo_commands
# Monitor authentication logs
-w /var/log/auth.log -p wa -k auth_log
# Detect privilege escalation
-a always,exit -F arch=b64 -S setuid -S setgid -k privilege_escalation
# List all running servicessudo systemctl list-units --type=service --state=running
# Disable services you do not need (examples)sudo systemctl disable --now avahi-daemon # mDNS/Bonjour discoverysudo systemctl disable --now cups # printing servicesudo systemctl disable --now bluetooth # Bluetooth (VPS has none)# Check open ports — close anything unexpectedsudo ss -tlnp
Generate new pair, update authorized_keys, remove old
Should I change the SSH port? Isn't security through obscurity bad?
Changing the port is not a security control — it is noise reduction. It does not prevent a determined attacker but eliminates 99% of automated bot traffic from your logs, making real threats easier to spot. Combine it with key-only auth and Fail2Ban for actual security.
Does this guide work on Hetzner, DigitalOcean, and AWS EC2?
Yes for Ubuntu 24.04 on all three. One difference: AWS EC2 uses Security Groups (cloud firewall) in addition to UFW. Configure both — Security Groups as the outer layer, UFW as a second layer. For DigitalOcean and Hetzner, UFW is your primary firewall.
What if I get locked out after changing SSH config?
Use your VPS provider's emergency console access: DigitalOcean Droplet Console, Hetzner VNC Console, or AWS Systems Manager Session Manager. Log in as root via the web console, fix /etc/ssh/sshd_config, and restart SSH with: sudo systemctl restart ssh.
Is this enough for PCI-DSS or SOC 2 compliance?
This covers the foundational controls but compliance requires more — formal access policies, change management procedures, vulnerability scanning, penetration testing, and documentation. Use this as your technical baseline and layer compliance-specific requirements (log retention periods, MFA for all admin access) on top.
Key Takeaways
Create a non-root sudo user and disable root SSH login before anything else — root compromises are total compromises
SSH key-based auth with PasswordAuthentication disabled eliminates brute-force attacks entirely
UFW default-deny with explicit allow rules limits your attack surface to exactly the ports your app needs
Fail2Ban auto-bans IPs after 3–5 failed SSH attempts — set maxretry lower than the default for SSH
Unattended-upgrades automatically installs security patches, closing the window on known vulnerabilities
auditd provides kernel-level logging of file changes, sudo usage, and privilege escalation — essential for forensics