Creating a Host-Based Firewall Script
Now we are ready to start designing a rule set that protects a single host. We will write the rule set in the form of a shell script, attempting to keep the script readable by parameterizing it using shell variables.
NOTE
We assume that the reader is familiar with basic Bourne shell scripting.
We store the firewall script in the file named /etc/sun_fw/fw.sh. An additional script is needed to automatically run the firewall script at boot time. Because these scripts are different for each Linux distribution, we will make scripts for the supported distributions available on Sun's web site.
Specify Firewall Script Parameters
We start the script with definitions that supply the script with the network parameters we will need later.
INTERFACE="eth0" IPADDR="192.168.0.2" BCASTADDR="192.168.0.255"
The next set of variables specifies which types of protocol sessions we want to allow inbound and outbound. In this example, we allow inbound access to SSH, HTTP, and FTP, and we allow outbound access only to DNS. This configuration reflects the necessary rules for a web and FTP server.
For desktop systems, we would need to allow outbound protocols, such as HTTP, HTTP/SSL (http), and FTP.
Also, we specify which Internet Control Message Protocol (ICMP) types can pass the firewall. Note that ICMP redirects are not allowed in this configuration. It might be necessary to add them, depending on your network configuration.
TCP_IN="ssh http ftp" TCP_OUT="domain ssh http https 1024:65535" UDP_IN="" UDP_OUT="domain ntp" ICMP_IN="destination-unreachable source-quench echo-request time-exceeded parameter-problem" ICMP_OUT="destination-unreachable source-quench echo-request time-exceeded parameter-problem"
Lastly, we add shell variables that make our script more readable. FW is an abbreviation for the iptables command, and the most common command (iptables append) receives its own abbreviation, NEW.
FW="/sbin/iptables" NEW="${FW} --append"
Load Helper Modules
The iptables firewall is modular, and its functionality can be extended by loading additional modules into the kernel. A module that is commonly used is ip_conntrack_ftp, which inspects FTP control connections and can be used to associate FTP data connections with existing control connections. Without this support for FTP, it would be necessary to open up a range of ports for use with inbound passive FTP connections, which would limit the usefulness of the firewall.
The following lines load the module ip_conntrack_ftp into the kernel. The MODPROBE command loads Linux kernel modules, including firewall helper modules.
MODPROBE="/sbin/modprobe" $MODPROBE ip_conntrack_ftp
Prepare the Firewall
First remove the previous firewall rules, then delete all user-defined chains. These tasks must be performed in this order, because user-defined rule chains can only be deleted if there are no references to them. By clearing all the chains first, we ensure that this is the case.
$FW --flush $FW --delete-chain
The predefined chains (INPUT, OUTPUT, and FORWARD) have a default policy, which decides what to do when none of the filter rules match. We set the default policy to DROP, which silently discards packets. Although not strictly necessary, we set the policy for FORWARD as well, which ensures that the host does not act as a router.
for ch in INPUT OUTPUT FORWARD; do $FW -P $ch DROP done
The kernel firewall is now in a known state, and we can start adding rules.
Establish Logging Rules
It is usually not a good idea to log every single packet that is rejected by firewall rules. Most IP networks tend to be noisy environments, and systems receive all kinds of unsolicited packets that can usually be ignored without problems. Examples of these types of packets are NetBIOS broadcasts, NTP broadcast and multicast packets, and Routing Information Protocol (RIP) broadcast or multicast packets.
On Linux, we have the additional problem that a large volume of log entries negatively impacts the performance of the system. We devise a strategy to lower the volume of packets that are logged if they are rejected. We do not currently log packets that are allowed through the firewall.
We implement a separate rule chain named discard to process all packets that our firewall rules do not pass. Depending on your environment, you might have to augment this rule chain to filter additional log entries.
We decide not to log any broadcast packets. In a controlled environment such as a service or DMZ (demilitarized zone) network, you might want to fine tune this strategy, depending on your policies and the risks you are mitigating.
The logging rule on the fourth line limits the number of log entries to about 10 per minute, to keep the log file manageable during a flooding attack. This rate is relatively low, and it should be adjusted to make sure that you capture as much information as possible without running the risk of overflowing your logging partition.
The fifth line handles a special case: If an attempt is made to connect to the ident service, the firewall replies with a TCP RST packet. We notice that delivery of email to certain remote systems is impaired because the remote system is attempting to contact the ident server on the test system. The addition of this rule notifies remote systems that no ident server exists, and speeds up the delivery of email. Any other packets are silently discarded.
$FW -N discard # create new rule $NEW discard -p udp -d ${BCASTADDR} -j DROP $NEW discard -p udp -d 255.255.255.255 -j DROP $NEW discard -m limit --limit 10/minute --limit-burst 20 -j LOG $NEW discard -p tcp --syn -d ${IPADDR} --dport ident -j REJECT --reject-with tcp-reset $NEW discard -j DROP
Add Anti-Spoofing Rules
We are now ready to start adding entries to the INPUT and OUTPUT chains, which filter the inbound and outbound traffic.
These two rules pass all traffic that does not pass through the protected interface, including traffic going through the loopback interface. Note that the exclamation mark (!) is used by iptables to invert the meaning of a condition, for example, -i ! eth0 matches all packets not coming in through interface eth0.
$NEW INPUT -i '!' ${INTERFACE} -j ACCEPT $NEW OUTPUT -o '!' ${INTERFACE} -j ACCEPT
It's relatively simple to forge IP addresses. One of the tasks of a firewall is to verify whether the address information contained in an IP packet is consistent with its knowledge of the network. For a host-based firewall, this knowledge is usually quite limited.
The first two rules verify that all incoming packets are actually intended for this host and do not appear to be sent from this host. The third rule disallows traffic that uses the address range assigned to the loopback interface. Similar rules can be added to filter out other address ranges, such as those defined in RFC 1918. We do not implement any outbound anti-spoofing rules.
$NEW INPUT -s ${IPADDR} -j discard $NEW INPUT -d '!' ${IPADDR} -j discard $NEW INPUT -s 127.0.0.0/8 -j discard
Add Dynamic Rules
The following two rules handle packets that belong to sessions for which the iptables firewall maintains state. When the firewall passes the initial packet of a session, it stores information about the session that enables it later to match packets to this session.
A packet matches the ESTABLISHED criterion if it is part of an existing TCP connection or UDP session. It matches the RELATED criterion if it is associated with an existing connection. For the purpose of this rule set, RELATED matches FTP data connections associated with existing FTP control connections. Also, RELATED matches certain ICMP packets that carry information about individual sessions.
$NEW INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT $NEW OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
NOTE
The capability to associate certain ICMP packets is often important because it allows Path MTU discovery to work correctly.
Manage Inbound Sessions
Stateful packet filtering uses a certain amount of memory per active connection. To limit the memory impact, we use stateful packet filtering only where necessary. This strategy limits the impact during a flooding or DOS attack. For simple inbound services like HTTP and secure shell (SSH), we do not need to use stateful packet filtering. The FTP protocol is a different matter, because passive mode FTP does not use a fixed port number for the data connection.
The firewall rule for the FTP case creates a new entry in the stateful packet filtering table maintained in the kernel. The rules matching ESTABLISHED and RELATED traffic process the remaining packets of a legitimate FTP session.
For all other protocols, we use static rules. Note that we do not limit the source port for these rules to unprivileged port numbers, as is commonly done for a simple packet filter, because there is no need for that. One disadvantage of using static rules for inbound is that the ports associated with services can be probed using stealth scans. This disadvantage is usually not a problem, because we are not trying to keep the existence of these services a secret.
For most protocols, we have to add one static rule for incoming traffic and one for outgoing traffic. In the case of FTP, we add only one rule, because the stateful filtering automatically takes care of the remaining traffic. The individual rules are generated by iterating over all the protocols we want to allow in.
for port in ${TCP_IN}; do case "${port}" in ftp) $NEW INPUT -p tcp --dport ${port} --syn -m state --state NEW -j ACCEPT ;; *) $NEW INPUT -p tcp --dport ${port} -j ACCEPT $NEW OUTPUT -p tcp '!' --syn --sport ${port} -j ACCEPT ;; esac done
We do not recommend the use of standard FTP, except for anonymous access, because the protocol exchanges authentication information as plain text. We recommend OpenSSH as an alternative.
The rules for inbound UDP sessions are exactly analogous to the ones for inbound TCP. Again, we do not use stateful packet filtering.
for port in ${UDP_IN}; do $NEW INPUT -p udp --dport ${port} -j ACCEPT $NEW OUTPUT -p udp --sport ${port} -j ACCEPT done
Manage Outbound Sessions
For outbound TCP sessions, we use stateful packet filtering because the security gain is considerable: Attackers cannot probe for open ports, and we can actually support normal FTP without resorting to special tricks.
for port in ${TCP_OUT}; do $NEW OUTPUT -p tcp --dport ${port} --syn -m state --state NEW -j ACCEPT done
For UDP, the security gains are even more substantial. Because the filter matches requests with responses, it blocks unsolicited packets used for overflow attacks on DNS and Network Transport Protocol (NTP) servers that only serve internal networks.
for port in ${UDP_OUT}; do $NEW OUTPUT -p udp --dport ${port} -m state --state NEW -j ACCEPT done
Manage ICMP
Certain types of ICMP packets are necessary for the correct functioning of TCP and UDP. We pass only the types required. The iptables stateful packet filtering automatically passes ICMP packets that are related to existing TCP or UDP sessions (use the RELATED rule when matching packets). Because we choose not to use stateful filtering for all traffic, we cannot rely on this mechanism to pass all the ICMP packets we need, so we are required to pass them explicitly. If the choice were made to use stateful inspection for all traffic, this would be unnecessary.
for t in ${ICMP_IN}; do case "${t}" in echo-request) $NEW INPUT -p icmp --icmp-type echo-request -j ACCEPT $NEW OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT ;; *) $NEW INPUT -p icmp --icmp-type ${t} -j ACCEPT ;; esac done for t in ${ICMP_OUT}; do case "${t}" in echo-request) $NEW OUTPUT -p icmp --icmp-type ${t} -m state --state NEW -j ACCEPT ;; *) $NEW OUTPUT -p icmp --icmp-type ${t} -j ACCEPT ;; esac done
Discard Other Traffic
All other incoming packets are invalid, so we jump to the discard chain, which takes care of logging.
$NEW INPUT -j discard
For outgoing packets that we discard, we do not create a separate logging chain. Instead of silently discarding the packets, we have the firewall send either a TCP, RST, or ICMP destination-unreachable packet. This allows applications on our host to quickly determine that the traffic is not allowed through the firewall.
$NEW OUTPUT -m limit --limit 10/minute --limit-burst 20 -j LOG $NEW OUTPUT -p tcp -j REJECT --reject-with tcp-reset $NEW OUTPUT -j REJECT