How to rate limit SSH with iptables

Public SSH listeners often receive repeated connection attempts even when key authentication and strong account policy are already in place. An iptables rate-limit rule can slow repeated new connections from the same source before they reach sshd, while leaving established sessions alone.

The recent match keeps a runtime list of source addresses and timestamps. A first rule drops sources that have already reached the hit threshold during the time window, and a following rule records new SSH attempts that are still below the threshold.

Use this as a host-level throttle, not as the only SSH defense. Keep a console or existing remote session open while testing, place allow rules for trusted management sources before the rate-limit rules when needed, and save the ruleset only after a new login still succeeds.

Steps to rate limit SSH with iptables:

  1. Confirm the current INPUT order before adding SSH limits.
    $ sudo iptables --list INPUT --line-numbers --numeric --verbose
    Chain INPUT (policy DROP 0 packets, 0 bytes)
    num   pkts bytes target     prot opt in     out     source               destination
    1        0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    2        0     0 ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0

    Place trusted source allows and established-connection rules before rate-limit drops. Related: How to allow an IP address with iptables
    Related: How to allow established connections with iptables

  2. Add the over-limit drop rule for new SSH attempts.
    $ sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 --name SSH --rsource -j DROP

    The example drops the fourth and later new connection attempt from the same source within 60 seconds. Adjust the window and hit count for the host's normal login pattern.

  3. Add the rule that records and accepts allowed new SSH attempts.
    $ sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSH --rsource -j ACCEPT

    The DROP rule must appear before the SET rule. Otherwise every new attempt can be accepted before the rate-limit decision is reached.

  4. Confirm the two recent rules appear in the correct order.
    $ sudo iptables -S INPUT
    -P INPUT DROP
    -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
    -A INPUT -i lo -j ACCEPT
    -A INPUT -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 4 --name SSH --mask 255.255.255.255 --rsource -j DROP
    -A INPUT -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m recent --set --name SSH --mask 255.255.255.255 --rsource -j ACCEPT
  5. Open a new SSH session from a trusted client.
    $ ssh admin@server.example.com
    Welcome to Ubuntu 26.04 LTS

    If the new login fails, keep the existing session open and remove the two recent rules by exact match before saving anything.

  6. Generate repeated test attempts from the same client.
    $ nc -vz -w 1 server.example.com 22
    Connection to server.example.com 22 port [tcp/ssh] succeeded!
    $ nc -vz -w 1 server.example.com 22
    Connection to server.example.com 22 port [tcp/ssh] succeeded!
    $ nc -vz -w 1 server.example.com 22
    Connection to server.example.com 22 port [tcp/ssh] succeeded!
    $ nc -vz -w 1 server.example.com 22
    nc: connect to server.example.com port 22 (tcp) timed out: Operation now in progress

    The exact visible failure depends on the client and network path. With DROP, over-limit attempts usually time out instead of receiving an immediate reset.

  7. Check counters on both SSH rate-limit rules.
    $ sudo iptables --list INPUT --line-numbers --numeric --verbose --exact
    Chain INPUT (policy DROP 0 packets, 0 bytes)
    num      pkts      bytes target     prot opt in     out     source               destination
    3           1        60 DROP       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 ctstate NEW recent: UPDATE seconds: 60 hit_count: 4 name: SSH side: source
    4           3       180 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 ctstate NEW recent: SET name: SSH side: source

    The accept counter should increase for early attempts, and the drop counter should increase after the threshold is reached.

  8. Save the runtime rules after a new login and counter check pass.
    $ sudo netfilter-persistent save
    run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save

    Do not save a rate limit that blocks trusted administration paths. Delete or reorder the runtime rules first, then retest.
    Related: How to save iptables rules permanently