Security

Advanced UFW rate limiting to prevent brute force attacks

Use ufw limit and custom before.rules entries to cap connection rates on SSH and mail ports, so password-guessing bots get blocked before they can try thousands of logins.

IntermediateLinux server6 min read

UFW rate limiting stops a single IP address from opening new connections faster than a threshold you set. In practice that means an SSH password-guessing bot gets dropped after a handful of attempts, while your own open session keeps working. This guide adds limit rules to SSH and mail ports, shows you how to lower the default threshold, and verifies that blocked attempts land in the firewall log.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux/Rocky 8–9, with sudo access.
  • Working SSH key login, plus access to the browser console in your VPS provider's control panel as a fallback. Rate limiting can lock you out if you reconnect too fast.
  • Your own admin IP address, so you can whitelist it first.

Check the current rules before changing anything:

Bash
sudo ufw status verbose
sudo ufw status numbered

Whitelist your admin IP so your own reconnects are never counted:

Bash
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'admin SSH'

Warning: If you apply a limit rule to port 22 and then lose access, open the browser console in your VPS provider's control panel and either delete the rule or add an allow rule for your current IP. Do not reboot the server while you are locked out of SSH.

How ufw limit works

ufw limit tracks new connections per source IP. When a source exceeds the threshold — six new connections in 30 seconds in current UFW versions — further new connections from that IP are denied. Existing sessions are not touched, so an open SSH shell or a long-running transfer survives.

Two details matter:

  • Only new connections are counted. A single SSH session opening many channels still counts as one connection.
  • The rule covers IPv4 and IPv6 by default. UFW adds a v6 rule automatically unless you restrict the family.

Confirm the threshold for your version, since it has changed between releases:

Bash
man ufw

Tip: ufw limit is a blunt per-IP brake, not an authentication check. Pair it with key-only SSH and Fail2ban for real brute-force protection.

Add UFW rate limiting to SSH

Add the rule:

Bash
sudo ufw limit 22/tcp comment 'SSH rate limit'

Expected output:

Rule added
Rule added (v6)

If you use a non-standard SSH port, substitute it:

Bash
sudo ufw limit 2222/tcp comment 'SSH rate limit'

Check that the rule is listed and the order is sane:

Bash
sudo ufw status numbered

You should see something like LIMIT IN 22/tcp for both v4 and v6. UFW evaluates rules top-down and stops at the first match, so an earlier ALLOW IN 22/tcp rule makes your limit rule dead weight. Remove or reorder it if that happens — see Delete, insert and reorder UFW rules safely.

Rate-limit other exposed ports

The same rule type works on any TCP port that accepts logins:

Bash
sudo ufw limit 587/tcp comment 'mail submission rate limit'
sudo ufw limit 465/tcp comment 'SMTPS rate limit'
sudo ufw limit 2222/tcp comment 'alternate SSH'

Warning: Mail submission ports are shared by real users. An office behind one NAT gateway presents a single source IP, and busy clients can trip a 6-per-30-seconds limit. If you see legitimate mail clients timing out, whitelist that network with sudo ufw allow from 198.51.100.0/24 to any port 587 proto tcp instead of limiting it.

Do not rate-limit ports 80 and 443 with ufw limit. Browsers open many parallel connections per page and you will block normal visitors. Handle HTTP floods at the web server layer instead — see Implementing advanced rate limiting in Nginx using limit_req.

Custom thresholds with before.rules

When six connections in 30 seconds is too generous, add your own rule using the kernel's recent module. Back up the file first:

Bash
sudo cp /etc/ufw/before.rules /etc/ufw/before.rules.bak

Open it:

Bash
sudo nano /etc/ufw/before.rules

Inside the *filter section, above the final COMMIT line, add:

# SSH brute-force rate limit: drop a source after 4 new connections in 60 seconds
-A ufw-before-input -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSH_LIMIT
-A ufw-before-input -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 --name SSH_LIMIT -j DROP

Save, then reload:

Bash
sudo ufw reload

The first line records each new connection from a source IP in the SSH_LIMIT list. The second drops the connection once that IP has four entries inside a 60-second window. Adjust --seconds and --hitcount to taste.

Warning: A syntax error in before.rules stops the firewall from reloading. Keep the backup file, test with sudo ufw reload, and if it fails restore with sudo cp /etc/ufw/before.rules.bak /etc/ufw/before.rules && sudo ufw reload.

Newer UFW builds may use an nftables backend, where the recent match is not available. If your reload fails, check your distribution's UFW documentation for the backend in use, or stick with ufw limit plus Fail2ban for per-IP banning.

Check it works

Turn on logging if it is off:

Bash
sudo ufw logging on
sudo tail -f /var/log/ufw.log

From a second machine, open a burst of TCP connections:

Bash
for i in $(seq 1 10); do nc -z -w1 203.0.113.10 22; done

In the log you should see entries such as:

[UFW BLOCK] IN=eth0 OUT= SRC=198.51.100.25 DST=203.0.113.10 PROTO=TCP SPT=51234 DPT=22

If you used the recent module, inspect the tracked list directly:

Bash
cat /proc/net/xt_recent/SSH_LIMIT

A source IP that keeps failing appears here with a rising hit count. Details on reading these lines are in Read and tune UFW firewall logs.

Troubleshooting

  • You cannot reconnect over SSH. Your own rapid reconnects tripped the limit. Use the browser console in your VPS provider's control panel, add sudo ufw allow from <your-ip> to any port 22 proto tcp, or remove the rule with sudo ufw delete limit 22/tcp.
  • Rule added but nothing is blocked. An earlier ALLOW rule for the same port matches first. Run sudo ufw status numbered and delete or reorder the allow rule.
  • Couldn't load match 'recent' on reload. The xt_recent kernel module is missing or the nftables backend is active. Try sudo modprobe xt_recent; if that fails, revert to ufw limit and add Fail2ban.
  • Legitimate users behind one NAT IP get blocked. Raise --hitcount and --seconds in before.rules, or whitelist that subnet for the port.
  • IPv6 attacks still succeed. Check for a matching v6 rule in sudo ufw status verbose; see UFW and IPv6: make sure your rules cover both.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.