This is the first post in a three-part series on hardening a public Ubuntu server.
In the previous post we secured SSH: replaced password login with key-based authentication, disabled root, and restricted which users can connect at all. That alone eliminates the majority of attacks targeting internet-facing servers.
But SSH is one layer. This series adds three more layers per post, ordered by how quickly you can apply them and how much impact they have. Start here — these three take less than fifteen minutes and protect against the most common active threats.
Assumes: Ubuntu 20.04 / 22.04 / 24.04, SSH already hardened, a non-root user with
sudoaccess.
1 — UFW: Default-Deny Firewall
A freshly provisioned server often exposes every port that has a service listening on it. The firewall changes that: block everything by default, allow only what you explicitly need.
Ubuntu ships with UFW (Uncomplicated Firewall) pre-installed. It is a usable frontend to iptables/nftables that keeps rules readable.
Enable with SSH already open
The most common way to lock yourself out: enable the firewall before allowing SSH. Do this first:
# Allow SSH through the firewall before activating it
sudo ufw allow OpenSSH
# If you run a web server, allow HTTP and HTTPS too
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Now enable
sudo ufw enable
# Verify
sudo ufw status verbose
The output should say Status: active and list exactly the ports you opened. Any port not in that list is now closed, regardless of what is listening on it.
Non-standard SSH port: if you changed the SSH port from 22, allow that specific port instead:
sudo ufw allow 2222/tcp comment "SSH"
Rate-limit connections
UFW can throttle incoming connections to a port — useful as a lightweight layer before Fail2ban is configured:
# Ban IPs that attempt more than 6 connections in 30 seconds
sudo ufw limit OpenSSH
Verify from your local machine
Before closing your session, open a second terminal and confirm SSH still works:
ssh deploy@your-server-ip
ssh deploy@your-server-ip
2 — Fail2ban: Brute-Force Protection
Even with key-only SSH, other services (web servers, mail servers, custom APIs) accept connections that can be brute-forced. Fail2ban monitors log files and adds a temporary firewall ban for IPs that produce too many failures.
sudo apt install fail2ban -y
Do not edit /etc/fail2ban/jail.conf — it gets overwritten on updates. Create a local override instead:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
Find the [DEFAULT] section at the top and set:
bantime = 1h
findtime = 10m
maxretry = 5
This bans any IP for one hour if it fails authentication more than five times within ten minutes. Adjust bantime to something longer (e.g., 24h or 1w) if your logs show persistent scanners.
The [sshd] jail is enabled by default. Check that it is active:
sudo fail2ban-client status sshd
Start and enable Fail2ban:
sudo systemctl enable --now fail2ban
Useful commands
# List all active jails
sudo fail2ban-client status
# Show banned IPs for a specific jail
sudo fail2ban-client status sshd
# Unban an IP (if you accidentally ban yourself during testing)
sudo fail2ban-client set sshd unbanip 1.2.3.4
# Watch the Fail2ban log live
sudo tail -f /var/log/fail2ban.log
3 — Unattended Upgrades: Automatic Security Patches
Unpatched packages are one of the most common root causes of server compromise. Enabling automatic security updates means you no longer depend on manually running apt upgrade before a CVE is exploited.
sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
Answer Yes to the single question in the dialog.
Verify the configuration file:
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
The most relevant section:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
This restricts automatic installation to security updates only — not feature updates that might break things.
Two more settings worth reviewing:
// Remove old kernel packages that are no longer needed
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
// Reboot if required (e.g. after a kernel update)
// Set to "true" only if you have a defined maintenance window
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
Verify the timer is running:
sudo systemctl status apt-daily-upgrade.timer
Test the configuration without actually installing anything:
sudo unattended-upgrade --dry-run --debug
What These Three Measures Do Together
| Measure | What it stops |
|---|---|
| UFW default-deny | Services accidentally exposed on unexpected ports |
| Fail2ban | Automated credential brute-forcing |
| Unattended upgrades | Exploitation of known, patched CVEs |
None of these are difficult to set up. They cover the three most common attack patterns against public servers and take less than fifteen minutes combined.
Part 2 covers the next layer: kernel parameter hardening, disabling unnecessary services, and tightening sudo and login policies.
