A freshly deployed server is exposed to the Internet from its first minute, and automated bots start trying SSH passwords almost immediately. In this tutorial you will apply the baseline every Ubuntu 24.04 or Debian 12 server should have: current packages, a non-root administrator, key-only SSH without root login, a UFW firewall, Fail2Ban, automatic security updates and correct time. Each step includes a check so you know it worked before moving on.

Prerequisites

To follow this guide you need:

  • A new server running Ubuntu 24.04 LTS or Debian 12, for example a CubePath VPS.
  • Root access over SSH (password or key) and the server's IP address, shown as your_server_ip.
  • An SSH key pair on your local computer. If you do not have one, create it with ssh-keygen -t ed25519 as described in our guide on connecting to a server via SSH.
  • Access to the server's web console (on CubePath, the VNC console in the dashboard) in case an SSH change locks you out.

Differences between Ubuntu and Debian are called out where they matter.

Step 1 - Updating the system

Start by installing all pending updates so the rest of the setup runs on patched packages. Log in as root:

ssh root@your_server_ip

Then refresh the package index and upgrade:

apt update
apt full-upgrade -y

If the kernel or core libraries were updated, a reboot is needed to load them. On Ubuntu, the flag file /var/run/reboot-required exists in that case:

ls /var/run/reboot-required 2>/dev/null && reboot

If the command prints nothing, no reboot is required. On Debian 12 this file is not always created at this point, so simply reboot if the upgrade installed a new linux-image package. After a reboot, wait a minute and log back in as root.

Step 2 - Creating a non-root sudo user

Working as root makes every typo a system-wide risk, and root is the first name bots try. Create a regular user for daily work; this guide uses sammy, replace it with your own your_user:

adduser sammy

adduser asks for a password and optional details (you can leave those empty). Choose a strong password: you will need it for sudo even after SSH switches to keys.

Add the user to the sudo group, which is allowed to run commands as root on Ubuntu and Debian:

usermod -aG sudo sammy

On a minimal Debian 12 install, sudo may not be installed. Install it if the next check fails with command not found:

apt install -y sudo

Verify that the new user can use sudo:

su - sammy -c 'sudo whoami'
[sudo] password for sammy:
root

Step 3 - Installing your SSH key for the new user

Copy your public key to the new user from your local computer:

ssh-copy-id sammy@your_server_ip

If your key is already in root's authorized_keys (for example because you added it when creating the server), you can copy it on the server instead, with the right ownership and permissions:

rsync --archive --chown=sammy:sammy ~/.ssh /home/sammy

Now open a second local terminal and log in as the new user:

ssh sammy@your_server_ip

You should get a shell without being asked for the server password (only for your key passphrase, if it has one). Run sudo -v to confirm sudo works in this session. From here on, work as sammy.

Step 4 - Hardening the SSH server

