Skip to content

Instantly share code, notes, and snippets.

@eusonlito
Last active January 1, 2024 11:36
Show Gist options
  • Save eusonlito/2919bd89b870343bff93b7c9f476579c to your computer and use it in GitHub Desktop.
Save eusonlito/2919bd89b870343bff93b7c9f476579c to your computer and use it in GitHub Desktop.
Strong iptables and ipset protection

Protect your server with a strong iptables rules and ipset lists.

1. Install ipset to manage ipstables lists

apt install ipset

2. Install iptables-persistent to preserve iptables rules on reboot

apt install iptables-persistent

3. Create ipset lists

# iptables fails on real SSH port will be blocked 24 hours
ipset create ssh-real hash:ip timeout 86400

# iptables INPUT connections on default SSH port will be blocked forever
ipset create ssh hash:ip

# iptables INPUT connections on FTP port will be blocked forever
ipset create ftp hash:ip

# iptables INPUT connections on MySQL, PostgreSQL and MongoDB ports will be blocked forever
ipset create mysql hash:ip
ipset create postgresql hash:ip
ipset create mongodb hash:ip

# iptables INPUT connections on Redis port will be blocked forever
ipset create redis hash:ip

# iptables INPUT connections on Plesk and WHM/cPanel ports will be blocked forever
ipset create plesk hash:ip
ipset create cpanel hash:ip

# register rules
ipset save -file /etc/iptables/ipset

4. Paste the rules.v4 contents into file /etc/iptables/rules.v4

  • Replace the XXX.XXX.XXX.XXX ip with a safe to connect IP, it will be your lifeguard.
  • Replace the port 987 with your real SSH port (always different than 22).

5. Test the iptables rules with iptables-apply

iptables-apply -t 60 /etc/iptables/rules.v4

6. Apply rules

iptables-restore < /etc/iptables/rules.v4

7. Check ipset status

ipset list

8. Persistent ipset

Create file /etc/systemd/system/ipset-persistent.service with:

[Unit]
Description=ipset persistent configuration
Before=network.target

# ipset sets should be loaded before iptables
# Because creating iptables rules with names of non-existent sets is not possible
Before=netfilter-persistent.service
Before=ufw.service

ConditionFileNotEmpty=/etc/iptables/ipset

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ipset restore -file /etc/iptables/ipset
ExecStop=/usr/sbin/ipset save -file /etc/iptables/ipset
ExecStop=/usr/sbin/ipset flush
ExecStopPost=/usr/sbin/ipset destroy

[Install]
WantedBy=multi-user.target

RequiredBy=netfilter-persistent.service
RequiredBy=ufw.service

Enable service with

systemctl daemon-reload
systemctl enable ipset-persistent.service

Thanks to https://selivan.github.io/2018/07/27/ipset-save-with-ufw-and-iptables-persistent-and.html

9. Install a script to geolocate blocked IPs

https://gist.github.com/eusonlito/5afa0d42f1aff3cd2b82a8c0a8a3b75d

10. Flush iptables rules

The default INPUT rule is DROP, then you can not flush rules with iptables -F or your access will be blocked forever.

You need to execute:

