Security

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 sudo rights 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 for root is 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.
  • nmap on your computer. In the examples, 203.0.113.10 is a documentation address for the server and 198.51.100.7 is 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 admin

The 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 ssh

If 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.10

The 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 enable

Log 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 verbose

Open 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.conf

Log 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 ruleset

Step 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 = systemd

After 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 sshd

To 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 --debug

In 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 -tulpn

The 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 20

The 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#

  1. Login from a new session. Repeat the two commands from step 1: the key gets you in, the password does not.
  2. 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 the open state. Check the IPv6 address the same way (the -6 option) if your connection supports it. And ping 203.0.113.10 should get replies.
  3. fail2ban status. sudo fail2ban-client status sshd shows 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 disable or sudo nft flush ruleset; if SSH is the cause, remove the faulty line and run sudo 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.conf and losing the bans. flush ruleset wipes every table, including those of fail2ban and Docker; after changing the rules, run sudo 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.