Basic Linux server hardening: SSH, firewall and updates
Contents
For an administrator who has received a dedicated server with Ubuntu LTS or Debian and wants to close the usual security gaps within an hour. The result: key-only SSH login, only the ports you need open to the outside, and security updates that install themselves.
What you will need#
- Ubuntu 24.04 LTS or 26.04 LTS, or Debian 13 (commands checked in October 2026).
- A user with
sudorights and key-based login. On our servers this user is created during installation and named after the distribution (debian,ubuntu), the password arrives by email, and password login forrootis disabled. A public key passed in the order comment is added to this user straight away. See also the first hour on a Linux server and SSH keys. nmapon your computer. In the examples,203.0.113.10is a documentation address for the server and198.51.100.7is the address you connect from.
Warning. A mistake in the SSH or firewall settings can cut you off from the server. Do not close your current session until you have tested login from a new terminal window: an open session survives the change and lets you fix everything.
Step 1 SSH: keys only, no root, only the users you need#
On Ubuntu and Debian, /etc/ssh/sshd_config starts by including the files in /etc/ssh/sshd_config.d/, and the first value found for each parameter wins. Our Linux images are set up by cloud-init, so it may hold a cloud-init file (on Ubuntu, 50-cloud-init.conf) with a PasswordAuthentication setting; so create a file that is read before it (sudo nano /etc/ssh/sshd_config.d/00-hardening.conf) and replace admin with the name you log in as (for example, debian):
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AllowUsers adminThe first two lines turn off password login. The third bars root from SSH with a key as well (password login was already disabled at installation). The fourth admits only the listed users: anyone not listed cannot log in, even with a key. For several administrators a group is handier: sudo groupadd sshusers, sudo usermod -aG sshusers admin and AllowGroups sshusers instead of AllowUsers. Do not set both parameters together, or a user has to pass both checks.
Check the syntax and the effective values, then apply the changes:
sudo sshd -t
sudo sshd -T | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|allowusers) '
sudo systemctl reload sshIf sshd -T shows a different value, it is set in a file read before yours. Now open a new terminal window on your computer and run:
ssh admin@203.0.113.10
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password admin@203.0.113.10The first command should let you in with your key; the second should end with Permission denied (publickey).
Should you change the SSH port?#
Honestly, it is not protection: scanners find SSH on any port, and a non-standard port only reduces the log noise. If you change it, do so after steps 1 and 2: open the new port in the firewall, add Port 2222 to your file, run sudo sshd -t and restart the service: on Debian with sudo systemctl restart ssh, on Ubuntu 24.04 and 26.04 (where systemd listens on the port) with sudo systemctl daemon-reload and sudo systemctl restart ssh.socket. Only after logging in with -p 2222 from a new window, close port 22 and add port = 2222 to the [sshd] section of the fail2ban settings (step 3).
Step 2 Firewall: incoming denied except what you need#
Deny all incoming connections except the ports that really must be reachable from the internet; allow outgoing ones. Ubuntu has ufw for this, Debian has nftables. Pick one.
Do not block ping (ICMP echo-request) completely: the data centre checks that the server is up by pinging it, and without a reply the monitoring will treat the server as down. The rules must cover IPv6 too: the server gets a block of addresses (usually a /64), and on fresh Linux images the first of them is configured at installation. Both examples take care of this: ufw lets ping through by default and applies its rules to IPv6 as well, and the nftables inet table covers both protocols.
Warning. Add the SSH rule before you enable the firewall, and schedule an automatic rollback first: if something goes wrong, the rules remove themselves in five to six minutes. Cancel the rollback once you have tested from a new window.
Ubuntu: ufw#
If you changed the SSH port, use it instead of 22. When ufw warns that SSH connections may be disrupted, answer y: the SSH rule is already there.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo systemd-run --on-active=5m --unit=fw-rollback /usr/sbin/ufw disable
sudo ufw enableLog in from a new terminal window. If it works, cancel the rollback and review the rules:
sudo systemctl stop fw-rollback.timer
sudo ufw status verboseOpen other ports as needed, for example sudo ufw allow 80,443/tcp for a website.
Debian: nftables#
Install the package if it is missing (sudo apt install nftables), keep a copy of the default file (sudo cp /etc/nftables.conf /etc/nftables.conf.bak) and replace the contents of /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
meta l4proto { icmp, ipv6-icmp } accept
tcp dport 22 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}The ICMP line is needed for monitoring, diagnostics and IPv6 to work. For a website, add tcp dport { 80, 443 } accept. Check the file without applying it, schedule the rollback and load the rules:
sudo nft -c -f /etc/nftables.conf
sudo systemd-run --on-active=5m --unit=fw-rollback /usr/sbin/nft flush ruleset
sudo nft -f /etc/nftables.confLog in from a new window. If it works, cancel the rollback and enable loading the rules at boot:
sudo systemctl stop fw-rollback.timer
sudo systemctl enable nftables
sudo nft list rulesetStep 3 fail2ban for sshd#
The fail2ban service reads the log and temporarily bans addresses that failed login attempts come from. Install it: sudo apt install fail2ban nftables. In the Ubuntu 24.04, 26.04 and Debian 13 packages the sshd jail is enabled at once: it reads the systemd journal and bans through nftables, so it needs the nft tool even when ufw manages the firewall. Do not edit the packaged .conf files; put your own values in /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
backend = systemdAfter five failed attempts within ten minutes the address is banned for an hour. The line backend = systemd repeats the default of these packages; on Debian 12 it is needed together with the python3-systemd package.
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status sshdTo unban an address by hand: sudo fail2ban-client set sshd unbanip 198.51.100.7.
Step 4 Automatic security updates#
The unattended-upgrades package installs security updates on its own. On Ubuntu it is usually already enabled; on Debian, install, enable and check it:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
sudo unattended-upgrade --dry-run --debugIn 20auto-upgrades both APT::Periodic parameters must equal "1"; the last command is a trial run that changes nothing. Put your own settings not in the packaged 50unattended-upgrades file but in a separate file next to it, 52unattended-upgrades-local. Kernel updates take effect only after a reboot, and by default the server does not reboot itself. If a few minutes of downtime at night is acceptable, allow the reboot in your file:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";If not, reboot by hand when the file /var/run/reboot-required appears.
Step 5 As few open services as possible#
sudo ss -tulpnThe addresses 0.0.0.0, [::] or * in the local address column mean the service listens on all interfaces, including the public one; 127.0.0.1 and [::1] mean it is reachable only from the server itself. Databases and internal web interfaces should listen on the local address only, even when the firewall closes the port. In PostgreSQL this is set by listen_addresses = 'localhost', in MariaDB and MySQL by bind-address = 127.0.0.1. Connect to them through an SSH tunnel (ssh -L 5432:127.0.0.1:5432 admin@203.0.113.10), WireGuard or the private network. Stop and disable a service you do not need: sudo systemctl disable --now example.service.
Step 6 Logs: who logged in and who tried#
sudo journalctl -u ssh --since today
sudo journalctl -u ssh --since "7 days ago" | grep Accepted
sudo journalctl -u ssh --since today | grep -Ei 'invalid|failed'
last -n 20The journal works everywhere, while last (login history) exists out of the box only on Ubuntu 24.04. On Debian 13 and Ubuntu 26.04 it comes back with sudo apt install wtmpdb libpam-wtmpdb; lastb is gone there, and the third command shows failed attempts.
How to check the result#
- Login from a new session. Repeat the two commands from step 1: the key gets you in, the password does not.
- Scan your own ports from outside. On your computer, not on the server, run
nmap -Pn -p- 203.0.113.10. Only the ports you opened deliberately should be in theopenstate. Check the IPv6 address the same way (the-6option) if your connection supports it. Andping 203.0.113.10should get replies. - fail2ban status.
sudo fail2ban-client status sshdshows failed-attempt counters and banned addresses.
If you have locked yourself out#
- The old session is still open: fix things from it with
sudo ufw disableorsudo nft flush ruleset; if SSH is the cause, remove the faulty line and runsudo systemctl reload ssh. - You scheduled a rollback: wait five to six minutes and log in again.
- fail2ban has banned you: wait an hour, or log in from another address and unban yourself.
- Nothing has helped: contact United Cloud technical support, available 24/7: phone +38 044 206 08 08, email info@united.net.ua. On request, support will give you the remote console (the link works only from the address you specify) or boot the server into rescue mode, a temporary Linux system with SSH access where you can mount the disk and fix the settings. Lite may have no console, so the timed rollback matters most there.
Common mistakes#
- Docker and ufw. Ports published by a container bypass ufw rules. Publish internal services on the local address only:
-p 127.0.0.1:5432:5432. - Reloading
nftables.confand losing the bans.flush rulesetwipes every table, including those of fail2ban and Docker; after changing the rules, runsudo systemctl restart fail2ban.
What next#
- Network-level DDoS protection is included with every server, but it does not cover application-level attacks. During an attack a filter at the network edge switches on by itself: simple rules by address and port, without tracking connection state; on most lines support can also switch it on for you (on Lite it may be unavailable). It does not replace the server firewall.
- Backups: the 3-2-1 rule and restic for Linux.