A practical iptables firewall in Linux

Posted on October 30, 2007 
Filed Under Networking, Unix

“We reserve the right to refuse service to anyone,” and iptables is how we do it. Rather than spend a lot of time trying to explain how ipchains begat iptables, let’s jump in. There are enough comments in the code so that everything will make sense, even if we skip much of the theory.

For the purpose of the examples, I’m going to pretend that my home cable modem is 172.16.0.128 and my remote server, the one running the iptables firewall, is at 192.168.1.132. These are fake, non-routing, example addresses. You MUST replace them with real working addresses.

Our first entry, our first iptables rule, ensures that I can continue to access the remote server using ssh, even if I mess up something while we’re experimenting. The # prompt indicates we are logged in as the root user:

# iptables -A INPUT -s 172.16.0.128 -d 192.168.1.132 \

   -p tcp --dport 22 -j ACCEPT

This is one line wrapped or folded by the backslash character. (That is ususally a safe way to enter a multi-line command.)

Let’s take it one field at a time. The -A INPUT and -j ACCEPT are the chain and the target… well, let’s skip those for now. We recognize the source address (-s) because I told you it would be 172.16.0.128 — and you can replace that with the address of your machine that needs remote access, which would be your home IP.

The destination address (-d) is also one we mentioned, 192.168.1.132 so again, you would replace it with the machine you are going to be protecting behind your firewall. That means we can fill in some blanks in our explanation:

-A INPUT  - append this rule to the INPUT chain

-s        - Source Address (replace with your home IP)

-d        - Destination Address (this server)

-p        - Protocol - TCP, UDP, or ICMP; SSH uses TCP.

--dport   - Destination Port; SSH uses port 22

-j ACCEPT - jump to the target named ACCEPT

As you might expect, ACCEPT means this packet is “OK” and may proceed to its original destination. If everthing matches, ACCEPT will be the target of the -j (”jump!”) command.

Now we’re getting somewhere! Source is INPUT, destination is destination, protocol — ? “SSH uses TCP/IP…” We can really take that on faith for the moment; suffice it that iptables can recognize, allow, or block any of the three major forms of Internet traffic. Two of those, TCP and UDP, are also associated with “port numbers” as well as with “IP addresses” so we will need a way to address ports as well as IP addresses.

Why are we specifying a destination port but not a source port? Because we want to access (or protect) a specific port on our server, port 22, no matter what port is being used as the source port on the machine that is trying to reach us.

That should do it. Any port on the home machine that wants to talk TCP to port 22 on the server is tagged as “okay” by this rule. In short, any rules we add after this one should not block us from being able to log in again to fix our mistakes.

(”Rules we add after this one….” Oooh, ominous!)

Now we can examine iptables in more depth, and even indulge in some hands-on experimentation. Remember: to err is human, but you really get the power to hose things up when you log in as root. You will be doing things below that CAN seriously impact your online life, so proceed slowly and use a local test machine with a local console, if at all possible.

Quick refresher: netfilter is a feature of the kernel, either compiled in or loaded as a kernel module. As such, it never actually stops running. It can, however, run with a rule set that does not do anything except allow packets to flow freely.

iptables is a command that allows you to talk to netfilter (and thus the kernel) and tell it to add or delete rules. The rules reside in kernel memory. If you flush the ruleset, or reboot, then “anything goes.” That is the default situation and it looks like this (-L means “list”):

# iptables -L
Chain INPUT (policy ACCEPT)

target     prot opt source               destination

Chain FORWARD (policy ACCEPT)

target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)

target     prot opt source               destination

So, what’s a chain? It’s just a set of rules.

INPUT - rules for traffic TO this server;
FORWARD - rules for traffic that will be passed along (e.g. if this box serves as a firewall);
OUTPUT - rules for traffic coming OUT of this server (e.g. to the Internet).

Netfilter/iptables is about controlling the flow of packets. A chain has a policy which is a default target, and it also has rules; each rule also has a target. When a packet matches a rule, the packet goes to the rule’s target. If no rule applies, the packet goes to the default target, specified by the chain’s policy, which as you can see above, has a default value of ACCEPT.

Targets include:

ACCEPT - Proceed to your destination;
REJECT - Go back to where you came from;
DROP - Pretend you never got here.

As you can see, the default set of policies simply says “ACCEPT” for all three chains, and there are no rules.

So our example above shows the flow: a packet comes in, hence it is following the INPUT chain, we then do a test, of its protocol, addresses, and ports, and if the packet matches what we are looking for, we jump to a target called ACCEPT. Does it sound overly complex? The thing is, the first coupld of generations of this filtering methodology needed to be overhauled soon after they were introduced. This time, the developers took a step back and designed a more robust, flexible system that would stand up to changing requirments. This one’s built to last!

