These two scripts use iptables to configure netfilter on a DMZ'ed server and on the firewall that protects it, assuming a simple inside-DMZ-outside architecture as described in Chapter 2 and Chapter 3. For the full example scenario to which these scripts apply, refer to Section 3.1.8.
The first script is for the bastion host "Woofgang," a public FTP/HTTP server, shown in Example A-1.
#! /bin/sh # init.d/localfw # # System startup script for local packet filters on a bastion server # in a DMZ (NOT for an actual firewall) # # Functionally the same as Example 3-10, but with SuSE-isms restored and # with many more comments. # # Structurally based on SuSE 7.1's /etc/init.d/skeleton, by Kurt Garloff # # The following 9 lines are SuSE-specific # ### BEGIN INIT INFO # Provides: localfw # Required-Start: $network $syslog # Required-Stop: $network $syslog # Default-Start: 2 3 5 # Default-Stop: 0 1 2 6 # Description: Start localfw to protect local heinie ### END INIT INFO # /End SuSE-specific stuff (for now) # Let's save typing & confusion with a couple of variables. # These are NOT SuSE-specific in any way. IP_LOCAL=208.13.201.2 IPTABLES=/usr/sbin/iptables test -x $IPTABLES || exit 5 # The following 42 lines are SuSE-specific # Source SuSE config # (file containing system configuration variables, though in SuSE 8.0 this # has been split into a number of files in /etc/rc.config.d) . /etc/rc.config # Determine the base and follow a runlevel link name. base=${0##*/} link=${base#*[SK][0-9][0-9]} # Force execution if not called by a runlevel directory. test $link = $base && START_LOCALFW=yes test "$START_LOCALFW" = yes || exit 0 # Shell functions sourced from /etc/rc.status: # rc_check check and set local and overall rc status # rc_status check and set local and overall rc status # rc_status -v ditto but be verbose in local rc status # rc_status -v -r ditto and clear the local rc status # rc_failed set local and overall rc status to failed # rc_reset clear local rc status (overall remains) # rc_exit exit appropriate to overall rc status . /etc/rc.status # First reset status of this service rc_reset # Return values acc. to LSB for all commands but status: # 0 - success # 1 - misc error # 2 - invalid or excess args # 3 - unimplemented feature (e.g. reload) # 4 - insufficient privilege # 5 - program not installed # 6 - program not configured # 7 - program is not running # # Note that starting an already running service, stopping # or restarting a not-running service as well as the restart # with force-reload (in case signalling is not supported) are # considered a success. # /End SuSE-specific stuff. # The rest of this script is non-SuSE specific case "$1" in start) echo -n "Loading Woofgang's Packet Filters" # SETUP -- stuff necessary for any bastion host # Load kernel modules first # (We like modprobe because it automatically checks for and loads any other # modules required by the specified module.) modprobe ip_tables modprobe ip_conntrack_ftp # Flush active rules and custom tables $IPTABLES --flush $IPTABLES --delete-chain # Set default-deny policies for all three default chains $IPTABLES -P INPUT DROP $IPTABLES -P FORWARD DROP $IPTABLES -P OUTPUT DROP # Give free reign to the loopback interfaces, i.e. local processes may connect # to other processes' listening-ports. $IPTABLES -A INPUT -i lo -j ACCEPT $IPTABLES -A OUTPUT -o lo -j ACCEPT # Do some rudimentary anti-IP-spoofing drops. The rule of thumb is "drop # any source IP address which is impossible" (per RFC 1918) # $IPTABLES -A INPUT -s 255.0.0.0/8 -j LOG --log-prefix "Spoofed source IP" $IPTABLES -A INPUT -s 255.0.0.0/8 -j DROP $IPTABLES -A INPUT -s 0.0.0.0/8 -j LOG --log-prefix "Spoofed source IP" $IPTABLES -A INPUT -s 0.0.0.0/8 -j DROP $IPTABLES -A INPUT -s 127.0.0.0/8 -j LOG --log-prefix "Spoofed source IP" $IPTABLES -A INPUT -s 127.0.0.0/8 -j DROP $IPTABLES -A INPUT -s 192.168.0.0/16 -j LOG --log-prefix "Spoofed source IP" $IPTABLES -A INPUT -s 192.168.0.0/16 -j DROP $IPTABLES -A INPUT -s 172.16.0.0/12 -j LOG --log-prefix "Spoofed source IP" $IPTABLES -A INPUT -s 172.16.0.0/12 -j DROP $IPTABLES -A INPUT -s 10.0.0.0/8 -j LOG --log-prefix " Spoofed source IP" $IPTABLES -A INPUT -s 10.0.0.0/8 -j DROP # The following will NOT interfere with local inter-process traffic, whose # packets have the source IP of the local loopback interface, e.g. 127.0.0.1 $IPTABLES -A INPUT -s $IP_LOCAL -j LOG --log-prefix "Spoofed source IP" $IPTABLES -A INPUT -s $IP_LOCAL -j DROP # Tell netfilter that all TCP sessions do indeed begin with SYN # (There may be some RFC-non-compliant application somewhere which # begins its transactions otherwise, but if so I've never heard of it) $IPTABLES -A INPUT -p tcp ! --syn -m state --state NEW -j LOG --log-prefix "Stealth scan attempt?" $IPTABLES -A INPUT -p tcp ! --syn -m state --state NEW -j DROP # Finally, the meat of our packet-filtering policy: # INBOUND POLICY # (Applies to packets entering our network interface from the network, # and addressed to this host) # Accept inbound packets that are part of previously-OK'ed sessions $IPTABLES -A INPUT -j ACCEPT -m state --state ESTABLISHED,RELATED # Accept inbound packets which initiate SSH sessions $IPTABLES -A INPUT -p tcp -j ACCEPT --dport 22 -m state --state NEW # Accept inbound packets which initiate FTP sessions $IPTABLES -A INPUT -p tcp -j ACCEPT --dport 21 -m state --state NEW # Accept inbound packets which initiate HTTP sessions $IPTABLES -A INPUT -p tcp -j ACCEPT --dport 80 -m state --state NEW # Log and drop anything not accepted above # (Obviously we want to log any packet that doesn't match any ACCEPT rule, for # both security and troubleshooting. Note that the final "DROP" rule is # redundant if the default policy is already DROP, but redundant security is # usually a good thing.) # $IPTABLES -A INPUT -j LOG --log-prefix "Dropped by default (INPUT):" $IPTABLES -A INPUT -j DROP # OUTBOUND POLICY # (Applies to packets sent to the network interface (NOT loopback) # from local processes) # If it's part of an approved connection, let it out $IPTABLES -I OUTPUT 1 -m state --state RELATED,ESTABLISHED -j ACCEPT # Allow outbound ping # (For testing only! If someone compromises your system they may attempt to use ping to identify other active IP addresses on the DMZ. Comment this rule out when you don't need to use it yourself!) # # $IPTABLES -A OUTPUT -p icmp -j ACCEPT --icmp-type echo-request # Allow outbound DNS queries, e.g. to resolve IPs in logs # (Many network applications break or radically slow down if they # can't use DNS. Although DNS queries usually use UDP 53, they may also use TCP # 53. Although TCP 53 is normally used for zone-transfers, DNS queries with # replies greater than 512 bytes also use TCP 53, so we'll allow both TCP and UDP # 53 here # $IPTABLES -A OUTPUT -p udp --dport 53 -m state --state NEW -j ACCEPT $IPTABLES -A OUTPUT -p tcp --dport 53 -m state --state NEW -j ACCEPT # Log & drop anything not accepted above; if for no other reason, for troubleshooting # # NOTE: you might consider setting your log-checker (e.g. Swatch) to # sound an alarm whenever this rule fires; unexpected outbound trans- # actions are often a sign of intruders! # $IPTABLES -A OUTPUT -j LOG --log-prefix "Dropped by default (OUTPUT):" $IPTABLES -A OUTPUT -j DROP # Log & drop ALL incoming packets destined anywhere but here. # (We already set the default FORWARD policy to DROP. But this is # yet another free, reassuring redundancy, so why not throw it in?) # $IPTABLES -A FORWARD -j LOG --log-prefix "Attempted FORWARD? Dropped by default:" $IPTABLES -A FORWARD -j DROP ;; # Unload filters and reset default policies to ACCEPT. # FOR LAB/SETUP/BENCH USE ONLY -- else use `stop'!! # Never run this script `wide_open' if the system is reachable from # the Internet! # wide_open) echo -n "DANGER!! Unloading Woofgang's Packet Filters!!" $IPTABLES --flush $IPTABLES -P INPUT ACCEPT $IPTABLES -P FORWARD ACCEPT $IPTABLES -P OUTPUT ACCEPT ;; stop) echo -n "Portcullis rope CUT..." # Unload all fw rules, leaving default-drop policies $IPTABLES --flush ;; status) echo "Querying iptables status (via iptables --list)..." $IPTABLES --line-numbers -v --list ;; *) echo "Usage: $0 {start|stop|wide_open|status}" exit 1 ;; esac
The second script is, according to my own assertions in Chapter 3, actually beyond the scope of this book: it's for a multihomed firewall system. But even though this book is about bastion hosts, and even though many of the things in this script are not described elsewhere in the book, I wanted to at least show a sample firewall configuration.
Like the previous script, it's copiously commented, but if you really want to learn how to build Linux firewalls, you'd be well advised to read the official Netfilter documentation, the iptables(8) manpage, or a book dedicated to Linux firewalls.
Again, the example scenario used below is the one described in Chapter 3 under "Every System Can Be Its Own Firewall: Using IPTables For Local Security." This example is admittedly somewhat unrealistic: the DMZ contains no DNS or SMTP servers, so all internal hosts are allowed to send email outward, and I haven't addressed the issue of inbound email at all (if I did, there would be an SMTP gateway in the DMZ, and only that host would receive SMTP traffic from the Internet). The services that are illustrated in Example A-2 should be enough to help you figure out how to accommodate others that are not.
#! /bin/sh # init.d/masterfw # # System startup script for packet filters on a three-homed SuSE 7.1 # Linux firewall (Internal network, DMZ network, External network). # # IMPORTANT BACKGROUND ON THIS EXAMPLE: the internal network is numbered # 192.168.100.0/24; the DMZ network is 208.13.201.0/29; and the external # interface is 208.13.201.8/29. The firewall's respective interface IP # addresses are 192.168.100.1, 208.13.201.1, and 208.13.201.9. # # All traffic originating on the internal network is hidden behind the # firewall, i.e. internal packets destined for DMZ hosts are given the # source IP 208.13.201.1 and those destined for the Internet are given # the source IP 208.13.201.9. # # In the interest of minimizing confusion here, traffic between the DMZ and # the Internet is not "NATted," (though it's certainly a good idea # to use NATted RFC 1918 IP addresses on your DMZ, or even to NAT non-RFC # 1918 addresses in order to add a little obscurity to your security ;-) # # Structurally based on SuSE 7.1's /etc/init.d/skeleton, by Kurt Garloff # # The following 9 lines are SuSE-specific # ### BEGIN INIT INFO # Provides: localfw # Required-Start: $network $syslog # Required-Stop: $network $syslog # Default-Start: 2 3 5 # Default-Stop: 0 1 2 6 # Description: Start localfw to protect local heinie ### END INIT INFO # /End SuSE-specific section # Let's save typing & confusion with some variables. # These are NOT SuSE-specific in any way. NET_INT=192.168.100.0/24 NET_DMZ=208.13.201.0/29 IFACE_INT=eth0 IFACE_DMZ=eth1 IFACE_EXT=eth2 IP_INT=192.168.100.1 IP_DMZ=208.13.201.1 IP_EXT=208.13.201.9 WOOFGANG=208.13.201.2 IPTABLES=/usr/sbin/iptables test -x $IPTABLES || exit 5 # The next 42 lines are SuSE-specific # Source SuSE config # (file containing system configuration variables, though in SuSE 8.0 this # has been split into a number of files in /etc/rc.config.d) . /etc/rc.config # Determine the base and follow a runlevel link name. base=${0##*/} link=${base#*[SK][0-9][0-9]} # Force execution if not called by a runlevel directory. test $link = $base && START_LOCALFW=yes test "$START_LOCALFW" = yes || exit 0 # Shell functions sourced from /etc/rc.status: # rc_check check and set local and overall rc status # rc_status check and set local and overall rc status # rc_status -v ditto but be verbose in local rc status # rc_status -v -r ditto and clear the local rc status # rc_failed set local and overall rc status to failed # rc_reset clear local rc status (overall remains) # rc_exit exit appropriate to overall rc status . /etc/rc.status # First reset status of this service rc_reset # Return values acc. to LSB for all commands but status: # 0 - success # 1 - misc error # 2 - invalid or excess args # 3 - unimplemented feature (e.g. reload) # 4 - insufficient privilege # 5 - program not installed # 6 - program not configured # 7 - program is not running # # Note that starting an already running service, stopping # or restarting a not-running service as well as the restart # with force-reload (in case signalling is not supported) are # considered a success. # /End SuSE-specific stuff. # The rest of this script is non-SuSE specific case "$1" in start) echo -n "Loading Firewall's Packet Filters" # SETUP # Load kernel modules first modprobe ip_tables modprobe ip_conntrack_ftp modprobe iptable_nat modprobe ip_nat_ftp # Flush old rules, old custom tables $IPTABLES --flush $IPTABLES --delete-chain $IPTABLES --flush -t nat $IPTABLES --delete-chain -t nat # Set default-deny policies for all three default chains $IPTABLES -P INPUT DROP $IPTABLES -P FORWARD DROP $IPTABLES -P OUTPUT DROP # Give free reign to loopback interfaces $IPTABLES -I INPUT 1 -i lo -j ACCEPT $IPTABLES -I OUTPUT 1 -o lo -j ACCEPT # Do some rudimentary anti-IP-spoofing drops on INPUT chain # $IPTABLES -A INPUT -s 192.168.0.0/16 -i $IFACE_EXT -j LOG --log-prefix #"Spoofed source IP " $IPTABLES -A INPUT -s 192.168.0.0/16 -i $IFACE_EXT -j DROP $IPTABLES -A INPUT -s 172.16.0.0/12 -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A INPUT -s 172.16.0.0/12 -j DROP $IPTABLES -A INPUT -s 10.0.0.0/8 -j LOG --log-prefix " Spoofed source IP " $IPTABLES -A INPUT -s 10.0.0.0/8 -j DROP $IPTABLES -A INPUT -s ! $NET_DMZ -i $IFACE_DMZ -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A INPUT -s ! $NET_DMZ -i $IFACE_DMZ -j DROP $IPTABLES -A INPUT -s ! $NET_INT -i $IFACE_INT -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A INPUT -s ! $NET_INT -i $IFACE_INT -j DROP $IPTABLES -A INPUT -s $NET_DMZ -i $IFACE_EXT -j LOG --log-prefix " Spoofed source IP " $IPTABLES -A INPUT -s $NET_DMZ -i $IFACE_EXT -j DROP $IPTABLES -A INPUT -s $IP_INT -i $IFACE_INT -j LOG --log-prefix #"Spoofed source IP (firewall's ) " $IPTABLES -A INPUT -s $IP_INT -i $IFACE_INT -j DROP $IPTABLES -A INPUT -s $IP_DMZ -i $IFACE_DMZ -j LOG --log-prefix #"Spoofed source IP (firewall's ) " $IPTABLES -A INPUT -s $IP_DMZ -i $IFACE_DMZ -j DROP $IPTABLES -A INPUT -s $IP_EXT -i $IFACE_EXT -j LOG --log-prefix "Spoofed source IP (firewall's ) " $IPTABLES -A INPUT -s $IP_EXT -i $IFACE_EXT -j DROP # Do the same rudimentary anti-IP-spoofing drops on FORWARD chain # $IPTABLES -A FORWARD -s 192.168.0.0/16 -i $IFACE_EXT -j LOG --log-prefix " Spoofed source IP " $IPTABLES -A FORWARD -s 192.168.0.0/16 -i $IFACE_EXT -j DROP $IPTABLES -A FORWARD -s 172.16.0.0/12 -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A FORWARD -s 172.16.0.0/12 -j DROP $IPTABLES -A FORWARD -s 10.0.0.0/8 -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A FORWARD -s 10.0.0.0/8 -j DROP $IPTABLES -A FORWARD -s ! $NET_DMZ -i $IFACE_DMZ -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A FORWARD -s ! $NET_DMZ -i $IFACE_DMZ -j DROP $IPTABLES -A FORWARD -s ! $NET_INT -i $IFACE_INT -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A FORWARD -s ! $NET_INT -i $IFACE_INT -j DROP $IPTABLES -A FORWARD -s $NET_DMZ -i $IFACE_EXT -j LOG --log-prefix "Spoofed source IP " $IPTABLES -A FORWARD -s $NET_DMZ -i $IFACE_EXT -j DROP $IPTABLES -A FORWARD -s $IP_INT -i $IFACE_INT -j LOG --log-prefix "Spoofed source IP (firewall's) " $IPTABLES -A FORWARD -s $IP_INT -i $IFACE_INT -j DROP $IPTABLES -A FORWARD -s $IP_DMZ -i $IFACE_DMZ -j LOG --log-prefix "Spoofed source IP (firewall's) " $IPTABLES -A FORWARD -s $IP_DMZ -i $IFACE_DMZ -j DROP $IPTABLES -A FORWARD -s $IP_EXT -i $IFACE_EXT -j LOG --log-prefix "Spoofed source IP (firewall's) " $IPTABLES -A FORWARD -s $IP_EXT -i $IFACE_EXT -j DROP # INBOUND POLICY # Accept inbound packets that are part of previously-OK'ed sessions $IPTABLES -A INPUT -j ACCEPT -m state --state ESTABLISHED,RELATED # Tell netfilter that all TCP sessions must begin with SYN $IPTABLES -A INPUT -p tcp ! --syn -m state --state NEW -j LOG --log-prefix "Stealth scan attempt?" $IPTABLES -A INPUT -p tcp ! --syn -m state --state NEW -j DROP # Accept packets initiating SSH sessions from internal network to firewall $IPTABLES -A INPUT -p tcp -s $NET_INT --dport 22 -m state --state NEW -j ACCEPT # Log anything not accepted above $IPTABLES -A INPUT -j LOG --log-prefix "Dropped by default (INPUT):" $IPTABLES -A INPUT -j DROP # OUTBOUND POLICY # If it's part of an approved connection, let it out $IPTABLES -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT # Allow outbound ping (comment-out when not needed!) # $IPTABLES -A OUTPUT -p icmp -j ACCEPT # Allow outbound DNS queries, e.g. to resolve IPs in logs $IPTABLES -A OUTPUT -p udp --dport 53 -j ACCEPT # Allow outbound HTTP for Yast2 Online Update $IPTABLES -A OUTPUT -p tcp --dport 80 -j ACCEPT # Log anything not accepted above $IPTABLES -A OUTPUT -j LOG --log-prefix "Dropped by default (OUTPUT):" $IPTABLES -A OUTPUT -j DROP # FORWARD POLICY # If it's part of an approved connection, let it out $IPTABLES -I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT # Tell netfilter that all TCP sessions must begin with SYN $IPTABLES -A FORWARD -p tcp ! --syn -m state --state NEW -j LOG --log-prefix "Stealth scan attempt?" $IPTABLES -A FORWARD -p tcp ! --syn -m state --state NEW -j DROP # Allow all access to Woofgang's web sites $IPTABLES -A FORWARD -p tcp -d $WOOFGANG --dport 80 -m state --state NEW -j ACCEPT # Allow all access to Woofgang's FTP sites $IPTABLES -A FORWARD -p tcp -d $WOOFGANG --dport 21 -m state --state NEW,RELATED -j ACCEPT # Allow dns from Woofgang to external DNS servers $IPTABLES -A FORWARD -p udp -s $WOOFGANG -m state --state NEW,RELATED --dport 53 -j ACCEPT # NOTE: the next few rules reflect a restrictive stance re. internal users: # only a few services are allowed outward from the internal network. # This may or may not be politically feasible in your environment, i.e., you # really shouldn't "allow all outbound," but sometimes you have no choice. # Allow dns queries from internal hosts to external DNS servers # NOTE: in practice this rule should be source-restricted to internal DNS # servers (that perform recursive queries on behalf of internal users) # $IPTABLES -A FORWARD -p udp -s $NET_INT -m state --state NEW,RELATED --dport 53 -j ACCEPT # Allow FTP from internal hosts to the outside world $IPTABLES -A FORWARD -p tcp -s $NET_INT -m state --state NEW,RELATED --dport 21 -j ACCEPT # Allow HTTP from internal hosts to the outside world $IPTABLES -A FORWARD -p tcp -s $NET_INT -m state --state NEW --dport 80 -j ACCEPT # Allow HTTPS from internal hosts to the outside world $IPTABLES -A FORWARD -p tcp -s $NET_INT -m state --state NEW --dport 443 -j ACCEPT # Allow SMTP from internal hosts to the outside world # NOTE: in practice this should be source-restricted to internal mail servers # $IPTABLES -A FORWARD -p tcp -s $NET_INT -m state --state NEW --dport 25 -j ACCEPT # Allow SSH from internal hosts to Woofgang # NOTE: in practice this should be source-restricted to internal admin systems # $IPTABLES -A FORWARD -p tcp -s $NET_INT -d $WOOFGANG -m state --state NEW --dport 22 -j ACCEPT # Log anything not accepted above - if nothing else, for t-shooting $IPTABLES -A FORWARD -j LOG --log-prefix "Dropped by default (FORWARD):" $IPTABLES -A FORWARD -j DROP # NAT: Post-Routing # Hide internal network behind firewall $IPTABLES -t nat -A POSTROUTING -s $NET_INT -o $IFACE_EXT -j SNAT --to-source $IP_EXT $IPTABLES -t nat -A POSTROUTING -s $NET_INT -o $IFACE_DMZ -j SNAT --to-source $IP_DMZ # Remember status and be verbose rc_status -v ;; # The following commented-out section is active in Example A-1 but # SHOULD NOT BE USED on a live firewall. (It's only here so I can tell you not # to use it!) Sometimes you can justify turning off packet filtering on a # bastion host, but NEVER on a firewall # wide_open) # echo -n "DANGER!! Unloading firewall's Packet Filters! ARE YOU MAD?" # # $IPTABLES --flush # $IPTABLES -P INPUT ACCEPT # $IPTABLES -P FORWARD ACCEPT # $IPTABLES -P OUTPUT ACCEPT # Remember status and be verbose rc_status -v ;; # Unload all fw rules, leaving default-drop policies stop) echo -n "Stopping the firewall (in a closed state)!" $IPTABLES --flush # Remember status and be quiet rc_status ;; status) echo "Querying iptables status..." echo " (actually doing iptables --list)..." $IPTABLES --list; rc=$? if test $rc = 0; then echo "OK" else echo "Hmm, that didn't work for some reason. Bummer." fi #rc_status ;; *) echo "Usage: $0 {start|stop|status}" exit 1 ;; esac rc_exit