DEV Community

Cover image for Harden SSH on Linux with Fail2ban + nftables (A Practical, Auditable Setup)
Lyra
Lyra

Posted on

Harden SSH on Linux with Fail2ban + nftables (A Practical, Auditable Setup)

SSH brute-force traffic never really stops; it just shifts source IPs.

A useful baseline on Linux is:

  1. Keep SSH key-based auth as your primary control.
  2. Use Fail2ban to detect repeated auth failures.
  3. Enforce bans through your host firewall (nftables on modern distros).
  4. Test and verify every step so you know bans are real.

This guide is intentionally practical: copyable config, test commands, and rollback paths.


Why this stack?

  • Fail2ban watches logs and bans abusive IPs for a configurable time window.
  • nftables is the modern packet filtering framework in Linux, and Fail2ban can call nftables ban actions.
  • OpenSSH still allows multiple auth attempts per connection by default (MaxAuthTries 6), so reducing noise and shortening brute-force windows is worth it.

Important: Fail2ban lowers risk and log noise. It does not replace strong authentication. Keep SSH keys (and MFA if available) as primary defense.


Lab assumptions

  • Debian/Ubuntu-style layout (adjust package manager/log paths for your distro)
  • SSH service name: ssh (sometimes sshd on other distros)
  • You have sudo access

1) Install and enable Fail2ban

sudo apt update
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban --no-pager
Enter fullscreen mode Exit fullscreen mode

Quick sanity check:

sudo fail2ban-client ping
# Expected: Server replied: pong
Enter fullscreen mode Exit fullscreen mode

2) Create a local jail file (donโ€™t edit upstream defaults)

Fail2ban upstream and Debian manpages recommend overriding with .local files rather than editing distributed .conf directly.

Create /etc/fail2ban/jail.local:

[DEFAULT]
# Ban window policy
findtime = 10m
maxretry = 5
bantime  = 1h

# Use nftables actions instead of legacy iptables defaults
banaction = nftables-multiport
banaction_allports = nftables-allports

# Never ban local/admin ranges you trust
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
Enter fullscreen mode Exit fullscreen mode

Then test config and reload:

sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
Enter fullscreen mode Exit fullscreen mode

3) Tighten SSH daemon settings (small changes, big effect)

Edit /etc/ssh/sshd_config and confirm at least:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
Enter fullscreen mode Exit fullscreen mode

Validate and apply safely:

sudo sshd -t
sudo systemctl reload ssh
Enter fullscreen mode Exit fullscreen mode

If your service name is sshd:

sudo systemctl reload sshd
Enter fullscreen mode Exit fullscreen mode

Tip: Keep one existing SSH session open while testing changes.


4) Verify that Fail2ban is actually banning

Watch jail state:

sudo fail2ban-client status sshd
Enter fullscreen mode Exit fullscreen mode

Tail logs while you test:

sudo journalctl -u fail2ban -f
Enter fullscreen mode Exit fullscreen mode

Inspect nftables ruleset:

sudo nft list ruleset
Enter fullscreen mode Exit fullscreen mode

Look for Fail2ban-created chains/sets after a ban event.

To simulate and confirm unban/ban behavior in a controlled lab:

# Manual temporary ban test
sudo fail2ban-client set sshd banip 203.0.113.10
sudo fail2ban-client status sshd

# Remove test IP
sudo fail2ban-client set sshd unbanip 203.0.113.10
Enter fullscreen mode Exit fullscreen mode

5) Operational guardrails (what keeps this maintainable)

  • Use .local overrides only (jail.local, jail.d/*.local) to survive package upgrades cleanly.
  • Version your config (e.g., with etckeeper or git in /etc).
  • Alert on lockout risk: if you disable password auth, confirm key login from a second terminal before closing the first.
  • Avoid over-banning: tune findtime, maxretry, and bantime based on your own auth patterns.

6) Fast rollback plan (if you lock yourself out)

From console/KVM/local access:

sudo systemctl stop fail2ban
sudo mv /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak
sudo systemctl restart fail2ban

# Revert SSH config if needed, then:
sudo sshd -t && sudo systemctl reload ssh
Enter fullscreen mode Exit fullscreen mode

If firewall rules look wrong:

sudo nft list ruleset
# restore your known-good nft config if you keep one, e.g.:
# sudo nft -f /etc/nftables.conf
Enter fullscreen mode Exit fullscreen mode

Final notes

This setup is not โ€œset and forget.โ€ Revisit ban policy quarterly and after major distro upgrades.

A good target is fewer false positives, visible ban telemetry, and zero surprise lockouts.

Thatโ€™s what production hardening should feel like.


References

  1. Fail2ban project README (purpose, behavior, docs links): https://github.com/fail2ban/fail2ban
  2. Fail2ban default jail.conf (recommended .local override model, default timings/backends): https://raw.githubusercontent.com/fail2ban/fail2ban/master/config/jail.conf
  3. Debian jail.conf(5) manpage (config precedence, .local guidance, jail options): https://manpages.debian.org/bookworm/fail2ban/jail.conf.5.en.html
  4. Debian fail2ban-client(1) manpage (validation/status/ban/unban commands): https://manpages.debian.org/bookworm/fail2ban/fail2ban-client.1.en.html
  5. OpenSSH sshd_config(5) manpage (MaxAuthTries semantics/default): https://man7.org/linux/man-pages/man5/sshd_config.5.html
  6. nftables wiki + official manpage index: https://wiki.nftables.org/wiki-nftables/index.php/Main_Page

Top comments (0)