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 ed25519as 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.
ImportantKeep your current SSH session open until you have confirmed, from a second terminal, that you can still log in after each SSH or firewall change.
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 usesudoinstead.PasswordAuthentication noandKbdInteractiveAuthentication no: only keys are accepted.MaxAuthTries 3andLoginGraceTime 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.
NoteIf you later change the SSH port, allow the new port in UFW before restarting SSH.
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:
| Check | Command | Expected |
|---|---|---|
| Key login as your user | ssh sammy@your_server_ip | Shell, no server password prompt |
| Root login blocked | ssh root@your_server_ip | Permission denied (publickey) |
| Firewall active | sudo ufw status | Status: active with SSH allowed |
| Fail2Ban running | sudo fail2ban-client status sshd | Jail listed |
| Automatic updates | systemctl status apt-daily-upgrade.timer | active (waiting) |
| Time synced | timedatectl | System 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.