Talking to iptables

You can enter an iptables command from the command line. That’s a good way to try it out, after you’ve made it failsafe as we did above. Below, each line that begins with “iptables” is one command. The saved file won’t say “iptables,” it will just begin with “-A” (add a rule). You may have an “iptables shell script” that is just a bunch of iptables commands, or you can have a file of iptables rules. On a RedHat system, the default file is called “/etc/sysconfig/iptables” and when you “start” iptables, by default it will look there for rules to load. You can also save an updated set of rules to that file at any time using the script /etc/init.d/iptables save (this is no longer the default method for Debian, however).

If you have never saved any rules, that file may not exist yet. That’s okay; don’t panic.

# /etc/init.d/iptables start Starting iptables [OK]

When you “stop” iptables, all rules in memory are flushed:

# /etc/init.d/iptables stop Stopping iptables [OK]

Digging a little deeper

To understand what iptables is actually doing, we need to look briefly at how TCP/IP works. Don’t worry, I’ll be gentle.

We already touched on the idea there are three kinds of Internet traffic; TCP and UDP, which are layers on top of IP, hence the name “TCP/IP”; and there is also ICMP, best known as a “ping.”

Every TCP/IP connection involves a three-way “handshake”:

NEW

Server1 connects to Server2 issuing a SYN (Synchronize) packet.

RELATED

Server 2 receives the SYN packet, and then responds with a SYN-ACK (Synchronize Acknowledgment) packet.

ESTABLISHED

Server 1 receives the SYN-ACK packet and then responds with the final ACK (Acknowledgment) packet.

After this 3-way handshake is complete, the traffic is now ESTABLISHED. It’s important that iptables can determine which of these three “states” a packet is in. This can be represented by three rules:

iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

iptables -A FORWARD -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT

iptables -A OUTPUT -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

Note how the last rule allows NEW traffic to leave the server, even before it is “established.” Someone has to be able to do this, or no one would ever be able to connect to anyone else!

This is the standard setup for a workstation, where you initiate all connections. Let’s go ahead and add these three rules to our default config.

We have to go one step further if we also want to offer a “service,” and allow an unknown outside address to initiate a connection into our server. In that case, you probably want one or more of these:

SSH:  (Allow remote access to this service from anywhere):

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 22 -j ACCEPT
Sendmail/Postfix: (ditto)

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 25 -j ACCEPT

(But see below for "how to change iptables rules"!)

FTP: (Notice how you can specify a range of ports 20-21)

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 20:21 -j ACCEPT

FTP is frankly a mess… you really ought to use SFTP or SCP instead. But an inbound connection on port 21 should be enough to start an FTP session with us as the server. That’s only because we are allowing outbound connections from any port.

webmin on a custom port

# iptables -A INPUT -p tcp -m tcp --dport 10000 -j ACCEPT
HTTP

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 80 -j ACCEPT

SSL

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 443 -j ACCEPT

IMAP

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 143 -j ACCEPT

IMAPS

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 993 -j ACCEPT

POP3

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 110 -j ACCEPT

POP3S

# iptables -A INPUT -d 192.168.1.132 -p tcp --dport 995 -j ACCEPT

MYSQL (Allow remote access from a particular IP):

# iptables -A INPUT -s 172.50.3.45 -d 192.168.1.132 -p tcp --dport 3306 -j ACCEPT

This is for the unlikely case that you want to connect to a remote MySQL server, either to monitor it or replicate it. Usually you will want to protect it from being accessed remotely, in which case omitting this rule will be enough, because in a moment we will say “everything not allowed is forbidden”!

Accept any traffic from localhost (let our machine loopback to itself):

# iptables -A INPUT -d 192.168.1.132 -s 127.0.0.1 -j ACCEPT
ICMP/Ping: Blocking pings does NOT secure your host and DOES break

network management.  Allow ICMP pings!

# iptables -A INPUT -d 192.168.1.132 -p icmp -j ACCEPT

If you have a single server, you only need one more rule to finish turning this config into a firewall and that is the rule to REJECT or DROP all traffic not otherwise addressed. You can skip down to that now. Or we can take things a little further and provide firewall protection to machines on a LAN, a local network behind this machine. Interested? I though you might be. This part completely supercedes my old document from 2001.

Packets to and from the LAN network will undergo Network Address Translation or NAT. All those machines will appear to the Internet as though they are using our IP address (because they are!).

