⚠️ Warning
These commands modify system security settings. Test on a non-production server first. If you're SSH'd into the server you're hardening, keep a second terminal open. A misconfigured firewall or SSH can lock you out.
Prerequisites
A fresh Ubuntu 24.04 LTS installation with root or sudo access. This guide assumes a headless server (no GUI). All commands are run as a non-root user with sudo privileges.
Pre-Flight Checklist
Create a non-root sudo user
Verify SSH access with that user
Have console access (for recovery if locked out)
Note your server's public IP
Phase 1: Initial System Lockdown
Start with updates and essential security packages.
sudo apt update && sudo apt upgrade -y
Reading package lists... Done
Building dependency tree... Done
sudo apt install -y ufw fail2ban unattended-upgrades apt-listchanges
✓ Security packages installed
Phase 2: Firewall (UFW)
UFW (Uncomplicated Firewall) is a user-friendly frontend for iptables. Default policy: deny everything incoming, allow everything outgoing.
Phase 2 — UFW Configuration
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# sudo ufw allow 2222/tcp
sudo ufw enable
Command may disrupt existing ssh connections. Proceed? (y|n) y
Firewall is active and enabled on system startup
sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing)
To Action From
-- ------ ----
22/tcp (SSH) ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
Rate Limiting with UFW
Limit brute-force attempts on SSH:
sudo ufw limit ssh/tcp
Rules updated
Phase 3: SSH Hardening
SSH is your primary attack surface. These changes significantly reduce exposure.
⚠️ Critical: Keep a Second Terminal Open
Test each SSH change in a new terminal before closing your current session. If you lock yourself out, you'll need console access to fix it.
If you do lock yourself out: it happens. I've done it. More than once. The first time feels like a disaster. The fifth time is just "oh, I need to reboot into recovery mode again." Every sysadmin has a story about the firewall rule that ate SSH. You're not incompetent — you're learning a system that doesn't forgive small mistakes. Document what you changed, fix it from the console, and move on. The lesson sticks better than any tutorial.
Step 1: Key-Based Authentication
Disable password auth entirely. Generate an SSH key on your local machine if you haven't:
Local Machine — Generate SSH Key
Generating public/private ed25519 key pair.
Enter file in which to save the key (~/.ssh/id_ed25519):
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip
✓ Key copied to server
Step 2: Harden SSH Configuration
Edit /etc/ssh/sshd_config:
/etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
MaxSessions 2
AllowTcpForwarding no
X11Forwarding no
AllowUsers your_username
Apply changes:
sudo sshd -t
✓ Configuration test passed (no output = good)
sudo systemctl restart sshd
Phase 4: Fail2Ban
Fail2Ban scans log files and bans IPs showing malicious signs. It catches brute-force attempts that slip past UFW rate limiting.
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status
Status: active
Configure Fail2Ban
Create a local configuration file:
/etc/fail2ban/jail.local
[DEFAULT]
bantime =
3600
findtime =
600
maxretry =
3
backend =
systemd
destemail =
[email protected]
sendername =
Fail2Ban
mta =
sendmail
action =
%(action_mwl)s
[sshd]
enabled =
true
port =
ssh
filter =
sshd
logpath =
%(sshd_log)s
maxretry =
3
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
|- Actions
|- Currently banned: 0
|- Total banned: 0
Quick feedback
Did this guide help?
Your answers shape what we write next.
Phase 5: Automatic Security Updates
Unattended-upgrades automatically installs security updates. For a self-hosted server, this is essential.
sudo dpkg-reconfigure -plow unattended-upgrades
Configuring unattended-upgrades
Automatically download and install stable updates?
Verify the configuration:
/etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
Phase 6: Security Headers (Apache/Nginx)
If you're running a web server, add these headers to harden the browser security posture.
For Apache
/etc/apache2/conf-available/security.conf
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Enable Apache Security Headers
sudo a2enmod headers
sudo a2enconf security
sudo apache2ctl configtest
sudo systemctl reload apache2
Phase 7: File Permissions & AIDE
Set proper permissions and monitor for unauthorized changes.
sudo apt install -y aide
sudo aideinit
Generating database...
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
echo "0 3 * * * root /usr/bin/aide --check" | sudo tee /etc/cron.d/aide-check
Secure Sensitive Files
sudo chmod 600 /etc/ssh/sshd_config
sudo chmod 600 /etc/fail2ban/jail.local
sudo chmod 700 /root
sudo chmod 750 /home/*
Phase 8: Log Monitoring
Logs are your visibility. Configure log rotation and basic monitoring.
sudo apt install -y logrotate
sudo tail -f /var/log/auth.log
sudo tail -f /var/log/fail2ban.log
sudo tail -f /var/log/ufw.log
Verification Checklist
Post-Hardening Verification
SSH only accepts keys (test: ssh -o PasswordAuthentication=no user@host)
UFW is active (sudo ufw status)
Fail2Ban is running (sudo systemctl status fail2ban)
Auto-updates configured (cat /etc/apt/apt.conf.d/20auto-upgrades)
Security headers present (curl -I https://yourdomain.com)
No root login (grep PermitRootLogin /etc/ssh/sshd_config)
Security Scan
Run a quick audit with Lynis:
sudo apt install -y lynis
sudo lynis audit system --quick
[+] Initializing program
[+] Performing tests in category: Authentication
...
Hardening index: [####......] 72/100
What's Next?
Aim for a hardening index above 70. Review Lynis suggestions and implement relevant ones. Re-run the scan monthly after updates.
Next Steps