The Linux Page

How to setup a Linux firewall

Firewall prevent unwanted accesses to connected computers.

Introduction

If you setup a Linux box, you want to setup a firewall before you connect your computer to the Internet. If you are setting up a remote server, it should only have the SSH port open. Connect to it, setup the fire, then only install the other servers and open ports as required (and only ports that need to be connected from the outside.)

Any port that you open without the firewall already setup is at risk. You may want to install PHP and along will come a database which may open a port to the Internet. Something that you just don't want to happen.

Setup Firewall

To prepare your setup, follow these simple steps:

# Become root
sudo su -
Password: ***

# Go to network settings
cd /etc/network

# Create rule files
vim ip4tables.rules
vim ip6tables.rules
# (see below for editing)

# Create a firewall script to setup the rules
vim firewall

# Change the firewall mode
chmod 755 firewall
# (you may want to use 700 instead, and 600 for ip?tables.rules)
/etc/network/firewall

# Edit the interfaces file
vim interfaces

Unfortunately, you will have to learn how to setup iptables rules. That being said, I have a few example here. One important thing, you do NOT want to use ESTABLISH and RELATED the wrong way and that's very easy to do that...

ip4tables.rules

The IPv4 and IPv6 rules are very similar. Some features are only available in one or the other and of course the IPv4 uses IP addresses in IPv4 format and IPv6 uses addresses in IPv6 format.

To convert IP addresses, you may install the ipv6calc tool. It is not really that practical, but it works as expected.

sudo apt-get install ipv6calc
ipv6calc --in ipv4addr 192.168.0.1 --out ipv6addr --action conv6to4
2002:c0a8:1::

Note: The ip6tables does not expect the square brackets we often see around IPv6 addresses.

Rules in iptables are checked one after another. The first rule that has an ACCEPT, a DROP, or a REJECT stops the process. Other rules such as a LOG and a RETURN let the process continue.

To open a port so someone from the outside can access your server, you want to add an ACCEPT rule. Say your computer has IP address 10.0.0.1 and you want to allow connections to an Apache server:

-A INPUT -i eth0 -p tcp -m tcp --dport 80 -d 10.0.0.1 --syn -j ACCEPT

Once a user connected to your server, the following packets will not match this rule anymore. Instead you need a rule that allows already connected users to continue to send packets. However, before you do such a thing, you have to block connections to any other TCP port. This is done like this:

# Whatever the port, any other NEW connections are rejected immediatly
-A INPUT -i eth0 -p tcp -m state --state NEW -m tcp -d 10.0.0.1 -j REJECT

So any other connections on any other port (say port 22 or 25) are rejected.

Now we can allow further packets to go through with an ESTABLISHED state:

-A INPUT -i eth0 -p tcp -m state --state ESTABLISHED -m tcp ! --syn -d 10.0.0.1 -j ACCEPT

This last rule is dangerous since an established connection on any TCP port is considered acceptable for any other connection to happen. In other words, with the previous REJECT rule with --state NEW, this newer rule would fail to prevent connections to your other ports (including port 22 and 25...)

Not only that, in many cases other documentations precognize to use RELATED as well. RELATED is useful only if you know exactly what you are doing. The one example given in the iptables documentation is the FTP protocol which has one control connection and one data connection. The data connection can be viewed as a RELATED connection and allowed if the user already has a connection to your FTP server. However, the truth is that is safe only if you specify the port:

# Our FTP server
-A INPUT -i eth0 -p tcp -m state --state NEW -m tcp -d 10.0.0.1 --dport 21 -j ACCEPT
# Out FTP data server
-A INPUT -i eth0 -p tcp -m state --state NEW,RELATED -m tcp -d 10.0.0.1 --dport 20 -j ACCEPT
# Prevent any other connections
-A INPUT -i eth0 -p tcp -m state --state NEW -m tcp -d 10.0.0.1 -j REJECT
# Allow more data to flow through existing connections
-A INPUT -i eth0 -p tcp -m state --state ESTABLISHED -m tcp ! --syn -d 10.0.0.1 -j ACCEPT
# Reject anything else
-A INPUT -i eth0 -p all -j REJECT

