10 of 15 server configurations in stockInstant deliveryNo setup fees

Security

Harden a new server: firewall, fail2ban and automatic updates

Close every port you do not serve, lock down SSH, ban brute-force attempts, patch automatically and avoid the firewall gaps Docker creates. Commands for Ubuntu, Debian, AlmaLinux and Rocky Linux.

6 min readUpdated October 14, 2026UbuntuDebianAlmaLinuxRocky Linux

Applies to
UbuntuDebianAlmaLinuxRocky Linux
Reading time
6 minutes
Updated
October 14, 2026

Stuck? Engineers answer 24/7

On this page

A new server is reachable from the whole internet as soon as it is online, and automated scanners find new addresses quickly. Hardening follows one rule: expose only what you actually serve, and keep it patched. Work through the steps in order, from an SSH session you keep open until every change is tested.

Avoid locking yourself out. Allow SSH before you enable a firewall, test each change from a second terminal, and remember that the IPMI console in your dashboard still works if a rule goes wrong.

Check what is listening

Shell
ss -tulpn

Each line is an open port. In the Local Address column, 0.0.0.0, [::] or * mean every interface, including the public one; 127.0.0.1 and [::1] mean local only. Stop and disable what you do not need, for example:

Shell
systemctl disable --now rpcbind.service rpcbind.socket

Services used only by your own applications should listen on localhost: bind-address = 127.0.0.1 for MySQL and MariaDB, listen_addresses = 'localhost' for PostgreSQL, bind 127.0.0.1 -::1 with protected-mode yes for Redis. Never expose databases, Redis, Memcached or Elasticsearch to the internet. If another server needs them, use the free private VLAN between your servers (on request) or a VPN, plus a firewall allow-list.

Set up a firewall

Deny incoming traffic by default and open only the ports you serve. The examples open SSH, HTTP and HTTPS.

Ubuntu and Debian: ufw

Shell
apt install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw enable
ufw status verbose

ufw writes the matching IPv6 rules as long as IPV6=yes is set in /etc/default/ufw, which is the default. To accept SSH only from your own address, add the specific rule before deleting the general one:

Shell
ufw allow from 198.51.100.7 to any port 22 proto tcp
ufw delete allow 22/tcp

If your address changes often, use ufw limit 22/tcp instead: it rejects addresses that open 6 or more connections within 30 seconds.

AlmaLinux and Rocky Linux: firewalld

Shell
dnf install -y firewalld
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --permanent --remove-service=cockpit
firewall-cmd --reload
firewall-cmd --list-all

The default public zone usually allows ssh, cockpit and dhcpv6-client; skip the cockpit line if you use its web console. Rules without a family apply to IPv4 and IPv6. To accept SSH only from your address (add a second rule with family="ipv6" if you connect over IPv6):

Shell
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.7/32" service name="ssh" accept'
firewall-cmd --permanent --remove-service=ssh
firewall-cmd --reload

On current releases, both ufw and firewalld drive nftables underneath. You can write /etc/nftables.conf by hand instead, but pick one approach: mixing a hand-written ruleset with ufw or firewalld makes the result hard to predict.

Lock down SSH

Log in with keys only. Log in with SSH keys walks you through it and creates /etc/ssh/sshd_config.d/00-keys-only.conf, which turns off password logins. Then add a few limits in a second drop-in file, /etc/ssh/sshd_config.d/10-hardening.conf. Current releases read these files before the main configuration, and the first value read wins:

Config
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no

Check the syntax, then reload (the service is sshd on AlmaLinux and Rocky Linux):

Shell
sshd -t
systemctl reload ssh

Once you log in with a sudo user such as deploy, change PermitRootLogin to no in 00-keys-only.conf. Moving SSH to another port reduces log noise, but it is not a security control.

Block brute force with fail2ban

fail2ban reads failed logins from the systemd journal and bans the source address in the firewall for a while. On AlmaLinux and Rocky Linux, enable EPEL first (dnf install -y epel-release) and install the same two packages with dnf:

Shell
apt install -y fail2ban python3-systemd

Create /etc/fail2ban/jail.local:

INI
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 198.51.100.7

[sshd]
enabled = true
backend = systemd
Shell
systemctl enable --now fail2ban
fail2ban-client status sshd

On a public server, Total failed normally starts counting within minutes. If it stays at 0, the filter does not match your logs: with OpenSSH 9.8 or later, failed logins are logged by sshd-session, which older fail2ban filters miss, so update fail2ban. To lift a ban: fail2ban-client set sshd unbanip 198.51.100.7.

Install security updates automatically

Ubuntu normally ships with unattended-upgrades enabled. On Debian, install and enable it:

Shell
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades

Both values in /etc/apt/apt.conf.d/20auto-upgrades must be "1"; preview a run with unattended-upgrade --dry-run --debug. Kernel updates need a reboot: to reboot automatically at a quiet hour, set Unattended-Upgrade::Automatic-Reboot "true"; and Unattended-Upgrade::Automatic-Reboot-Time "04:00"; in /etc/apt/apt.conf.d/50unattended-upgrades.

On AlmaLinux and Rocky Linux, install dnf-automatic:

Shell
dnf install -y dnf-automatic dnf-plugins-core

In /etc/dnf/automatic.conf, set upgrade_type = security and apply_updates = yes in the [commands] section, then enable the timer. dnf needs-restarting -r tells you when a reboot is needed:

Shell
systemctl enable --now dnf-automatic.timer
dnf needs-restarting -r

Docker bypasses ufw and firewalld

Docker writes its own iptables rules, and traffic to a published container port is forwarded before your firewall’s input rules see it. A container started with -p 6379:6379 is reachable from the whole internet, whatever ufw says. Two fixes:

  1. Publish on localhost and put a reverse proxy in front of anything public. In Compose, write "127.0.0.1:6379:6379".

    Shell
    docker run -d -p 127.0.0.1:6379:6379 redis
  2. Filter in the DOCKER-USER chain, which Docker evaluates before its own rules. Replace eth0 with your public interface (ip route show default):

    Shell
    iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 6379 --ctdir ORIGINAL -j DROP

    These rules are not saved across reboots: apply them from a systemd unit or your configuration management, and mirror them with ip6tables if containers use IPv6.

Never expose the Docker API on TCP port 2375: anyone who reaches it gets root on the host. Manage remote engines over SSH instead, for example DOCKER_HOST=ssh://deploy@server.example.com.

Time sync, audits and console access

  • Keep the clock right. TLS, two-factor codes and log correlation depend on it. timedatectl must show System clock synchronized: yes; if no time service runs, install chrony. Leave out chrony’s allow directive unless you mean to serve time: an NTP server open to the internet can be abused for amplification attacks.
  • Audit with Lynis (optional, packaged in Debian, Ubuntu and EPEL). lynis audit system prints concrete suggestions: treat them as a checklist, not a score to maximize.
  • Keep out-of-band access private. Use the IPMI console only through your dashboard, and protect your dashboard account with a unique password.

Next steps

Need a hand?

Engineers answer 24/7, with a 30-minute first-response target for critical issues.

Contact support