All my servers are running SSH on non standard port (987) and I want to permanent block all IPs that try to connect to port 22.

I have now the rule iptables -A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -j DROP but this rule do not add IP to iptables as blocked, only current connection is drop.

Can iptables add a IP as globaly blocked (disable connection on any other port) after first connection try on port 22?

Complete rules are:

iptables -N SSH
iptables -A SSH -p tcp -m tcp --dport 22 -m state --state NEW -j DROP
iptables -A SSH -p tcp -m tcp --dport 987 -m state --state NEW -m recent --rcheck --set --rsource
iptables -A SSH -p tcp -m tcp --dport 987 -m state --state NEW -m recent --rcheck --update --seconds 300 --hitcount 4 --rsource -j DROP


(I'm using iptables v1.8.4 (legacy))

UPDATE 1: Thanks to @A.B

Connections on port 22 managed by ipset with 86400 seconds IP block (1 day).

Connections on port 987 managed by iptables with a 300 seconds block after connection 4 tries (failed or success) in 300 seconds.

ipset create ssh22 hash:ip timeout 86400

iptables -N SSH22
iptables -A SSH22 -p tcp -m tcp --dport 22 -m state --state NEW -j SET --add-set ssh22 src
iptables -A SSH22 -m set --match-set ssh22 src -j DROP

iptables -N SSH
iptables -A SSH -p tcp -m tcp --dport 987 -m state --state NEW -m recent --name ssh --set --rsource
iptables -A SSH -m recent --name ssh --update --seconds 300 --reap --hitcount 4 --rsource -j DROP

UPDATE 2: Final iptables working version:



-A INPUT -p tcp -m tcp --dport 987 -m state --state NEW -m recent --set --name ssh --rsource
-A INPUT -m recent --update --seconds 300 --reap --hitcount 4 --name ssh --rsource -j DROP

-A INPUT -p tcp -m tcp --dport 22 -j SET --add-set ssh22 src
-A INPUT -m set --match-set ssh22 src -j LOG --log-prefix "[SSH 22] " --log-level 4
-A INPUT -m set --match-set ssh22 src -j DROP

-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT

-A INPUT -p tcp -m tcp --dport 80  -j ACCEPT
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 987 -j ACCEPT

-A INPUT -i lo -p all -j ACCEPT


3 Answers 3


Using recent

recent is an abnormal match: it doesn't just filter, it's also doing changes (which is usually done in a target rather than a match): changing entries in a list. It must be used twice like you already did elsewhere: once to set the list, and once anyway to check the list. The second time shouldn't check anything else, so any connection from the IP to anything will match and be dropped, not just to port 22.

Your user chain SSH should be called from INPUT (or FORWARD) before any generic stateful rule like -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT is used.

So replace the start of your chain:

iptables -N SSH
iptables -A SSH -p tcp -m tcp --dport 22 -m state --state NEW -m recent --name banip --rsource --set -j DROP # -j DROP could be left: the next rule would do it.
iptables -A SSH -m recent --name banip --rsource --update -j DROP

You can then add your additional rules for port 987. As they don't state a list name, they'll be using DEFAULT which won't interfere with banip.

Should you want those IP addresses to expire after some time (eg 12345 s), you'd also add for example --seconds 12345 --reap after the --update parameter as documented.

By default the recent's list can accept 100 entries, so to use recent as a ban list, the xt_recent kernel module should be loaded, before loading iptables rules, with an option like ip_list_tot=65535 because a list that can have only 100 entries is probably not large enough to implement a ban list. This parameter is global for all list sizes.

You should also consider using ipset instead, as described below, because it's usually more flexible.

Using ipset

After some level of complexity is reached, better use the companion ipset framework. ipset can handle arbitrary large hashed lists of various network-related types (IP address, (TCP and/or UDP) port...).

You can do this with iptables's set match and SET target + ipset. ipset will act as memory.

Create the set with the companion tool ipset:

ipset create banip hash:ip

This will create a hashed set with 65536 entries by default, which is way better than 100 or 255. See the manual to see how to create a larger set if needed (the set's size once determined can't be increased from the packet path, only by further using the ipset command). You can pre-populate, save, restore, list contents of, etc. this set with appropriate ipset commands.

The rules above can be replaced with:

iptables -N SSH
iptables -A SSH -p tcp -m tcp --dport 22 -m state --state NEW -j SET --add-set banip src
iptables -A SSH -m set --match-set banip src -j DROP

If you want the entries to expire the easiest is to have created the set with a timeout parameter.

One of the methods for fail2ban is to use ipset (but the trigger is from log files rather than packet path).

There are yet other methods possible, like using nftables (and its built-in sets) instead of iptables + ipset. Feel free to test it.

  • My final rules, two iptables chains SSH22 and SSH, one on port 22 with ipset and other one on port 987 with default iptables block: "ipset create ssh22 hash:ip timeout 86400" "iptables -N SSH22" "iptables -A SSH22 -p tcp -m tcp --dport 22 -m state --state NEW -j SET --add-set ssh22 src" "iptables -A SSH22 -m set --match-set ssh22 src -j DROP" "iptables -N SSH" "iptables -A SSH -p tcp -m tcp --dport 987 -m state --state NEW -m recent --name ssh --set --rsource" "iptables -A SSH -m recent --name ssh --update --seconds 300 --reap --hitcount 4 --rsource -j DROP"
    – Lito
    Commented May 18, 2020 at 8:08
  • I have tested the rules, but connections IP on port 22 are never added to ipset. I have added a log rule on SSH22 chain but connections not logged to any log: pastebin.com/0kd2tAFK What's the problem with this setup?
    – Lito
    Commented May 18, 2020 at 14:20
  • Finally, it works :) pastebin.com/3vEaLJRT Thanks!
    – Lito
    Commented May 18, 2020 at 15:33
  • Even if it works, it's not as should be done. See my specific comment about the generic stateful rule (-m conntrack --ctstate ESTABLISHED...) and order of rules. blacklisting should always take effect before this rule, but you put it after it rather than before.
    – A.B
    Commented May 18, 2020 at 15:37
  • Oh, sorry, I was checking a lot of settings and don't take in mind your comment: pastebin.com/9xRpgbq9
    – Lito
    Commented May 18, 2020 at 15:45

IPtables is not able to manage that kind of dynamic analysis by itself. From your description, what you need is actually a tool that manages these kind of rules for you. Overall, I can recommend fail2ban and denyhosts for that specific case, but there might be others.

I successfully tried both with a similar use-case. You can setup different rules for different ports and protocols, rule expiration, max retries and many other options.

Official links:




I agree with @J A's answer.

I did however want to share my open-source XDP Firewall project here that should be able to achieve this. The tool utilizes XDP and should be able to block traffic a lot faster than IPTables if your NIC supports XDP-native. Otherwise, it should be the same speed as IPTables if you're using XDP-generic/SKB mode (most NICs/modern kernels should support this).

The only cons to using this program are:

  1. It uses a config file to specify filtering rules. There are no commands at the moment to add/remove filtering rules.

  2. After the program is restarted, the IP blacklist will be wiped.

I'm hoping to implement commands that'll add filter rules and save them automatically in the future along with the ability to permanently block IPs that support restarts as well.

For your case, the following config should do:

interface = "changeme";
updatetime = 15;

filters = (
        enabled = true,
        action = 0,

        tcpopts = (
                enabled = true,
                dport = 22,
                blocktime = 9999999999

This will block packets targeted against destination port 22 over TCP for 9999999999 seconds (you may increase this if need to be since the blocktime config option is a uint64_t type).

I hope this helps in your case!

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .