Hands-on How-to is a service mark of Brass Cannon LLC
 

A Practical iptables Firewall on Linux

A Hands-on How-toSM

from Brass Cannon Consulting

A little vague handwaving can often save hours of tedious explanation.

Better safe than sorry

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 you can continue to access your server using ssh, even if we mess up something while we're experimenting. The # prefix 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

Take this command one field at a time:

-A INPUT - means append this rule to the INPUT chain (we'll explain chains in just a moment)
-s - Source Address (172.16.0.128 - replace with your home IP)
-d - Destination Address (this server - replace it!)
-p - Protocol - TCP, UDP, or ICMP; SSH uses TCP.
--dport - Destination Port; SSH uses port 22
-j - Jump - if everything matches, jump to the ACCEPT target

As you'd expect, ACCEPT means this packet is "OK" and may proceed to its original destination.

Why are we specifying a destination port but not a source port? That's how TCP works. Source ports are usually irrelevant; what matters is that the SSH server is listening on destination port 22. Any source port is valid on the machine that is trying to reach the server.

In short, whatever rules we add after this one should not block us from being able to log in again and fix our mistakes.

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

Note the 3 "Chains":

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).

The Cliff Notes version?

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 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. 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.

TALKING TO IPTABLES

You can enter an iptables command from the command line. That's a good way to try it out, if 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; 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]

To understand what iptables is actually doing, we need to look at how TCP/IP works. Every 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, so we do want to 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 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.

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"
[loser ~]$ telnet example.org 25
Trying 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."

http://www.5dollarwhitebox.org/wiki/index.php/Howtos_Basic_IPTables
http://www.linuxhomenetworking.com/wiki/index.php/Quick_HOWTO_:_Ch14_:_Linux_Firewalls_Using_iptables
http://www.liniac.upenn.edu/sysadmin/security/iptables.html
http://www.sitepoint.com/article/secure-server-iptables

Google
 
Web handsonhowto.com



Hands-On How-To Index