Howto: IPTables: Personal Firewall
Last updated 9 August 2023
I've had numerous requests in the recent past as to how I set up my iptables rules for my machines. Well, in this howto I will attempt to explain how I set up iptables for use on a personal computer. I use similar setups on router boxes, as well as servers. In fact, this setup is generic enough for almost any situation - I've only ever seen a few cases where the guidelines provided in this howto was not sufficient.
Each distribution has it's own method of installing packages, I use gentoo, on which a simple "emerge -uav iptables" is sufficient to install all userspace packages that you require (for this guide at least).
The appropriate kernel modules are however also required. These can be found under "Device Drivers", "Networking support", "Networking options", "Network packet filtering (replaces ipchains)" and finally "IP: Netfilter Configuration". At a minimum you will need these options:
The following modules are also useful, but probably won't be explained in this guide.
Whether you want to compile a monolithic kernel or a modular kernel is up to you. I'm actually beginning to tend towards modular kernels, but still like to compile monolithic kernels as far as possible. This sounds like a bit of a paradox, but consider this: Not all modules are always used, as such, I compile these modules as modules, and modules that I always use I compile statically into the kernel. This would include those modules I specified under "must have".
Another really useful option in the kernel is SYN Cookies. Just compile it into the kernel and add "net.ipv4.tcp_syncookies = 1" to /etc/sysctl.conf and run "sysctl -p". Whilst you are at it, sommer add "net.ipv4.conf.default.rp_filter = 1" and "net.ipv4.ip_forward = 0" in there as well. The ip_forward rule should be set to 1 if you intend to run a router box.
Configuring the INPUT chain
Finally we get round to the real stuff. Actually, this is quite simple. To give you an idea of where we are headed, take a look at the following set of rules:
Chain INPUT (policy ACCEPT 1076K packets, 253M bytes) pkts bytes target prot opt in out source destination Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 1540K packets, 1625M bytes) pkts bytes target prot opt in out source destination
Which happens to be the default ruleset. You can obtain your current ruleset by issues "iptables -L". I like also adding -v (verbose) and -n (numerical). Verbosity just gives more (and mostly very important) information, -n just makes things quicker by not doing hostname lookups (should be the default imho). The above shows you the fundamentals of iptables, the filter table (which contains three chains, INPUT, FORWARD, OUTPUT).
The first thing to note is that by default all packets gets accepted - this is exactly what we don't want. In fact, I like the idea of "block everything but that which is needed". For this reason we change the default policy to DROP. At this stage I should probably mention that there are four special targets, two of which I actually use:
There are other targets too, but these are the only two we'll require for this tutorial.
Back to changing that default policy. Policies are set using the -P parameter to iptables, to change the policy for INPUT to DROP, issue the following command:# iptables -P INPUT DROP
And since we are not interrested in routing (yet), do the same for the FORWARD chain:# iptables -P FORWARD DROP
At this point you were hopefully not working via ssh, as you will now not be able to talk to your box, not even via the lo device. No IP traffic can enter your box at this point of time. Perfectly secure? Hopefully, unless there is a bug in the netfilter code (I have discovered one in the ULOG target a long while back which under specific circumstances could have been used to DoS the box). Right, at this point we probably want to be able to start opening specific services, but hang on a little longer, let's first prep the rules a bit more.# iptables -A FORWARD -m state --state INVALID -j DROP
This is strictly speaking not really needed, but I like putting it in anyway, it just tries to trap many invalid (midstream packets that isn't associated with any streams type of thing) as soon as possible and just DROP those packets without having to go through the entire chain.
Once a connection has been established, we want to accept all packets for that connection, we also want to accept related packets (for example ICMP port unreachable, ICMP echo response, SYN/ACK packets upon SYN, RST packets and so forth). This is where the state module steps in and saves the day:# iptables -A INPUT -m state --state established,related -j ACCEPT
This says load the state module (-m state), then issues the --state flag with established and related. established does exactly what it says, all packets that form part of an established connection, related covers all other packets such as ICMP error messages et al. The state module also has invalid, snat and dnat parameters. I've never had a need for snat or dnat (read the man page if your interrested) and whilst it is probably a good idea to use the invalid flag with a DROP rule as the first rule I've never bothered with this as invalid packets will never (afaik) be accepted by any of my other rules.
Right, now we can maintain connections (but not yet establish them). That would obviously be the next step? Almost, first we want to state that we trust the lo device by saying that we want to accept all input packets from that device:# iptables -A INPUT -i lo -j ACCEPT
This just says accept all packets that orriginated on the local machine. Now we are ready to establish new connections. This is in fact only required if you are hosting any services on your computer such as ssh, http or ftp. I do, so I issue the following command:# iptables -A INPUT -i eth0 -p tcp -m multiport --dports 21,22,80,139,445,873,16384 --syn -j ACCEPT
Yes, I run lots of services:
The --syn says to only accept pure SYN packets, ie, not SYN/ACK or ACK or any other packets, only packets for which the SYN bit is set and both the ACK and RST bits are unset.
I do something similar for UDP, on which ports 137 and 138 needs to be open in order for samba to work:# iptables -A INPUT -i eth0 -p udp --dport 137:138 -j ACCEPT
Since there is only a single port range that requires to be open I don't bother with multiport this time round.
I'm running on a semi public network (which is why I tighten my rules as far as possible). But I own two other machines which I trust entirely - well, I'm the only user on them. And since the above is not the only networking protocols I use (think ldap, nfs, portmap, ntp, dhcp, cups) I still need to open some ports for those machines as well. Well, I could repeat the above with each of those machines MAC addresses as additional restriction, or I could just trust them with rules such as:# iptables -A INPUT -m mac --mac-source 00:A0:C9:96:94:D3 -s 192.168.0.2 -j ACCEPT
Note that I specify both the MAC address and the IP addess, this is simply in case someone manages to bounce a packet off the machine which then contains a different IP (One of the machines serves as a router on the network). I do this because I myself have used similar techniques to bypass firewalls (I had the permission of the owner to perform penetration testing).
At this point in time my setup (for a non-router) machine is complete, and I can state that it is most likely the simplest ruleset to obtain maximum effectiveness from a firewall. "iptables -L -v -b" reports:
Chain INPUT (policy DROP 8 packets, 1153 bytes) pkts bytes target prot opt in out source destination 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 state INVALID 27868 4038K ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED 8 480 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 5 300 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 multiport dports 21,22,80,139,445,873,16384 tcp flags:0x16/0x02 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:137:138 0 0 ACCEPT all -- * * 192.168.0.2 0.0.0.0/0 MAC 00:A0:C9:96:94:D3 Chain FORWARD (policy DROP 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 1574K packets, 1627M bytes) pkts bytes target prot opt in out source destination
Hmm, just in the time it took me to configure this firewall I already dropped 8 packets. Right, if you would like to know what these packets are, you can use the LOG target (compile in LOG target support):# iptables -A INPUT -j LOG
Will log all packets to syslog before sending them to oblivion. If you have a really high drop rate you will perhaps want something like:# iptables -A INPUT -m limit --limit 1/min -j LOG
Which will log an average of 1 packet per minute (and simply proceed to drop the rest). If there are specific packets you would like to not have logged, simply DROP them explicitly before the LOG rule (you can use iptables -I for this purpose, for example:# iptables -I INPUT 6 -p udp --sport 631 --dport 631 -j DROP
Will drop all cups traffic before the LOG statement above (presuming you've added the LOG statement to the rules above). This happened to be most of the DROP'ed traffic (cups broadcast traffic to browse for other printers on the network).
IMPORTANT NOTICE: This firewall ruleset breaks the samba browsing. For that to function properly a few specialised rules that seriously undermines security has to be set up. Other machines can still browse you (the rule does accept broadcast packets), but you cannot do broadcast lookups. One way to work around this is to set up a WINS server (highly recommended). You can also set up a rule to accept all packets comming from port 138 to your machine on udp port numbers greater than 1024:# iptables -A INPUT -i eth0 -p udp --sport 138 --dport 1025: -j ACCEPT
But I don't like that a lot since you can now be port-scanned if the scannee uses port 138 to scan from :). The recent module might be able to help here but as of yet I could not figure out how. My finalised ruleset then looks like:
Chain INPUT (policy DROP 87 packets, 11470 bytes) pkts bytes target prot opt in out source destination 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 state INVALID 46238 6659K ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED 12 720 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 14 840 ACCEPT tcp -- eth0 * 0.0.0.0/0 0.0.0.0/0 multiport dports 21,22,80,139,445,873,16384 tcp flags:0x16/0x02 3 696 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:137:138 0 0 ACCEPT udp -- eth0 * 0.0.0.0/0 0.0.0.0/0 udp spt:138 dpts:1025:65535 25 4107 ACCEPT all -- * * 192.168.0.2 0.0.0.0/0 MAC 00:A0:C9:96:94:D3 81 10578 DROP udp -- * * 0.0.0.0/0 0.0.0.0/0 udp spt:631 dpt:631 22 2833 LOG all -- * * 0.0.0.0/0 0.0.0.0/0 LOG flags 0 level 4 Chain FORWARD (policy DROP 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 1591K packets, 1628M bytes) pkts bytes target prot opt in out source destination
One last question may be something to the effect of "Why do you not firewall the OUTPUT chain?". Well, answer is simple, I trust the security of my machine, I doubt seriously that it'll get compromised so imho there is no reason to try to lock someone it. If someone does get in - well done, he'll probably be able to obtain root if (s)he's that good, so there is no real use. Just for the record, I have set up machines where it was important that the machine be as quiet as possible, where I have firewalled the OUTPUT chain too with great success.
Configuring a firewall router