We’ll be using Source NAT (SNAT) and Destination NAT (DNAT). These packets take a side trip through the PREROUTING path, which is something new to us, not one of the three default “chains” we’ve been using. This may also require us to enable some extra Linux modules, which may not be compiled in to our kernel. Remember, iptables is just a way to give orders to the kernel; if the necessary module isn’t loaded, the kernel won’t know what to do with our commands.

You can load modules with modprobe; you can then switch these modules on and off through a command called sysctl, or by echoing a variable into a “pseudofile” called /proc/sys/net/ipv4/ip_forward — /proc files are actually representations of kernel memory that allow you to “peek” and “poke” values. The default “iptables” script probably sets up most of this stuff, but if it doesn’t work, check to make sure the iptable_net module is loaded and turned on.

In this next example the firewall creates a many-to-one NAT for the 172.16.100.0 network in which all the machines appear on the Internet as our example firewall’s IP address, 192.168.1.132.

#---------------------------------------------------------------

# Have to load the NAT module in a system startup script!

#---------------------------------------------------------------

modprobe iptable_nat

#---------------------------------------------------------------

# Then switch it on

#---------------------------------------------------------------

echo 1 > /proc/sys/net/ipv4/ip_forward

#---------------------------------------------------------------

Once the above is done, the script below should work:

#---------------------------------------------------------------

# NAT ALL traffic:

# TO:             FROM:            MAP TO SERVER:

# Anywhere        172.16.100.0/24  192.168.1.132 (FW IP)

#

# SNAT is used to NAT all other outbound connections initiated

# from the protected network to appear to come from

# IP address 192.168.1.132

#

# POSTROUTING:

#   SNAT connections from your inside network to the Internet

#

# PREROUTING:

#   DNAT connections from the Internet to your inside network

#

# - Interface eth0 is the internet interface 192.168.1.132

# - Interface eth1 is the private network interface 172.16.100.*

#---------------------------------------------------------------

# POSTROUTING statements for Many:1 NAT

# (Connections originating from the entire private network)

# iptables -t nat -A POSTROUTING -s 192.168.2.0/24 \

   -j SNAT -o eth0 --to-source 192.168.1.132

# Allow forwarding for all New and Established SNAT connections

# originating on the private network AND already established

# DNAT connections inbound:

# iptables -A FORWARD -t filter -o eth0 -m state \

   --state NEW,ESTABLISHED,RELATED -j ACCEPT

The assumption here is that boxes on the private network are not servers. If a client wants to put a server on your 192.168.2.x network, you will have to provide mapping from your outside address to a specific address AND port on the inside. The Quick How-to reference below (from which this was shamelessly stolen) has code to do that.

CLOSE THE BLAST DOORS!

Now that we’ve defined all the traffic that we want, it’s time to become a FIREWALL — which means we reject or drop everything sent to our IP that is not expected and wanted.

# iptables -A INPUT -d 192.168.1.132 -j REJECT

Or, even more stringent, reject misaddressed packets aimed at ANY IP, in case some benighted soul is trying to spoof addresses:

# iptables -A INPUT -j DROP# iptables -A FORWARD -j DROP

Finally, to save your active rules (under RedHat/Fedora/CentOS) execute the following:

# /etc/init.d/iptables save

This will save your rules to ‘/etc/sysconfig/iptables’ which you can back up and edit at your leisure. An alternative approach is simply to list your iptables commands, one per line, in the correct order, and run that as a shell script. It is always a good idea to use a script, since a single typo can ruin your whole day. As long as Rule One is in effect, you should be able to get in and run iptables --flush to undo the mess and start over.

Question? “You in the back there, Sakamoto?”

“I don’t want to accept mail from everywhere — I have an antispam service, and I only want to accept mail from their servers. How do I create a rule that will only allow inbound connections on port 25 from my anti-spam vendor, at a small range of addresses? Say, 10.1.1.10 through 10.1.1.14?”

A quick check with a subnet calculator tells us this range can be represented as “10.1.1.9/29″. That’s what we will pop into iptables as the source (-s) range, in place of the existing “allow anyone” rule.

Our old rule (which has no -s restriction):

# iptables -A INPUT -d 192.168.1.132 -p tcp \

   --dport 25 -j ACCEPT

thus becomes:

# iptables -A INPUT -s 10.1.1.9/29 -d 192.168.1.132 -p tcp \

 --dport 25 -j ACCEPT

Here’s a dirty little secret: You never change iptables rules; you only add and delete them.

If we add this rule to our existing “stack” of rules, it won’t work. Why not? Because the rules are applied in the order we added them, and we already have a rule that accepts port 25 traffic from everywhere. Those packets will have been sent on their way before we get to this more restrictive rule… so we need to delete the rule that accepts all mail.