With key login confirmed, turn off password logins and direct root login. Instead of editing the main file, create a drop-in file. OpenSSH keeps the first value it reads for each option and reads /etc/ssh/sshd_config.d/*.conf in alphabetical order before the rest of sshd_config, so a file named 00-... overrides defaults such as 50-cloud-init.conf, which on some images turns password login back on.

sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers sammy

What these do:

  • PermitRootLogin no: root cannot log in over SSH at all; you use sudo instead.
  • PasswordAuthentication no and KbdInteractiveAuthentication no: only keys are accepted.
  • MaxAuthTries 3 and LoginGraceTime 30: limit attempts per connection and how long an unauthenticated connection may stay open.
  • AllowUsers sammy: only the listed users may log in. Add other administrators here, separated by spaces.

Validate the configuration. sshd -t prints nothing when the syntax is correct, and sshd -T shows the effective values:

sudo sshd -t
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers)'
permitrootlogin no
passwordauthentication no
allowusers sammy

Apply the change:

sudo systemctl restart ssh

From your second terminal, confirm that you can still log in as sammy, and that root is now rejected:

ssh root@your_server_ip
[email protected]: Permission denied (publickey).

Step 5 - Enabling the UFW firewall

UFW (Uncomplicated Firewall) is a front end for the kernel firewall. It is installed by default on Ubuntu; on Debian 12, install it first:

sudo apt install -y ufw

Set a default policy that blocks incoming traffic and allows outgoing traffic, then allow SSH before enabling the firewall. The limit rule allows SSH but temporarily blocks an address that opens 6 or more connections within 30 seconds:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH

If you run a web server, open its ports now too:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Enable the firewall and answer y:

sudo ufw enable

Check the result:

sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           LIMIT IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
22/tcp (OpenSSH (v6))      LIMIT IN    Anywhere (v6)
80/tcp (v6)                ALLOW IN    Anywhere (v6)
443/tcp (v6)               ALLOW IN    Anywhere (v6)

Open a new SSH session to confirm you are not locked out.

Step 6 - Installing Fail2Ban

Even with passwords disabled, bots keep hammering port 22 and fill your logs. Fail2Ban watches authentication failures and bans offending IP addresses for a while.

sudo apt install -y fail2ban

Never edit jail.conf directly, because package upgrades overwrite it. Put your settings in jail.local:

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw

[sshd]
enabled = true
backend = systemd

banaction = ufw makes bans appear as UFW rules, and backend = systemd reads SSH events from the journal, which works on Ubuntu 24.04 and on Debian 12 (where /var/log/auth.log does not exist by default). If you manage the server from a fixed IP address, add ignoreip = 127.0.0.1/8 ::1 your_ip under [DEFAULT] so you can never ban yourself.

Enable the service and check the SSH jail:

sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

Within a few hours on a public IP the failed and banned counters will start to rise. To unban an address manually, run sudo fail2ban-client set sshd unbanip 198.51.100.7.

Step 7 - Turning on automatic security updates

unattended-upgrades installs security updates daily without waiting for you. It is installed and enabled by default on Ubuntu 24.04; on Debian 12, install it:

sudo apt install -y unattended-upgrades

Make sure the daily run is enabled. Choose Yes in the dialog:

sudo dpkg-reconfigure --priority=low unattended-upgrades

This writes /etc/apt/apt.conf.d/20auto-upgrades. Confirm its content:

cat /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

By default only packages from the security origin are installed automatically, and the server is never rebooted. If you want a scheduled reboot when a kernel update requires it, create a small override file:

sudo nano /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Run a dry run to check that the configuration is valid and see what would be installed:

sudo unattended-upgrade --dry-run --debug

The output lists the allowed origins and the packages it would upgrade, and ends without errors. Real runs are logged in /var/log/unattended-upgrades/unattended-upgrades.log.

Step 8 - Setting the timezone and time synchronization

Accurate time matters for logs, TLS certificates and Fail2Ban's time windows. Check the current state:

timedatectl
               Local time: Thu 2026-09-24 10:15:02 UTC
           Universal time: Thu 2026-09-24 10:15:02 UTC
                 RTC time: Thu 2026-09-24 10:15:02
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Keeping servers on UTC makes logs from different machines easy to compare. If you prefer a local zone, list them with timedatectl list-timezones and set one:

sudo timedatectl set-timezone Europe/Madrid

If NTP service shows inactive (common on minimal Debian installs), install and enable systemd-timesyncd:

sudo apt install -y systemd-timesyncd
sudo timedatectl set-ntp true

Run timedatectl again and confirm System clock synchronized: yes.

Step 9 - Reviewing listening services

Every service that listens on a public address is attack surface. List what is listening on TCP and UDP:

sudo ss -tulpn
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
udp   UNCONN 0      0         127.0.0.54:53         0.0.0.0:*     users:(("systemd-resolve",pid=512,fd=16))
udp   UNCONN 0      0      127.0.0.53%lo:53         0.0.0.0:*     users:(("systemd-resolve",pid=512,fd=14))
tcp   LISTEN 0      4096         0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=1021,fd=3),("systemd",pid=1,fd=86))
tcp   LISTEN 0      4096            [::]:22            [::]:*     users:(("sshd",pid=1021,fd=3),("systemd",pid=1,fd=86))

Addresses on 127.0.0.x are only reachable from the server itself. On a new server you should only see SSH on a public address. If you find something you do not need, stop and disable it, for example:

sudo systemctl disable --now service_name

Only disable services you recognize. The firewall from Step 5 already blocks anything you did not explicitly allow, so this step is about keeping the server lean rather than a replacement for UFW.

Verification

Run through this checklist from a fresh terminal on your local computer and on the server:

CheckCommandExpected
Key login as your userssh sammy@your_server_ipShell, no server password prompt
Root login blockedssh root@your_server_ipPermission denied (publickey)
Firewall activesudo ufw statusStatus: active with SSH allowed
Fail2Ban runningsudo fail2ban-client status sshdJail listed
Automatic updatessystemctl status apt-daily-upgrade.timeractive (waiting)
Time syncedtimedatectlSystem clock synchronized: yes

Troubleshooting

Locked out after an SSH change: open the web console of the server, log in as your user, and review /etc/ssh/sshd_config.d/00-hardening.conf. The most common mistake is an AllowUsers line that does not include your user. Fix it, run sudo sshd -t, then sudo systemctl restart ssh.

Locked out after enabling UFW: from the web console run sudo ufw allow OpenSSH (or your custom port) and try again. sudo ufw disable is a temporary way back in, not a fix.

Fail2Ban does not start: run sudo fail2ban-client -t to test the configuration and sudo journalctl -u fail2ban -n 50 to see the error. A typo in a section name such as [sshd] is the usual cause.

You banned yourself: from the web console run sudo fail2ban-client set sshd unbanip your_ip, then add your address to ignoreip.

Conclusion

Your Ubuntu or Debian server now has a patched base system, a non-root administrator, key-only SSH, a default-deny firewall, brute-force protection and automatic security updates. From here you can change the SSH port to cut log noise, set up regular backups of your data, and install the services the server is meant to run, opening only their ports in UFW.