Note that this is relatively safe since port 20 will be your FTP server and it only talks FTP. Only people who are already connected on the control port can also connect to the data port. However, that does not make the FTP protocol any safer. Instead, users should look into another protocol altogether (HTTP, SFTP, SSH...)

So really ESTABLISHED,RELATED that we see everywhere does not make sense. If it is already ESTABLISHED, how can it also not be related?

[my old sample below contradict this statement, I still have to update all of that...]

Allowing Connections to Another Device

So you have an Internet connection to your Main Server and a LAN connection to a Device. For example, your main server is a standard server running Linux and the device is a Raspberry Pi.

What you'd like to do is offer another user to connect directly on a server running on your Raspberry Pi.

This can be done using the Firewall allowing a really fast connection and always in place too (opposed to a form of proxy or tunnel using SSH which can stop working).

In this case we need a few rules to re-route the incoming and outgoing traffic on the Main Server. The Raspberry Pi, as far as it's concerned, will not know whether the packets are coming from the Main Server or the Internet. Also, since the Raspberry Pi is behind your firewall, it is not as huge as priority to have a firewall setup on it. (It would be safer, though, depends what you do with it).

As you will see, this includes many forward, prerouting and postrouting to make it all work. Some is to allow port 8080 and the rest is to let the Raspberry connect to the Internet as if it were directly connected to the provider router.

*filter

# Accept established connections in both directions
-A INPUT -i eno2 -m conntrack -s 192.168.2.121 -d 192.168.2.1 --ctstate ESTABLISHED,RELATED -j ACCEPT

# Forward data for existing connections with 192.168.2.121
-A FORWARD -i eno1 -o eno2 -p tcp -m state --state ESTABLISHED,RELATED -m tcp ! -s 192.168.2.1 -d 192.168.2.121 ! --syn -j ACCEPT
-A FORWARD -i eno1 -o eno2 -p udp -m state --state ESTABLISHED,RELATED -m udp ! -s 192.168.2.1 -d 192.168.2.121 -j ACCEPT

# Forward port 8080 when it is a new connection
-A FORWARD -i eno1 -o eno2 -p tcp --syn --dport 8080 -m conntrack --ctstate NEW -j ACCEPT

# All packets from 192.168.2.121 get forwarded to eno1
-A FORWARD -i eno2 -o eno1 -p tcp -m tcp -s 192.168.2.121 -j ACCEPT
-A FORWARD -i eno2 -o eno1 -p udp -m udp -s 192.168.2.121 -j ACCEPT

# I also like to control the output (double protection!)
-A OUTPUT -o eno2 -p tcp -m tcp -s 192.168.2.1 -d 192.168.2.121 -j ACCEPT
-A OUTPUT -o eno2 -p udp -m udp -s 192.168.2.1 -d 192.168.2.121 -j ACCEPT

*nat
# Allow conections from the outside using port 8080 to go to 192.168.2.121
-A PREROUTING -i eno1 -p tcp --dport 8080 -j DNAT --to-destination 192.168.2.121
# Actually send the 8080 packets to 192.168.2.121
-A POSTROUTING -o eno2 -d 192.168.2.121 -p tcp --dport 8080 -j SNAT --to-source 192.168.2.1
# Allow other traffic to go to the Internet from the Raspberry
-A POSTROUTING -o eno1 -s 192.168.2.121 -j SNAT --to-source 10.1.10.2

Running a Public UDP Service

If you do run any public UDP server on your system, you may want to consider blocking ports 0 to 1023 because replies to those ports are likely used for amplificiation attacks.

Here is an example of such a rule:

-A INPUT -i eno1 -p udp -m udp --sport 0:1023 -j DROP

The eno1 is the publicly facing interface. Remove that if you want to block such traffic on all interfaces.

Running a DNS

If you do run a DNS on your server, first make sure you block ports 0 to 1023 as source ports. Those are not required as source. (See previous entry)