To delete a rule we type it in exactly as we did to add it, but change the -A (for Append) to -D (for Delete):

# iptables -D INPUT -d 192.168.1.132 -p tcp --dport 25 \

   > -j ACCEPT

Now, I don’t know about you, but that “ACCEPT” at the end is a little distracting. Surely we want this rule to stop accepting packets? Don’t be confused. You always retype a rule in full, in order to identify it. The alternative would be to refer to its line number or some other abbreviation, and while that might be simpler it would be much less safe. Hitting “4″ instead of “5″ could lead to a network disaster.

By the way, Cisco’s IOS software works the same way. You type the whole rule to add it, and you type the whole rule to delete it. The part that counts is not the body of the rule, nor whether it says “ACCEPT” at the end. Our concern at this point is only with the “-D” that tells iptables to “find this rule, which I’m typing out in full so there is no possibility of confusion, and DELETE it!”

If you add the new rule now, though, you’re still in for a disappointment. Your ruleset will now reject all SMTP traffic! What happened? -A stands for append. New rules are added after existing ones… and our “REJECT all traffic” rule now comes before the (newly added) “ACCEPT smtp from these addresses” rule. Simple as that. In order to get the rules to work the way we intended, we must also remove the “REJECT all traffic” rule and add it back so that it comes after the new ACCEPT rule.

Now you can see why it’s important that our failsafe “always let me in” rule must be the very first rule! If you delete that rule, and then you run afoul of the “block all traffic” rule… well, it’s a long drive from my home in Oregon to my server in Virginia.

So the sequence of commands you would use is actually:

# iptables -A INPUT -s 10.1.1.9/29 -d 192.168.1.132 \

   > -p tcp --dport 25 -j ACCEPT

# iptables -D INPUT -d 192.168.1.132 -p tcp --dport 25 \

   > -j ACCEPT

# iptables -D INPUT -d 192.168.1.132 -j REJECT

# iptables -A INPUT -d 192.168.1.132 -j REJECT

Add the new, more restrictive ACCEPT rule; remove the “ACCEPT all smtp” rule; and remove and replace the “REJECT all unexpected packets” rule. (If you’re wondering — yes, deleting the REJECT rule does open your firewall, but only for an instant.)

Let’s verify that the new rules are in the correct order:

# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

ACCEPT     tcp  --  example.net          example.org   tcp dpt:ssh

ACCEPT     all  --  anywhere             anywhere      state RELATED,ESTABLISHED

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:ssh

ACCEPT     tcp  --  anywhere             anywhere      tcp dpt:ftp

ACCEPT     tcp  --  anywhere             anywhere      tcp dpt:10000

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:http

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:https

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:imap

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:imaps

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:pop3

ACCEPT     tcp  --  anywhere             192.168.1.132 tcp dpt:pop3s

ACCEPT     all  --  mail.example.org     192.168.1.132

ACCEPT     icmp --  anywhere             192.168.1.132

#  Here's the new "exclusive" smtp rule:

ACCEPT     tcp  --  10.1.1.9/29          192.168.1.132 tcp dpt:smtp

#  Here's the "reject everything" rule:

REJECT     all  --  anywhere             192.168.1.132 reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)

target     prot opt source               destination

ACCEPT     all  --  anywhere             anywhere            state RELATED,ESTABLISHED

ACCEPT     all  --  anywhere             anywhere            state NEW,RELATED,ESTABLISHED

Chain OUTPUT (policy ACCEPT)

target     prot opt source               destination

ACCEPT     all  --  anywhere             anywhere            state NEW,RELATED,ESTABLISHED

Looks good. Now, try to telnet to port 25 from some outside machine that is not your antispam vendor:

# ssh -l loser ragnar.brasscannon.net # access remote machine as user named "loser"

Welcome to ragnar, loser.

[loser ~]$ telnet example.org 25Trying 192.168.1.132...

telnet: Unable to connect to remote host: Connection refused

Bingo! That’s what we want to see. At this point you would also send yourself some mail to make sure the antispam vendor CAN connect, of course.

And remember to save the new rules again:

# /etc/init.d/iptables save

References

I combined two of the pages below in order to meet the needs of a recent client, and the result was too good not to share. I’m sure the authors will still recognize huge chunks of their material; I am openly acknowledging my debt. “When you steal from one source, it’s plagiarism; when you steal from four, it’s research.”

www.5dollarwhitebox.org
www.linuxhomenetworking.com
www.liniac.upenn.edu
www.sitepoint.com


Comments

Leave a Reply

You must be logged in to post a comment.