How to harden an Ubuntu server (SSH, nftables, fail2ban, auditd, unattended-upgrades)


A fresh Ubuntu server is not safe to expose to the internet as-is: it allows password logins, has no firewall policy, does not ban brute-force attempts, and does not patch itself. This walkthrough locks down a new Ubuntu server 22.04 or 24.04 host in seven steps. You will move SSH to key-only access, put a default-deny firewall in front of it with nftables, ban repeat offenders with fail2ban, tighten the kernel, turn on system auditing with auditd, and enable automatic security updates. Every step is copy-paste and includes a check so you know it worked.

One warning before you start: SSH and firewall changes can lock you out. Keep a second SSH session open to the server the whole time, and only close it once you have confirmed a fresh login still works.

Prerequisites

  • A fresh Ubuntu 22.04 or 24.04 server with sudo access.
  • An SSH key pair on your local machine. If you do not have one: ssh-keygen -t ed25519

Step 1: update, and create a non-root sudo user

Patch first, then create a normal user so you can stop logging in as root:

sudo apt update && sudo apt upgrade -y

sudo adduser deploy

sudo usermod -aG sudo deploy

From your local machine, install your public key for that user, then confirm you can log in as deploy:

ssh-copy-id deploy@<server-ip>

ssh deploy@<server-ip>

Do not continue until that login works. The next step disables password login, so key access has to be in place first.

Step 2: harden SSH

Ubuntu reads drop-in config from /etc/ssh/sshd_config.d/, which is cleaner than editing the main file. Create a hardening file:

sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF'

PermitRootLogin no

PasswordAuthentication no

KbdInteractiveAuthentication no

PubkeyAuthentication yes

AuthenticationMethods publickey

MaxAuthTries 3

LoginGraceTime 30

X11Forwarding no

AllowUsers deploy

ClientAliveInterval 300

ClientAliveCountMax 2

EOF

Validate the syntax before applying it, then reload:

sudo sshd -t

sudo systemctl reload ssh

If sshd -t prints nothing, the config is valid. Now open a new terminal and confirm a key login still works and that password login is refused:

ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@<server-ip>

That command should be denied. Root login should also be refused.

Step 3: put a default-deny firewall in front with nftables

Ubuntu ships nftables. Write a ruleset that drops everything inbound except loopback, established connections, ICMP at a sane rate, SSH (rate-limited), and web traffic:

sudo tee /etc/nftables.conf > /dev/null <<'EOF'

#!/usr/sbin/nft -f

flush ruleset

table inet filter {

  chain input {

    type filter hook input priority 0; policy drop;

    iif "lo" accept

    ct state established,related accept

    ct state invalid drop

    ip protocol icmp icmp type echo-request limit rate 5/second accept

    ip6 nexthdr ipv6-icmp accept

    tcp dport 22 ct state new limit rate 10/minute accept

    tcp dport { 80, 443 } accept

  }

  chain forward {

    type filter hook forward priority 0; policy drop;

  }

  chain output {

    type filter hook output priority 0; policy accept;

  }

}

EOF

Load it and enable it at boot:

sudo nft -f /etc/nftables.conf

sudo systemctl enable --now nftables

sudo nft list ruleset

The policy drop on the input chain is the important part: anything you did not explicitly allow is blocked. Confirm your open SSH session survived and a new one connects before moving on. If you do not serve web traffic, remove the port 80 and 443 line.

Step 4: ban brute-force attempts with fail2ban

fail2ban reads the logs and bans IPs that fail to authenticate repeatedly:

sudo apt install -y fail2ban

Create a local jail that watches SSH via the systemd journal and bans through nftables:

sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'

[DEFAULT]

bantime  = 1h

findtime = 10m

maxretry = 5

backend  = systemd

banaction = nftables-multiport

[sshd]

enabled = true

port = ssh

EOF

Restart and check the jail is active:

sudo systemctl restart fail2ban

sudo fail2ban-client status sshd

You will see the jail status and, over time, a list of banned IPs. backend = systemd matters on current Ubuntu because SSH logs to the journal, not a flat file.

Step 5: tighten the kernel with sysctl

A short set of network and memory hardening defaults:

sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null <<'EOF'

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

net.ipv6.conf.all.accept_redirects = 0

kernel.randomize_va_space = 2

EOF

sudo sysctl --system

The final command applies the settings and prints each one back so you can confirm they took.

Step 6: turn on system auditing with auditd

auditd records who did what, which is what you need when you are reconstructing an incident:

sudo apt install -y auditd audispd-plugins

sudo systemctl enable --now auditd

Add rules that watch the files an attacker would touch, track kernel module loading, and then lock the rule set:

sudo tee /etc/audit/rules.d/hardening.rules > /dev/null <<'EOF'

-w /etc/passwd -p wa -k passwd_changes

-w /etc/shadow -p wa -k shadow_changes

-w /etc/sudoers -p wa -k sudoers_changes

-w /etc/ssh/sshd_config -p wa -k sshd_config

-a always,exit -F arch=b64 -S init_module -S delete_module -k modules

-e 2

EOF

sudo augenrules --load

sudo auditctl -l

Test it by touching a watched file and searching the audit log:

sudo touch /etc/sudoers.d/zzz-test && sudo rm /etc/sudoers.d/zzz-test

sudo ausearch -k sudoers_changes -i | tail

Note that -e 2 makes the rules immutable until the next reboot. That is deliberate: an attacker cannot quietly disable auditing. If you need to edit rules, you have to reboot first.

Step 7: enable automatic security updates

Unpatched software is the most common way in. Turn on unattended security upgrades:

sudo apt install -y unattended-upgrades

sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null <<'EOF'

APT::Periodic::Update-Package-Lists "1";

APT::Periodic::Unattended-Upgrade "1";

EOF

Confirm what it would do without applying anything yet:

sudo unattended-upgrade --dry-run --debug

By default it installs security-pocket updates only. If you want it to reboot automatically after a kernel update, add these two lines to /etc/apt/apt.conf.d/50unattended-upgrades and pick a quiet hour:

Unattended-Upgrade::Automatic-Reboot "true";

Unattended-Upgrade::Automatic-Reboot-Time "03:00";

What goes wrong

Locked out of SSH. Almost always one of three things: you disabled password auth before the key worked, you reloaded a config that failed sshd -t, or the nftables ruleset has no rule allowing port 22. Keep a second session open and test every change from a third one before trusting it.

nftables rules vanish after reboot. You loaded them with nft -f but did not enable the service. sudo systemctl enable --now nftables makes /etc/nftables.conf load at boot.

fail2ban never bans anyone. The usual cause is the wrong backend. On current Ubuntu, SSH logs go to the journal, so backend = systemd is required. If SSH runs on a non-standard port, set it in the [sshd] jail.

Cannot edit audit rules. That is -e 2 doing its job. Reboot, edit, reload, re-lock.

Auto-reboot at a bad time. If you enable automatic reboot, set the time to a maintenance window. A production box rebooting at noon because of a kernel update is its own kind of incident.

BlueGrid.io Content Team

Three people pose together against a plain white background. The woman on the left is smiling with her hand on her hip, while the two men beside her stand closely, one in a hoodie and the other in a plaid shirt.

BlueGrid.io Content Team

BlueGrid.io Team is an editorial collective of engineers, practitioners, and contributors sharing insights across technology, operations, company culture, and the people behind the systems. Content is created through interviews, hands-on experience, internal collaboration, and editorial review, reflecting both how systems are built and how teams work together in real-world environments.

Share this post

Share this link via

Or copy link