Further, you may want to block some requests that include a name which is not legal on your machine. One such name is peacecorps.gov which I block with the following rule:

-A INPUT -i eno1   -p udp -m udp --dport 53
    -m string --hex-string "|0A|peacecorps|03|gov|" --algo bm -j DROP

The rule uses an hexadecimal string because we have to specify the length of the string. Notice that the domain name periods are not included.

Also, make sure you use a length in hexadecimal. The number is between 1 and 255 in hex. Here I show a domain name with 10 characters so I used 0x0A for the size.

Old Firewall Entry

There is probably a way to do that "cleanly". There is a good community description of the iptables command line. However, they tell you to auto-load and auto-save, or use the command line and then save the tables. And they give you a script to do this and that. I prefer my simple solution (he! he!).

What I do is create a file named iptables (and ipv6tables) under /etc/network (seems reasonable.) And in there I type in the command line options that I want to use. This means if you get an error, you don't have to retype and or move the cursor slowly between command line options, you use your preferred editor, like vim for instance (best editor ever, right?!)

A line in that iptables file really simply looks like the command line of the iptable tool when used in your console. For instance, to allow nothing else than port 80 connections and assuming your computer is on network 192.168.0.0, do this:

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]

# Allow the local network (totally open)
-A INPUT -i lo -s 192.168.1.1 -j ACCEPT
-A INPUT -i lo -s 192.168.2.1 -j ACCEPT
-A INPUT -i lo -s 192.168.3.1 -j ACCEPT
... (repeat as many times as you have local networks) ...
-A INPUT -i lo -s 127.0.0.1 -j ACCEPT
-A INPUT -i lo -j REJECT

# Allow external connections to Apache
-A INPUT -i eth0 -p tcp -m tcp --dport 80 -d 192.168.1.1 --syn -j ACCEPT
-A INPUT -i eth0 -p tcp -m state --state ESTABLISHED,RELATED -m tcp ! --syn -d 192.168.1.1 -j ACCEPT
-A INPUT -i eth0 -p all -j REJECT
WARNING
Note that I do not say that will work for you. There is no guarantee here that this is correct in any way. Especially, the eth0 entry is what most people end up using. You may need to use eth1, eth2, bg0, bg1, or whatever else that you happen to have on your system. I suggest you read the documentation of iptables a few times before thinking that what you've done is right.

You may find it useful to allow your BIND and email servers too. But this is not part of this How To. Once you have a "good" setup (one that iptables gobbles), add rules as necessary. In general, it is a very good idea to start with close to no rules and add only what you need so your servers and clients work.

But ping does not work anymore?! Ah! Yes. The Unix tool called ping requires the icmp protocol to be open. I personally like to have it open. This is what you need to add before the REJECT line in the previous example:

-A INPUT -i eth0 -p icmp -j ACCEPT

For good security (at least to my point of view) it is a good idea to stop illegal addresses from connecting. This means at least all the addresses that are reserved for local networks (Intranet). The following shows what to do:

:bad_tcp_packets - [0:0]

-A INPUT -j bad_tcp_packets
-A FORWARD -j bad_tcp_packets

-A bad_tcp_packets -i eth0 -s 192.168.0.0/16 -j LOG --log-prefix "bad_tcp_packet: " --log-uid
-A bad_tcp_packets -i eth0 -s 192.168.0.0/16 -j DROP
-A bad_tcp_packets -i eth0 -s 10.0.0.0/8 -j DROP
-A bad_tcp_packets -i eth0 -s 172.16.0.0/12 -j DROP

In general, the FORWARD chain should never get such bad packets. But you are never too secure, are you?

Next I setup my OUTPUT rules. These are very similar, just use OUTPUT instead of INPUT and add the few entries you need and at the end of the chain use a LOG and DROP or REJECT. It is important to setup the OUTPUT because you may want to block some internal computers from accessing the outside. Also, that will give you accounting of all the traffic going around. You may see 1,000 hits on your Apache server, and notice 10,000 output packets. This is normal, but you wouldn't know if you hadn't any OUTPUT rules. The accounting also includes how many bytes are being transferred. This grows to Gb pretty quickly on my server.

