Hi there! In this tutorial I would like to show you how to increase server security by using iptables
as a firewall. To be honest, not many people are actually using iptables
or any firewall. I think that this is bad practice, because you they allow all traffic to go in and out. You should always limit the possible entry points to your server.
Firewalld vs iptables
Since CentOS 7, we have new tool called firewalld
. This is not actually an alternative to iptables
. firewalld
is a wrapper for iptables
. Many people say, that it's easier to use than iptables, but to be honest I believe that it's not flexible enough. Maybe I'm wrong, but I'd love to see some advanced example, how to transform iptables
rules below to firewalld
🙂 If you want to use firewalld
instead of iptables
, unfortunately you need to read different tutorial. Here is great article about firewalld from DigitalOcean.
How to install iptables on CentOS7?
Before we will install iptables
, we need to get rid of firewalld
first :
sudo yum remove firewalld -y
Next, we can install iptables:
sudo yum install iptables iptables-services -y
iptables-services
is simple script that will help us save and restore firewall rules.
Secure iptables rules for CentOS
First, let's check if there are any rules by executing following command:
sudo iptables -S
If you will get following output:
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
It means that you allow all traffic, both incoming and outgoing to your server. However if you have anything more than output above, copy it to separate file as a backup.
The easiest way of adding rules is by editing iptables rules file. Open the file, or create one if it doesn't exists:
sudo vi /etc/sysconfig/iptables
I will describe whole file line by line, but at the bottom of this post you can find whole content that I'm using for iptables.
Opening and closing tags
*filter
File must contains two indicators:
- start of the ruleset *filter
- end of the ruleset COMMIT
You need to have both in order to get iptables configured properly. Between these two lines, you can add iptables rules.
Clear all existing rules
-X
-F
-Z
At the very beginning I'd like to clear whole rules. In other words - enable all traffic. The reason is that I want to be able to execute that file over and over again, and I will always set the rules that I have in file. No other rules will be applied (for instance rules added by command line).
Allowing loopback
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
-A INPUT -d 127.0.0.0/8 -j REJECT
-A OUTPUT -d 127.0.0.0/8 -j REJECT
Next thing is to allow all loopbacks. Those are local connection and blocking them might cause errors in some connections. In addition we will block those which doesn't use lo0.
Keep established connections
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
All connections that are active now, should remain untouched. It will prevent from interruption of services.
PING command
-A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
-A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT
-A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
In most cases you will need to be able to ping server. These rules will allow two things. First - you'll be able to ping your own server. Second - you will be able to execute ping from your server. Both are usually needed and quite useful.
Protection from PING of Death attack
-N PING_OF_DEATH
-A PING_OF_DEATH -p icmp --icmp-type echo-request -m hashlimit --hashlimit 1/s --hashlimit-burst 10 --hashlimit-htable-expire 300000 --hashlimit-mode srcip --hashlimit-name t_PING_OF_DEATH -j RETURN
-A PING_OF_DEATH -j DROP
-A INPUT -p icmp --icmp-type echo-request -j PING_OF_DEATH
Ping is cool, however you might get attacked with Ping of Death attack. Here is simple protection.
Prevent some nasty attacks
-N PORTSCAN
-A PORTSCAN -p tcp --tcp-flags ACK,FIN FIN -j DROP
-A PORTSCAN -p tcp --tcp-flags ACK,PSH PSH -j DROP
-A PORTSCAN -p tcp --tcp-flags ACK,URG URG -j DROP
-A PORTSCAN -p tcp --tcp-flags FIN,RST FIN,RST -j DROP
-A PORTSCAN -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
-A PORTSCAN -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL ALL -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL NONE -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP
-A INPUT -f -j DROP
-A INPUT -p tcp ! --syn -m state --state NEW -j DROP
This is really nice piece of rules that will prevent port scanning, SYN flood attacks, invalid packages, malformed XMAS packets, NULL packets, etc.
UDP traffic
-A INPUT -p udp --sport 53 -j ACCEPT
-A OUTPUT -p udp --dport 53 -j ACCEPT
-A INPUT -p udp --sport 123 -j ACCEPT
-A OUTPUT -p udp --dport 123 -j ACCEPT
I enable usually only ports for outgoing traffic (from our server to outside world). There are two ports that I'd like to open:
- 53 - DNS port. It's a must if you want to use curl or yum. If you will have it closed, you will not resolve any domain name.
- 123 - NTP port. If you are using chrony or ntpd, you need to enable that port to allow NTP deamon synchronisation.
TCP traffic
# Open TCP ports for incoming traffic
-A INPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
-A INPUT -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
-A INPUT -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
# Open TCP ports for outgoing traffic
-A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
-A INPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
-A INPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
With TCP it's more complicated, but it's not that hard. First, you need to think what traffic you need to access from your server (outgoing traffic). I usually allow only SSH, HTTP and HTTPS traffic. Yum requires HTTP and HTTPS ports for pulling new packages. You will need it also for wget or curl. SSH is not mandatory, but if you want to pull packages from git via ssh protocol, you will need it as well.
I usually enable the same for incoming traffic. If you have httpd or nginx installed, you need to enable port 80. If you are using SSL for HTTPS, you need to enable 443 also. In addition to these two ports you must enable port 22 for SSH. If you will block this, you won't be able to get access to your server!
Block everything else
-A INPUT -j DROP
-A FORWARD -j DROP
-A OUTPUT -j DROP
At the very end, before closing COMMIT tag I add these three rules. So everything that was not specified above will be dropped. Both incoming and outgoing traffic.
How to apply rules?
There are two ways how you can apply the rules. First, save the changes in iptables file. First method is not permanent method. It's good way of testing your firewall before saving them permanently. If anything will go wrong, you can just restart the server and you will have all traffic open. Make sure that you check SSH access with these rules. Log out and try to login after applying rules.
So non permanent way of applying rules is:
sudo iptables-restore < /etc/sysconfig/iptables
Try to check rules with iptables -S to see the difference:) Check if everything is working fine. If so, you can set them permanently. After each server restart, rules will be applied automatically.
sudo systemctl start iptables.service
sudo systemctl enable iptables.service
If you want to reload rules, simply edit the file, add what you need and restart iptables service:
sudo systemctl restart iptables.service
You can use our Ansible LAMP on Steroids project to make configuration of your server easier!
It is based on Ansible. If you don't know what Ansible is, check our tutorial first.
Clone our repository and setup your server faster with LAMP on steroids.
Whole content of iptables rules
*filter
# Clear all iptables rules (everything is open)
-X
-F
-Z
# Allow loopback interface (lo0) and drop all traffic to 127/8 that doesn't use lo0
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
-A INPUT -d 127.0.0.0/8 -j REJECT
-A OUTPUT -d 127.0.0.0/8 -j REJECT
# Keep all established connections
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Allow ping
-A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
-A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT
-A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
# Protect from ping of death
-N PING_OF_DEATH
-A PING_OF_DEATH -p icmp --icmp-type echo-request -m hashlimit --hashlimit 1/s --hashlimit-burst 10 --hashlimit-htable-expire 300000 --hashlimit-mode srcip --hashlimit-name t_PING_OF_DEATH -j RETURN
-A PING_OF_DEATH -j DROP
-A INPUT -p icmp --icmp-type echo-request -j PING_OF_DEATH
# Prevent port scanning
-N PORTSCAN
-A PORTSCAN -p tcp --tcp-flags ACK,FIN FIN -j DROP
-A PORTSCAN -p tcp --tcp-flags ACK,PSH PSH -j DROP
-A PORTSCAN -p tcp --tcp-flags ACK,URG URG -j DROP
-A PORTSCAN -p tcp --tcp-flags FIN,RST FIN,RST -j DROP
-A PORTSCAN -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
-A PORTSCAN -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL ALL -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL NONE -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP
-A PORTSCAN -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP
# Drop fragmented packages
-A INPUT -f -j DROP
# SYN packets check
-A INPUT -p tcp ! --syn -m state --state NEW -j DROP
# Open ports for outgoing UDP traffic
-A INPUT -p udp --sport 53 -j ACCEPT
-A OUTPUT -p udp --dport 53 -j ACCEPT
-A INPUT -p udp --sport 123 -j ACCEPT
-A OUTPUT -p udp --dport 123 -j ACCEPT
# Open TCP ports for incoming traffic
-A INPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
-A INPUT -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
-A INPUT -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
# Open TCP ports for outgoing traffic
-A INPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
-A INPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
# Drop all other traffic
-A INPUT -j DROP
-A FORWARD -j DROP
-A OUTPUT -j DROP
COMMIT
What's next?
We secured our system with basic firewall. That will increase the security of our server. In one of the next episodes we will change the configuration of our SSH and therefore make it more secure.
As always You can use our Ansible playbook for faster provisioning of our server. You can find it on GitHub.