{"id":2242,"date":"2019-02-13T11:51:49","date_gmt":"2019-02-13T09:51:49","guid":{"rendered":"https:\/\/upcloud.com\/global\/us\/resources\/tutorials\/iptables-firewall-recent-triggering-ipset\/"},"modified":"2019-02-13T11:51:49","modified_gmt":"2019-02-13T09:51:49","slug":"iptables-firewall-recent-triggering-ipset","status":"publish","type":"tutorial","link":"https:\/\/upcloud.com\/global\/resources\/tutorials\/iptables-firewall-recent-triggering-ipset\/","title":{"rendered":"How to setup iptables firewall effortlessly using &#8216;recent&#8217; triggering and ipset"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">In this article are shown example instructions for a simple firewall disallowing new connections if the remote host initiates a connection too often. This is especially the case with SSH brute force attacks. Most administrators know the feeling of annoyance when they look at the system security logs and notice the enormous amounts of failed SSH logins.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What makes the described solution effortless is that there is no need to install and configure a separate log watcher daemon \u2013 just bring in some firewall rules. The setup might be most useful in the context of security-sensitive hosts where additional security measures would be justified, even more so if, for some reason, strong (e.g. key based) authentication can\u2019t be used.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/signup.upcloud.com\/\">Test hosting on UpCloud!<\/a><\/div>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Description<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This article describes deploying IPv4 address bans based on how often a remote client tries to connect to a specific port, using&nbsp;<em>netfilter<\/em>\u2018s&nbsp;<tt>iptables<\/tt>&nbsp;and&nbsp;<tt>ipset<\/tt>&nbsp;tools.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Matching lists of addresses or networks by using just\u00a0iptables\u00a0is indeed messy because\u00a0iptables\u00a0itself does not support matching multiple separate addresses or networks in one rule. This means that every checked address or network would need its own rule in the ruleset. Such complicated and long rulesets bring administration and performance concerns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With&nbsp;<tt>ipset<\/tt>&nbsp;a list of addresses (or networks, etc.) can be matched from one rule. Performance considerations such as indexing the address set make matching and lookups a lot more efficient.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>The example is as on CentOS 7. Similar should be achievable on other systems too.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setup process<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">First, the needed tools should be installed:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">yum install iptables iptables-services ipset ipset-service<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">All offending IPv4 addresses are going to be saved, open the following file for edit:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">vi \/etc\/sysconfig\/ipset-config<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And set&nbsp;<tt>IPSET_SAVE_ON_STOP=\"yes\"<\/tt>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The&nbsp;<tt>ipset<\/tt>&nbsp;in this example is created with the&nbsp;<em>timeout<\/em>&nbsp;parameter which makes the set\u2019s entries expire. Without the timeout, the entries will last until removed by hand.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ipset create sshin_bans hash:ip timeout 3600\nservice ipset save\nsystemctl start ipset\nsystemctl enable ipset<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Insert desired ruleset into&nbsp;<tt>\/etc\/sysconfig\/iptables<\/tt>. Following is a simple example ruleset:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">*filter\n:INPUT ACCEPT [0:0]\n:FORWARD ACCEPT [0:0]\n:OUTPUT ACCEPT [0:0]\n:SSHIN - [0:0]\n\n# 1\n-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n-A INPUT -i lo -j ACCEPT\n-A INPUT -p tcp -m tcp --dport 22 -s 10.0.0.0\/12 -m conntrack --ctstate NEW -j ACCEPT\n\n# 2\n-A INPUT -m set --match-set sshin_bans src -j DROP\n\n# 3-4\n-A INPUT -p icmp -j ACCEPT\n-A INPUT -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -j SSHIN\n\n# 5 Block direct SSH bruteforce\n-A SSHIN -m recent --set --name bruteforce\n-A SSHIN -m recent --update --seconds 3600 --hitcount 5 --name bruteforce -j LOG --log-level info --log-prefix \"SSH blocked: \"\n-A SSHIN -m recent --update --seconds 3600 --hitcount 5 --name bruteforce -j SET --add-set sshin_bans src\n-A SSHIN -m recent --update --seconds 3600 --hitcount 5 --name bruteforce -j DROP\n-A SSHIN -j ACCEPT\n\n# 6 if you want to filter ports:\n#-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT\n#-A INPUT -j REJECT --reject-with icmp-host-prohibited\n#-A FORWARD -j REJECT --reject-with icmp-host-prohibited\n\nCOMMIT<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Explanation of the above ruleset:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Established, localhost and new SSH connections from your private network are allowed right away.<\/li>\n\n\n\n<li>The&nbsp;<tt>sshin_bans<\/tt>&nbsp;address list (<tt>set<\/tt>) is checked. If the source address is found from the set the connection is immediately dropped (i.e. no connections at all, to any port, are allowed from that specific address).<\/li>\n\n\n\n<li>ICMP is allowed.<\/li>\n\n\n\n<li>New SSH (port 22) connections are directed to SSHIN chain for further evaluation.<\/li>\n\n\n\n<li>SSH connection counting and processing magic:\n<ul class=\"wp-block-list\">\n<li>an iptables&nbsp;<tt>recent<\/tt>&nbsp;table named&nbsp;<tt>bruteforce<\/tt>&nbsp;is checked\/updated for the packet source address.<\/li>\n\n\n\n<li>The\u00a0<tt>recent<\/tt>\u00a0table is inspected:\n<ul class=\"wp-block-list\">\n<li>if during 3600 seconds (1 hour) there are 5 or more hits to the rule, then log, add the address to&nbsp;<tt>sshin_bans<\/tt>ipset, and drop the new connection;<\/li>\n\n\n\n<li>otherwise, accept the connection.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Optional filter rules.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Then enabling the ruleset:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">systemctl start iptables\niptables-restore \/etc\/sysconfig\/iptables\nservice iptables save\nsystemctl enable iptables<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In this&nbsp;setup, the ban list is persistent as the set is saved upon shutdown but the bans expire after 1 hour. Blocked addresses can be delisted manually by using the following command:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ipset del sshin_bans <span style=\"color: #ff0000;\">172.16.6.10<\/span><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This removes an address from the set and allows that host to connect again, provided it does not trigger a new ban from&nbsp;<tt>iptables<\/tt>\u2018s&nbsp;<tt>recent<\/tt>&nbsp;match.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Honeypotting<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With <tt>ipset<\/tt> and the <tt>SET target<\/tt> it\u2019s easy to catch also those hosts which try to connect to a port with no daemon listening on it. If there supposedly was nothing listening on port 25, just by having a rule such as&nbsp;<tt>-A INPUT -p tcp --dport 25 -j SET --add-set honeypot_victims src<\/tt>&nbsp;adds the remote host to the&nbsp;<tt>honeypot_victims<\/tt>&nbsp;set when the offending host matches the chain \u2013 i.e. the remote host tried to connect to port 25.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To enable this honeypot, add the rule to your <tt>\/etc\/sysconfig\/iptables<\/tt> file.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-A INPUT -p tcp --dport 25 -j SET --add-set honeypot_victims src<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then create the honeypot_victims jail using ipset, save the config and restart the software.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ipset create honeypot_victims hash:ip timeout 3600\nservice ipset save\nsystemctl restart ipset\nsystemctl restart iptables<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It should be clear anyway that one got to know what they are doing when using this kind of setup. There might be innocent errors that make clients connect to the wrong host or port and denying access on the basis of so primitive heuristic might open up the chance for denial of service.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The access control method shown in this article is lighter and more streamlined because it does not need a separate log watcher daemon (e.g. fail2ban). Another good thing is that&nbsp;Netfilter&nbsp;is usually readily available and in use anyway, so accommodating a few more rules to the ruleset does not require much extra effort.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is not, however, a universal solution. The real downside is that Netfilter counts only incoming connections, not failed logins, and therefore it could be easier to accidentally lock oneself out or accidentally cause a small denial of service.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Still, the principal solution for denying access to abusive hosts as described in this article has been tried and tested by the author for a considerable length of time. Even on a small-scale multiuser system, there has only been a tiny amount of bans for legitimate users over several years (one or two cases) but obviously, this is not a large-scale multiuser solution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the presented example solution wasn\u2019t suitable, a better and more intelligent solution might be&nbsp;<a href=\"https:\/\/www.sshguard.net\/\" target=\"_blank\" rel=\"noopener\"><em>sshguard<\/em><\/a>&nbsp;which has the ability to catch offenders during a longer timespan. sshguard can feed IP addresses into an&nbsp;<tt>ipset<\/tt>&nbsp;set too.<\/p>\n","protected":false},"author":18,"featured_media":27437,"comment_status":"open","ping_status":"closed","template":"","community-category":[259,253],"class_list":["post-2242","tutorial","type-tutorial","status-publish","has-post-thumbnail","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/2242","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial"}],"about":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/types\/tutorial"}],"author":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/users\/18"}],"replies":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/comments?post=2242"}],"version-history":[{"count":0,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/2242\/revisions"}],"wp:attachment":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/media?parent=2242"}],"wp:term":[{"taxonomy":"community-category","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/community-category?post=2242"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}