The FORWARD chain is different from the INPUT and OUTPUT (IMPORTANT: See more information about IP forwarding below. Especially how to make it work on older system as the kernel would turn it OFF by default.) It actually requires a NAT entry. And just in case you did not know yet, this allows you to plug X computers on a server and have that server forward the packets from these X computers to the Internet (or whatever network.) It is much better, if you can, to have two NICs in this case. One NIC goes to the Internet and one NIC goes to your Intranet. Otherwise, it is difficult to protect the network as well as I can do it... First you enter rules in the FORWARD chain like for INPUT and OUTPUT. At first, you can leave that all empty since in general the FORWARD chain is safer than the other two. Make sure that the FORWARD is working, then protect it.

The NAT entry is similar to what we've seen so far. You may have noticed the '*filter' at the very beginning of the descriptions of this chapter. This means you are defining the filters. (Yeah, I know.) Filters will block packets, but they won't actually do anything to allow packet forwarding. That requires the '*nat' entries.

*nat
:POSTROUTING - [0,0]
-A POSTROUTING -o eth0 -s 192.168.2.2  -j SNAT --to-source 192.168.1.1
-A POSTROUTING -o eth0 -s 192.168.2.3  -j SNAT --to-source 192.168.1.1
-A POSTROUTING -o eth0 -s 192.168.2.4  -j SNAT --to-source 192.168.1.1
...

Obviously, you need to change the IP addresses to match your network. Yet, this will tell the server to forward packets from 192.168.2.2 (and .3, .4) to 192.168.1.1 which is the server or gateway in this case.

Alright! Now that you have typed all the firewall rules, you need to COMMIT them. Add the word COMMIT on a line by itself. This means "make all these rules I just added current". If an error occurred while installing these rules, then the COMMIT fails and your firewall is not modified. Here the problem is if you reboot and there is an error. NO RULE WILL BE ADDED TO YOUR FIREWALL. So, please try to remember that each time you change a rule, you MUST run iptables on that file to make sure that, if it ever happens, on next reboot it works (how often do you reboot Ubuntu anyway?! On my end one instance did last a little over 300 days, but because the kernel gets updates, you are generally bound to reboot relatively often anyway...)

One last note about security, you should NOT use addresses 1, 2, 3, 4, etc. in order. Instead, choose whatever random number which is still available (i.e. 199, 201, 57, etc.) This makes it harder for hackers to penetrate any one of these systems, just in case. If the firewall works well anyway, it should not be a problem.

Using ipset

iptables comes with extensions and one of them is ipset. It supports:

  • IPs
  • ports
  • MAC addresses
  • networks (IP/mask)

and mixes of these (i.e. you can match the source and destination IP addresses against one ipset).

All of these items can be used by one rule.

The number of items in a set is limited but it will not go as slow as the normal iptables because the match implementation searches its tables very quickly (using hashes and such). It also allows for optimized memory allocations compared to the standard iptables rules.

ipset Example with Ports

As far as the firewall is concerned, the rule to allow a connection to one of your TCP services is going to be the same 99% of the time, except for the port. My one main exception is SSH which I like to protect a little more, say with a static IP address.

If you have a very few services, you may want to consider using the `multiport` extension. This means the list of ports remain in the memory managed by iptables. If the number of services is rather large, then you probably want to create one iptables rule and one ipset port bitmap.

To do so, first create the ipset. We will name it service_ports.

sudo ipset create service_ports bitmap:port range 1-1020

Then add an INPUT rule that looks like this:

sudo iptables -A INPUT -p tcp -m tcp --syn \
      -m set --match-set service_ports src \
      -j ACCEPT

At any time after you created the `service_ports` set, you can add ports to it:

# Websites
sudo ipset add service_ports 80
sudo ipset add service_ports 443

# Mail
sudo ipset add service_ports 25
sudo ipset add service_ports 993

# FTP
sudo ipset add service_ports 20
sudo ipset add service_ports 21

...

Now your users can access your FTP, HTTP and Mail services.