Securing your Linux server
Securing a Linux server is the first step before any production deployment. A freshly created VPS with a public IP address receives automated SSH login attempts within minutes: bots constantly scan the Internet for weak passwords, misconfigured services and unpatched software. Here are the essential measures, in the order I apply them on a new machine.
The goal is not to make the server impregnable, which does not exist, but to reduce the attack surface and make opportunistic attacks ineffective. The examples below target Debian and Ubuntu; the principles apply to every distribution.
Preparing a user and an SSH key
Before touching the SSH configuration, create an unprivileged user and check that you can log in with a key. This is the step that keeps you from locking yourself out. On your workstation, generate an Ed25519 key if you do not have one, then copy it to the server:
# On the server (as root)
adduser deploy
usermod -aG sudo deploy
# On your workstation
ssh-keygen -t ed25519 -C "deploy@myserver"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@203.0.113.10
ssh deploy@203.0.113.10
Only move on once key-based login works with the new user, and check that it can use sudo.
SSH configuration
# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Port 2222
MaxAuthTries 3
AllowUsers deploy
Each directive has a specific role:
PermitRootLogin no: the root account, present on every machine, is the first target of attacks. Administrators log in with their own account and then usesudo, which leaves a named audit trail.PasswordAuthentication no: without passwords, dictionary attacks become pointless. Also setKbdInteractiveAuthentication no, otherwise password authentication may remain possible through PAM.Port 2222: changing the port does not protect against a targeted attack, but it greatly reduces the noise in the logs.MaxAuthTries 3: limits the number of attempts per connection.AllowUsers deploy: an allowlist of accounts permitted to log in. Any other user is rejected, even with a valid key.
On recent distributions, you can put these directives in a dedicated file, for example /etc/ssh/sshd_config.d/hardening.conf, rather than editing the main file. Be careful: for each directive, sshd keeps the first value it reads, and included files are read at the beginning. Before restarting, always test the configuration and keep your current session open:
sudo sshd -t && sudo systemctl restart ssh
# From a second terminal, before closing the current session:
ssh -p 2222 deploy@203.0.113.10
On Ubuntu 24.04, SSH is socket-activated by systemd: after changing the port, run sudo systemctl daemon-reload then sudo systemctl restart ssh.socket so the new port takes effect.
Firewall with UFW
The firewall principle is simple: deny all incoming traffic, then explicitly open only the ports you need. UFW (Uncomplicated Firewall) is a readable layer on top of nftables/iptables, installed by default on Ubuntu.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
Order matters: allow the SSH port before running ufw enable, otherwise your current connection may be cut. Then check the rules with sudo ufw status verbose. Two useful options:
sudo ufw limit 2222/tcpreplaces theallowrule and denies an address that attempts 6 or more connections within 30 seconds.sudo ufw allow from 10.0.0.0/24 to any port 5432 proto tcpopens a port only for a private network, for example for a database.
An important trap: Docker bypasses UFW. A port published with -p 5432:5432 is reachable from the Internet even if UFW does not allow it, because Docker writes its own iptables rules. Publish internal ports on 127.0.0.1 (-p 127.0.0.1:5432:5432) or do not publish them at all.
Automatic updates
Most compromises exploit known vulnerabilities that have already been fixed. The unattended-upgrades package installs security updates automatically:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
By default, only security updates are applied. Some of them, kernel updates in particular, only take effect after a reboot. You can allow an automatic reboot at an off-peak time in /etc/apt/apt.conf.d/50unattended-upgrades:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
To check the configuration without installing anything: sudo unattended-upgrade --dry-run --debug. On a server that must not reboot on its own, leave this option disabled and watch for the /var/run/reboot-required file.
Fail2Ban
Protect yourself against brute-force attacks:
sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Fail2Ban reads the logs, spots repeated authentication failures and temporarily bans the offending IP address through the firewall. The jail.conf file can be overwritten by updates: always work in jail.local. Rather than copying the whole file, it is often enough to write only the values you want to override:
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
[sshd]
enabled = true
port = 2222
backend = systemd is required on Debian 12, where SSH logins are logged to journald rather than to /var/log/auth.log. Remember to set the custom SSH port, otherwise the ban would apply to port 22. A few useful commands:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 198.51.100.7
Reducing the attack surface
Every service listening on the network is a potential door. List the open ports and disable anything that is not needed:
sudo ss -tulpn
sudo systemctl disable --now cups.service # example: a useless service on a server
Databases, Redis and admin interfaces should listen on 127.0.0.1 or on a private network, never on the public interface. A few kernel parameters also harden the network stack:
# /etc/sysctl.d/99-hardening.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
Apply them with sudo sysctl --system.
Logs and auditing
A secure server must also let you understand what happened after the fact. Make the systemd journal persistent (Storage=persistent in /etc/systemd/journald.conf), and ship the logs to a separate machine: an attacker who gains root access can wipe local logs. For regular checks, the Lynis tool (sudo lynis audit system) reviews hundreds of configuration points and suggests concrete improvements.
Security checklist
- Disable root login over SSH
- Use key-based authentication only
- Configure a restrictive firewall
- Install and configure Fail2Ban
- Enable automatic security updates
- Set up centralised logging
- Put system monitoring in place
- Check open ports with
ss -tulpnand the ports published by Docker - Set up off-site backups and test restoring them
Security is an ongoing process, not a one-off configuration: run an audit again after every significant change and keep access to the strict minimum.