|
|
A Practical iptables Firewall on LinuxA Hands-on How-toSMfrom Brass Cannon ConsultingA little vague handwaving can often save hours of tedious explanation. |
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). |
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.
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.
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
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