This example configuration controls access to a private (masqueraded) network. It permits access to a variety of services on the boundary machine, a scheme that is frowned on by some security gurus, but which is a comfortable tradeoff between an unprotected network and an expensive multi-box firewall protection system.
The actual configuration file, without all the explanatory material in it can be retrieved here. Iptables comes with some support programs to save and restore the rulesets; these are called iptables-save and iptables-restore. These programs use a shortened form of the command strings that would actually be used if you typed all the commands in yourself. That format is what is used here.
Iptables handles packets that would start a new connection by consulting its nat table. Such packets are either TCP packets with the SYN flag set, or UDP packets that have not been preceded by similar ones recently. Address translation works by making packets that will leave the private network all appear as though they came from the boundary machine. Then when a reply arrives, it will be re-translated by the boundary machine and routed to the requestor. To preserve the chance for computers on the private network to talk with the boundary machine, this translation must not occur until after routing, so that anything destined for the boundary machine is left alone. Note that we specify both that the packet came from a private network address, and that it is routed out on eth1, the interface to the public Internet.
*nat :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] :OUTPUT ACCEPT [0:0] -A POSTROUTING -o eth1 -s 192.168.1.0/255.255.255.0 -j SNAT --to-source 18.104.22.168 COMMIT
The next thing we do is set up default policies for the filtering chains. We set the INPUT and FORWARD policies to DROP, because we want to allow only those packets that correspond to traffic that we explicitly allow. Recall that the INPUT rules are only consulted for packets that are destined for the boundary machine, and the FORWARD rules are only consulted for packets that are to be routed through the boundary.
*filter :INPUT DROP [0:0] :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0]
These rulesets will be filled in step 6, below.
:ICMP-CHK - [0:0] :UDP-CHK - [0:0] :TCP-CHK - [0:0] :TCP-SYN - [0:0]
These rules not only make the checking easier to understand, but also increase the efficiency of the checking process for each packet. The first step is to allow local traffic, both on the loopback interface (within the boundary machine), and arriving on the private-side ethernet interface. Then we must check the packets arriving from the public Internet. Since most traffic is TCP, those are separated out first, and are never subjected to the rules for other protocols. Similarly, UDP packets only have to suffer through one TCP rule (which they flunk) before they are properly directed to the right ruleset. Since ICMP messages are the least frequent, they are handled last; any other packets that appear are merely logged and then dropped (because of the INPUT chain policy).
-A INPUT -i lo -j ACCEPT -A INPUT -i eth0 -j ACCEPT -A INPUT -p tcp -j TCP-CHK -A INPUT -p udp -j UDP-CHK -A INPUT -p icmp -j ICMP-CHK -A INPUT -j LOG --log-prefix INPT:
Generally, we do not want to allow external sources to initiate connections with machines behind our firewall. But if a machine behind the firewall initiates a connection, we want to allow that. For TCP, the connection setup takes three steps: a request from the initiator, an acknowledgment from the responder (telling the initiator that the connection is set up), and a reply from the initiator letting the responder know that the connection is established both ways.
The first connection packet is handled by the rule in the nat table, which sets up the necessary address translation, and then passes the initial connect packet out onto the Internet. The second step, the reply from an external source, is checked by the second rule below. Once these two packets have passed through the boundary machine, the connection becomes ESTABLISHED, and further packets belonging to this connection are handled by the third rule below.
UDP traffic is connectionless, so we only allow incoming traffic that is destined for non-privileged UDP ports. All remaining traffic is logged, and then dropped (according to the FORWARD policy, set above). Outgoing traffic passes unimpeded according to the first rule, that anything coming from eth0, the interface to the private network, is accepted.
-A FORWARD -i eth0 -j ACCEPT -A FORWARD -p tcp -m tcp --tcp-flags SYN,ACK,FIN,RST SYN,ACK -j ACCEPT -A FORWARD -p tcp -m state --state ESTABLISHED -j ACCEPT -A FORWARD -p udp -m udp --dport 1024:65535 -j ACCEPT -A FORWARD -j LOG --log-prefix PASS:
Now that all the housekeeping is done, we can create actual protocol filtering rules for packets that are addressed to the boundary machine itself. Our first step is to specify the standard TCP service ports that we want to open so that external customers can make contact with them. We handle first the various services that we guess will occur most frequently: web-server (HTTP), secure-shell, e-mail, and DNS update requests from our ISP. All other requests for connection on privileged (low-numbered) ports are referred to the chain TCP-SYN for processing.
Finally, traffic to non-privileged ports is accepted unconditionally. This is not as circumspect as one might wish, but without a lot of analysis, tighter control is difficult. Any packets that fall to the bottom of the TCP-CHK chain will be logged. Such packets will be addressed to low-numbered ports, but do not have the correct flags set to start a new connection. Therefore, any such packet is an error in protocol.
-A TCP-CHK -p tcp --dport 80 -j ACCEPT -A TCP-CHK -p tcp --sport 80 -j ACCEPT -A TCP-CHK -p tcp --dport 22 -j ACCEPT -A TCP-CHK -p tcp --dport 25 -j ACCEPT -A TCP-CHK -p tcp -s 22.214.171.124/24 --dport 53 -j ACCEPT -A TCP-CHK -p tcp --dport 0:1023 --syn -j TCP-SYN -A TCP-CHK -p tcp --dport 1024:65535 -j ACCEPT -A TCP-CHK -j LOG --log-prefix IN-TCP: -A TCP-CHK -j DROP
This chain defines common packet types that we want to drop. These are merely counted in this chain, and then dropped; anything that is unexpected will be logged. This makes reading the syslog a lot less boring. The incidence of these packets can be retrieved from a daily or weekly capture of the traffic statistics (via iptables -nvL). New ports should be added to this list when their appearance in the syslog becomes common, and you are sure that the corresponding service is not supposed to be allowed on your boundary machine.
-A TCP-SYN -p tcp --dport 135 -j DROP -A TCP-SYN -p tcp --dport 139 -j DROP -A TCP-SYN -p tcp --dport 443 -j DROP -A TCP-SYN -p tcp --dport 445 -j DROP -A TCP-SYN -p tcp --dport 901 -j DROP -A TCP-SYN -j LOG --log-prefix IN-TCP: -A TCP-SYN -j DROP
Similarly, we create a list of the standard UDP service ports that we will accept. I have configured DNS and NTP services only in this ruleset. The first three rules deal with various forms of DNS traffic; the next three allow Network Time Protocol traffic from servers that I have subscribed to. Finally, there is a series of common break-in attempts that should just be dropped outright. Any less-common packet is then logged before being dropped.
-A UDP-CHK -p udp --sport 1024:65535 --dport 53 -j ACCEPT -A UDP-CHK -p udp --sport 53 --dport 53 -j ACCEPT -A UDP-CHK -p udp --sport 53 --dport 1024:65535 -j ACCEPT -A UDP-CHK -p udp -s 126.96.36.199 --sport 123 --dport 123 -j ACCEPT -A UDP-CHK -p udp -s 188.8.131.52 --sport 123 --dport 123 -j ACCEPT -A UDP-CHK -p udp -s 184.108.40.206 --sport 123 --dport 123 -j ACCEPT -A UDP-CHK -p udp --dport 137 -j DROP -A UDP-CHK -p udp --dport 139 -j DROP -A UDP-CHK -p udp --dport 1434 -j DROP -A UDP-CHK -j LOG --log-prefix IN-UDP: -A UDP-CHK -j DROP
Finally, we create a list of ICMP messages that we will accept. Depending on the settings in /proc/sys/net/ipv4, our boundary machine may ignore some or all of these, but at least we can allow that choice to be made by the system, instead of just dropping them all.
-A ICMP-CHK -p icmp --icmp-type echo-reply -j ACCEPT -A ICMP-CHK -p icmp --icmp-type echo-request -j ACCEPT -A ICMP-CHK -p icmp --icmp-type destination-unreachable -j ACCEPT -A ICMP-CHK -p icmp --icmp-type source-quench -j ACCEPT -A ICMP-CHK -p icmp --icmp-type redirect -j ACCEPT -A ICMP-CHK -p icmp --icmp-type time-exceeded -j ACCEPT