A server you've just been handed is on the internet from the very first minute. The bots that constantly scan port 22 for weak passwords won't wait for you to finish setting up; look at /var/log/auth.log a few hours after launch and you'll see the failed login attempts piling up.
The good news is that most of that risk goes away with a handful of simple, repeatable steps. This guide walks through them in order: updates, a non-root user, key-only SSH login, a firewall, automatic security updates, fail2ban and time settings. Every command is written for Ubuntu 24.04 and assumes you're currently logged in as root or as the user your hosting provider created.
Short answer: To harden a fresh Ubuntu 24.04 server, update all packages, create a non-root user with sudo, and configure SSH to accept keys only, with root login disabled. Then enable ufw with SSH and ports 80 and 443 open, turn on unattended-upgrades for automatic security patches, install fail2ban, and check the time zone and clock sync. All of it takes less than an hour.
First things first: update
The images hosting providers use are often weeks or months behind on updates. Before anything else, upgrade the packages:
apt update
apt full-upgrade -y
If /var/run/reboot-required exists afterwards, the kernel or a core library has changed. Reboot now, while nothing is running on the server yet:
[ -f /var/run/reboot-required ] && reboot
A non-root user with sudo
Working as root day to day means every typo runs with full privileges. Instead, create an ordinary user and elevate with sudo only when needed. This guide calls the user deploy; pick any name you like.
adduser deploy
usermod -aG sudo deploy
adduser asks for a password. It won't be used for SSH logins, but sudo will ask for it, so choose a strong one and keep it in your password manager.
Key-only SSH login
Passwords can be guessed; an Ed25519 key, for all practical purposes, cannot. The goal of this section is to shut off password logins and direct root logins entirely.
Create and copy a key
If you don't have a key on your own machine yet, create one (run this on your laptop, not the server):
ssh-keygen -t ed25519 -C "laptop"
If your provider installed your key for root when the server was created, the simplest approach is to copy it to the new user. On the server, as root:
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
If password login is still enabled, running ssh-copy-id deploy@SERVER_IP from your laptop does the same thing. Now, in a new terminal, confirm that ssh deploy@SERVER_IP logs you in without a password and that sudo -v works. Don't go any further until it does. For more on keys, see connecting over SSH and creating a key.
A drop-in file for sshd
On Ubuntu 24.04, the main /etc/ssh/sshd_config includes every /etc/ssh/sshd_config.d/*.conf file in its first few lines. Rather than editing the main file, put your settings in a separate file; that way, upgrades to the openssh-server package won't prompt you about a modified config.
One subtle point: for each option, sshd applies the first value it reads, not the last1. On many cloud servers, cloud-init creates a file such as 50-cloud-init.conf containing PasswordAuthentication yes. To make sure your settings win, give your file a name that sorts earlier alphabetically:
# Only key-based logins, never as root
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
# Fewer chances per connection, shorter idle handshakes
MaxAuthTries 3
LoginGraceTime 30
# Nothing on this server needs X11
X11Forwarding no
# Only these users may log in over SSH
AllowUsers deploy
The AllowUsers line is both the most powerful and the most dangerous line in this file: any user not listed here can't log in, even with the right key. Spell the name exactly.
Before applying it, check the syntax and see which values sshd actually picked up:
sudo sshd -t
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowusers)'
The second command should print permitrootlogin no, passwordauthentication no and kbdinteractiveauthentication no. If you still see yes, another file is setting that option earlier.
Before you reload: keep a second session open
This is the single most important piece of advice in this guide. Before reloading sshd, open a second SSH session and keep it open until you're done. Reloading, or even restarting, the SSH service doesn't drop existing connections, so if the configuration is wrong, that open session is your only way to fix it without resorting to your provider's emergency console.
sudo systemctl reload ssh
On Ubuntu the service is called ssh, not sshd. Now test from a third terminal:
# Must work
ssh deploy@SERVER_IP
# Must fail with "Permission denied (publickey)"
ssh root@SERVER_IP
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@SERVER_IP
Close the earlier sessions only once all three results are what you expected.
Firewall with ufw
ufw is installed on Ubuntu but not enabled. Set the default policy to "deny all incoming, allow all outgoing" and open only SSH and web traffic. Order matters: allow SSH first, then enable ufw, or you'll lock yourself out.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
OpenSSH is a predefined profile that opens port 22; list the profiles with sudo ufw app list. If your web server supports HTTP/3, open 443/udp as well.
Two things that catch people out:
- Ports Docker publishes with
-pbypass ufw, because Docker writes its own iptables rules directly. If you run Docker, publish internal services on127.0.0.1only. - If your provider also offers a cloud firewall, keep both layers consistent; having both is fine, and even better.
Automatic security updates
Most breaches exploit vulnerabilities whose patches were released long ago. The unattended-upgrades package exists for exactly this: it installs security patches every day on its own. It's usually preinstalled on Ubuntu 24.04, but make sure:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
The second command creates or updates the following file, which should contain:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Some patches, such as kernel updates, don't take effect until a reboot. If your server can tolerate a few minutes of downtime in the middle of the night, turn on automatic reboots. Instead of editing 50unattended-upgrades, put your changes in a separate file that's read after it:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
04:00 is in the server's time zone, which is why the time settings below matter. To check that everything is configured correctly:
sudo unattended-upgrade --dry-run --debug
Run logs are written to /var/log/unattended-upgrades/.
fail2ban for SSH
With password login disabled, password guessing no longer works. So fail2ban is mostly about reducing noise and wasted load here: IPs with repeated failed attempts get banned for a while, and your logs stay readable.
sudo apt install -y fail2ban python3-systemd
Don't edit jail.conf; package updates may overwrite it. Put your settings in jail.local:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Never ban localhost; add your own static IP here if you have one
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
backend = systemd
With backend = systemd, logs are read straight from the journal, so you don't depend on any particular log file existing. Enable the service and check its status:
sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
If you ever ban your own IP, run this from your provider's console or from another server:
sudo fail2ban-client set sshd unbanip 203.0.113.7
Time zone and clock sync
An accurate clock isn't cosmetic: TLS certificates, time-limited tokens, log ordering and cron schedules all depend on it. Check the current state:
timedatectl
There are two sensible choices for the time zone. UTC is better for servers that serve several regions or whose logs are compared with other servers; local time is more convenient for a server whose users are in one region and whose cron jobs are tied to business hours. What matters is choosing deliberately:
sudo timedatectl set-timezone Europe/Berlin
# or keep it universal:
# sudo timedatectl set-timezone Etc/UTC
Ubuntu 24.04 uses systemd-timesyncd for clock synchronisation. Make sure it's on and the clock is synced:
sudo timedatectl set-ntp true
timedatectl show -p NTPSynchronized
timedatectl timesync-status
The second command should print NTPSynchronized=yes. If it doesn't, your provider's firewall is probably blocking outbound UDP on port 123.
First-hour checklist
Keep this list handy and run through it from top to bottom for every new server:
apt update && apt full-upgrade, plus a reboot if needed.- Create a non-root user and add it to the
sudogroup. - Copy the public key to that user's
~/.ssh/authorized_keys, with700and600permissions. - Test key login and
sudoin a new terminal. - Create
/etc/ssh/sshd_config.d/00-hardening.confand verify it withsshd -tandsshd -T. - Keep a second session open, then run
systemctl reload ssh. - Confirm that root and password logins are rejected.
- ufw:
allow OpenSSHfirst, then 80 and 443, thenenable. - unattended-upgrades enabled, and a decision made about automatic reboots.
- fail2ban with
jail.local, checked withfail2ban-client status sshd. - A deliberately chosen time zone and
NTPSynchronized=yes.
Frequently asked questions
Does changing the SSH port from 22 improve security?
Changing the port only reduces bot noise in your logs; it isn't a substitute for real security. Once password and root logins are disabled and only keys are accepted, password guessing on port 22 is effectively useless. If you do change the port, open the new one in ufw before reloading.
What if I lock myself out after changing SSH settings?
If you kept a second session open, fix 00-hardening.conf from there and run systemctl reload ssh again. If no session is open, your only option is your hosting provider's emergency (web) console.
Do I still need fail2ban with key-only SSH login?
It isn't essential, because password guessing no longer works. But fail2ban temporarily bans IPs with repeated failed attempts, which cuts down on wasted load and log volume.
Why doesn't ufw block Docker ports?
For ports published with -p, Docker writes its own iptables rules directly, and those rules bypass ufw. Publish internal services such as databases on 127.0.0.1 only.
Should my server's time zone be UTC or local time?
UTC is better for servers whose logs are compared with other servers or whose users span several regions. If your users are in one region and cron jobs are tied to business hours, local time is more convenient; what matters is choosing deliberately and keeping NTP on.
Wrap-up
None of these steps is complicated, and together they take less than an hour, yet they move your server off the list of easy targets. If you remember only one thing, make it this: before changing any SSH or firewall setting, keep a way back in open. The next step, and a topic of its own, is a backup you can actually restore from.
The
sshd_configman page states that for each keyword, the first obtained value is used. Seeman sshd_config. ↩