iptables -P INPUT   ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT  ACCEPT
iptables -F
iptables -X
[Unit]
Description=ipset persistent configuration
Before=network.target
# ipset sets should be loaded before iptables
# Because creating iptables rules with names of non-existent sets is not possible
Before=netfilter-persistent.service
Before=ufw.service
ConditionFileNotEmpty=/etc/iptables/ipset
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ipset restore -file /etc/iptables/ipset
ExecStop=/usr/sbin/ipset save -file /etc/iptables/ipset
ExecStop=/usr/sbin/ipset flush
ExecStopPost=/usr/sbin/ipset destroy
[Install]
WantedBy=multi-user.target
RequiredBy=netfilter-persistent.service
RequiredBy=ufw.service
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
# Always allow this IP
-A INPUT -s XXX.XXX.XXX.XXX/32 -j ACCEPT
# Accept any related or established connections
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Allow all traffic on the loopback interface
-A INPUT -i lo -j ACCEPT
# Allow input PING
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Drops invalid incoming packets
-A INPUT -m conntrack --ctstate INVALID -j DROP
# Block real SSH port after 5 attempts in 5 minutes
-A INPUT -m set --match-set ssh-real src -j DROP
-A INPUT -p tcp --dport 987 -m state --state NEW -m recent --set --name ssh-real --rsource
-A INPUT -m recent --update --seconds 300 --hitcount 5 --rttl --name ssh --rsource -j SET --add-set ssh-real src
-A INPUT -m set --match-set ssh-real src -j LOG --log-prefix "[FIREWALL] [SSH-REAL] " --log-level 4
-A INPUT -m set --match-set ssh-real src -j DROP
# Block all attempts on default SSH port 22
-A INPUT -m set --match-set ssh src -j DROP
-A INPUT -p tcp -m tcp --dport 22 -j SET --add-set ssh src
-A INPUT -m set --match-set ssh src -j LOG --log-prefix "[FIREWALL] [SSH] " --log-level 4
-A INPUT -m set --match-set ssh src -j DROP
# Block all attempts on default FTP port 21
-A INPUT -m set --match-set ftp src -j DROP
-A INPUT -p tcp -m tcp --dport 21 -j SET --add-set ftp src
-A INPUT -m set --match-set ftp src -j LOG --log-prefix "[FIREWALL] [FTP] " --log-level 4
-A INPUT -m set --match-set ftp src -j DROP
# Block all attempts on default MySQL port 3306
-A INPUT -m set --match-set mysql src -j DROP
-A INPUT -p tcp -m tcp --dport 3306 -j SET --add-set mysql src
-A INPUT -m set --match-set mysql src -j LOG --log-prefix "[FIREWALL] [MYSQL] " --log-level 4
-A INPUT -m set --match-set mysql src -j DROP
# Block all attempts on default PostgreSQL port 5432
-A INPUT -m set --match-set postgresql src -j DROP
-A INPUT -p tcp -m tcp --dport 5432 -j SET --add-set postgresql src
-A INPUT -m set --match-set postgresql src -j LOG --log-prefix "[FIREWALL] [POSTGRESQL] " --log-level 4
-A INPUT -m set --match-set postgresql src -j DROP
# Block all attempts on default MongoDB port 27017
-A INPUT -m set --match-set mongodb src -j DROP
-A INPUT -p tcp -m tcp --dport 27017 -j SET --add-set mongodb src
-A INPUT -m set --match-set mongodb src -j LOG --log-prefix "[FIREWALL] [MONGODB] " --log-level 4
-A INPUT -m set --match-set mongodb src -j DROP
# Block all attempts on default Redis port 6379
-A INPUT -m set --match-set redis src -j DROP
-A INPUT -p tcp -m tcp --dport 6379 -j SET --add-set redis src
-A INPUT -m set --match-set redis src -j LOG --log-prefix "[FIREWALL] [REDIS] " --log-level 4
-A INPUT -m set --match-set redis src -j DROP
# Block all attempts on Plesk related ports 8443 and 8880
-A INPUT -m set --match-set plesk src -j DROP
-A INPUT -p tcp -m tcp -m multiport --dports 8443,8880 -j SET --add-set plesk src
-A INPUT -m set --match-set plesk src -j LOG --log-prefix "[FIREWALL] [PLESK] " --log-level 4
-A INPUT -m set --match-set plesk src -j DROP
# Block all attempts on WHM/cPanel related ports 2082,2083,2086,2087,2089
-A INPUT -m set --match-set cpanel src -j DROP
-A INPUT -p tcp -m tcp -m multiport --dports 2082,2083,2086,2087,2089 -j SET --add-set cpanel src
-A INPUT -m set --match-set cpanel src -j LOG --log-prefix "[FIREWALL] [CPANEL] " --log-level 4
-A INPUT -m set --match-set cpanel src -j DROP
# Accept input on open ports
-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
COMMIT
# iptables fails on real SSH port will be blocked 24 hours
ipset create ssh-real hash:ip timeout 86400
# iptables INPUT connections on default SSH port will be blocked forever
ipset create ssh hash:ip
# iptables INPUT connections on FTP port will be blocked forever
ipset create ftp hash:ip
# iptables INPUT connections on MySQL, PostgreSQL and MongoDB ports will be blocked forever
ipset create mysql hash:ip
ipset create postgresql hash:ip
ipset create mongodb hash:ip
# iptables INPUT connections on Redis port will be blocked forever
ipset create redis hash:ip
# iptables INPUT connections on Plesk and WHM/cPanel ports will be blocked forever
ipset create plesk hash:ip
ipset create cpanel hash:ip
# register rules
ipset save -file /etc/iptables/ipset
@knzcx
Copy link

knzcx commented Sep 23, 2022

I have a question and I'm not quite sure if it's even possible. My webserver is behind an ELB and I get the user IP via X-Forward-For header. So I can block IPs and it works with:

iptables -A INPUT -p tcp --dport 80 -m string --algo bm --string 'X-Forwarded-For: 123.123.123.123' -j DROP

This way I can block individual IPs without any problems. However, I would like to block all IPs in an ipset "blacklist" (list is updated daily).

root@37b22f39a8bf:/var/www/html# ipset list
Name: blacklist
Type: hash:ip
Revision: 4
Header: family inet hashsize 4096 maxelem 65536
Size in memory: 136
References: 1
Number of entries: 217
Members:
37.220.36.240
138.94.75.17
199.249.230.120
199.249.230.123
138.68.94.5
35.240.137.176
43.158.217.52
114.7.200.107
221.0.90.52
more...

But when I add an IP to my blacklist, it is ignored by iptables.

I have set up the following rule for my blacklist on a test basis

root@37b22f39a8bf:/var/www/html# iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination         
DROP all -- anywhere anywhere match-set blacklist src

Chain FORWARD (policy ACCEPT)
target prot opt source destination         

Chain OUTPUT (policy ACCEPT)
target prot opt source destination 

I guess the XFF header IP is ignored because iptables only gets the ELB IP by this rule and does not look into the XFF header.

Abstractly described I need something like this:
iptables -A INPUT -p tcp --dport 80 -m string --algo bm --string 'X-Forwarded-For: blacklist' -j DROP
I want iptables to check the XXF header against my blacklist and drop the IP if there is a match.

It would really be a blessing if someone could help me or even just tell me that it doesn't work this way or has another idea how I can implement this to just pass IP lists to iptables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment