Enhancing Security for FreeBSD Using IPFW and SSHGuard

Updated on August 28, 2015
Enhancing Security for FreeBSD Using IPFW and SSHGuard header image

VPS servers are frequently targeted by intruders. A common type of attack shows up in system logs as hundreds of unauthorized ssh login attempts. Setting up a firewall is very useful, but by itself may not adequately control disruptive intrusion attempts.

This tutorial shows how to construct an enhanced intrusion barrier for FreeBSD using two programs, the ipfw firewall and sshguard. SSHGuard is a small add-on program that monitors system logs for "abusive" entries. When offenders attempt to gain access, sshguard instructs ipfw to block traffic originating from the offender's IP address. The offender is then effectively shut out.

Once it's understood how these programs work, managing server protection is pretty simple. Though this guide is focused on configuring FreeBSD, parts of it apply to other OS and firewall software.

Step 1. Configuring IPFW

FreeBSD provides 3 firewalls in its default (GENERIC) kernel, ipfw, pf, and ipfilter. Each has advantages and fans, but ipfw is FBSD's native firewall software and pretty straightforward to use for our purposes. It's worth noting that ipfw does many things as its man page shows, however capabilities such as NAT, traffic shaping, etc., aren't needed for the typical VPS situation. Fortunately, the firewall's basic features easily meet our requirements.

To start the firewall at boot up time, add the following to /etc/rc.conf :

firewall_enable="YES"
firewall_script="/usr/local/etc/IPFW.rules"
firewall_logging="YES"

The service command is available to start/stop the firewall manually:

[user@vultr ~]$ sudo service ipfw start

Naturally, ipfw won't do anything until it adds rules, often from a file, in this example located at /usr/local/etc/IPFW.rules. The rules file could in fact be located anywhere or have any name, as long as it matches the "firewall_script" parameter. The rules file is described in detail below.

Step 2. Install and configure SSHGuard

sshguard comes in several flavors for use with different firewalls. Use the pkg utility to fetch and install sshguard-ipfw:

[user@vultr ~]$ sudo pkg install sshguard-ipfw

In most cases that's all one needs to do. The appropriate variable is automatically inserted into /etc/rc.conf for starting on boot up:

sshguard_enable="YES"

The defaults normally work well. If different values are necessary, the sshguard man page gives detailed info about the parameters:

# sshguard--program defaults, so don't need to be in rc.conf unless assigning different value
# sshguard_pidfile="/var/run/sshguard.pid"
# sshguard_watch_logs="/var/log/auth.log:/var/log/mail"
# sshguard_blacklist="40:/var/db/sshguard/blacklist.db"
# sshguard_safety_thresh="40"
# sshguard_pardon_min_interval="420"
# sshguard_prescribe_interval="1200"

You can start sshguard with the usual service invocation:

[user@vultr ~]$ sudo service sshguard start

Step 3. Create a rules script

The hardest part is creating the firewall ruleset. ipfw can make use of the provided /etc/rc.firewall script, but it has to be modified to accommodate SSHGuard, as well as different operational scenarios. A number of web pages and FreeBSD Manual have useful information about doing this. However, writing a rules file isn't that hard, besides, a custom ruleset can be easier to understand and change when necessary.

An important feature of ipfw rules is that first match wins which means rule order is important. In ipfw, each rule is a command, and the rule file is an executable shell script. That allows the ruleset to be changed by altering rules then running the rules file as the shell script it is:

[user@vultr /usr/local/etc]$ sudo ./IPFW.rules

Generally, a rules file will define a variable for the ipfw command, then clear the current rules, issue generic rules, then proceed to set "out" rules, followed by "in" rules. The ipfw manual page and other resources contain a wealth of information about rule structure and options which are numerous to say the least.

Since the FreeBSD sshguard version has been updated to version 1.6.2, the method of inserting blocking rules for offenders has changed. Now the offenders' addresses are kept in an ipfw table (table 22 to be specific), rather than inserted into the rules above 55000 as before.

Fortunately, it's pretty simple to set up the rules file to use the table. It's just a matter of putting the table rule in the right place, and making sure to use the correct syntax when writing the rule.

When sshguard finds an offender, it puts the offender's address into its block list, and also inserts the address into the ipfw table so it will "trigger" denying access. This rule will accomplish these purposes:

01000 deny ip from table\(22\) to any

It's still necessary to put rules allowing inbound services above 01000 in this case. For example, let's say address 10.20.30.40 is an offender in table 22, and we have this ipfw rule:

56420 allow tcp from any to me dst-port 22 in via $vif

Since ipfw encounters rule 01000 before rule 56420, 10.20.30.40 is blocked. It won't ever be seen by the "allow 22 in" rule at all. If the allow rule had a "regular" number like 00420, the bad traffic would be let in and never blocked (because 00420 is less than 01000 and "first match wins").

A nice feature of the updated version is that now when sshguard starts up all the addresses in the block list are added to the table and are available to block incoming offenders without delay. The block list is cumulative and retained between sessions.

At this point it's probably sensible to show a complete ipfw ruleset modified for sshguard. The comments should make it pretty easy to follow the rule logic:

#!/bin/sh

# ipfw config/rules
# from FBSD Handbook, rc.firewall, et. al.

# Flush all rules before we begin.
ipfw -q -f flush

# Set rules command prefix
cmd="ipfw -q add "

vif="vtnet0"

# allow all for localhost
$cmd 00010 allow ip from any to any via lo0

# checks stateful rules.  If marked as "keep-state" the packet has
# already passed through filters and is "OK" without futher
# rule matching
$cmd 00101 check-state

# allow DNS out
$cmd 00110 allow tcp from me to any dst-port 53 out via $vif setup keep-state
$cmd 00111 allow udp from me to any dst-port 53 out via $vif keep-state

# allow dhclient connection out (port numbers are important)
$cmd 00120 allow udp from me 68 to any dst-port 67 out via $vif keep-state

# allow HTTP HTTPS replies
$cmd 00200 allow tcp from any to any dst-port 80 out via $vif setup keep-state
$cmd 00220 allow tcp from any to any dst-port 443 out via $vif setup keep-state

# allow outbound mail
$cmd 00230 allow tcp from any to any dst-port 25 out via $vif setup keep-state
$cmd 00231 allow tcp from any to any dst-port 465 out via $vif setup keep-state
$cmd 00232 allow tcp from any to any dst-port 587 out via $vif setup keep-state

# allow icmp re: ping, et. al. 
# comment this out to disable ping, et.al.
$cmd 00250 allow icmp from any to any out via $vif keep-state

# alllow timeserver out
$cmd 00260 allow tcp from any to any dst-port 37 out via $vif setup keep-state

# allow ntp out
$cmd 00270 allow udp from any to any dst-port 123 out via $vif keep-state

# allow outbound SSH traffic
$cmd 00280 allow tcp from any to any dst-port 22 out via $vif setup keep-state

# otherwise deny outbound packets
# outbound catchall.  
$cmd 00299 deny log ip from any to any out via $vif

# inbound rules
# deny inbound traffic to restricted addresses
$cmd 00300 deny ip from 192.168.0.0/16 to any in via $vif
$cmd 00301 deny ip from 172.16.0.0/12 to any in via $vif
$cmd 00302 deny ip from 10.0.0.0/8 to any in via $vif
$cmd 00303 deny ip from 127.0.0.0/8 to any in via $vif
$cmd 00304 deny ip from 0.0.0.0/8 to any in via $vif
$cmd 00305 deny ip from 169.254.0.0/16 to any in via $vif
$cmd 00306 deny ip from 192.0.2.0/24 to any in via $vif
$cmd 00307 deny ip from 204.152.64.0/23 to any in via $vif
$cmd 00308 deny ip from 224.0.0.0/3 to any in via $vif

# deny inbound packets on these ports
# auth 113, netbios (services) 137/138/139, hosts-nameserver 81 
$cmd 00315 deny tcp from any to any dst-port 113 in via $vif
$cmd 00320 deny tcp from any to any dst-port 137 in via $vif
$cmd 00321 deny tcp from any to any dst-port 138 in via $vif
$cmd 00322 deny tcp from any to any dst-port 139 in via $vif
$cmd 00323 deny tcp from any to any dst-port 81 in via $vif

# deny partial packets
$cmd 00330 deny ip from any to any frag in via $vif
$cmd 00332 deny tcp from any to any established in via $vif

# allowing icmp re: ping, etc.
$cmd 00310 allow icmp from any to any in via $vif

# allowing inbound mail, dhcp, http, https
$cmd 00350 allow udp from any 53 to me in via $vif
$cmd 00360 allow tcp from any 53 to me in via $vif
$cmd 00370 allow udp from any 67 to me dst-port 68 in via $vif keep-state
    
$cmd 00400 allow tcp from any to me dst-port 80 in via $vif setup limit src-addr 2
$cmd 00410 allow tcp from any to me dst-port 443 in via $vif setup limit src-addr 2

# SSHguard puts offender addresses in table 22. Set up the table rule
# Please note the '\(22\)' syntax, necessary since it's run as shell command
$cmd 01000 deny ip from table\(22\) to any

# allow inbound ssh, mail. PROTECTED SERVICES: numbered ABOVE sshguard block list range 
$cmd 56420 allow tcp from any to me dst-port 22 in via $vif setup limit src-addr 2
$cmd 56530 allow tcp from any to any dst-port 25 in via $vif setup keep-state
$cmd 56531 allow tcp from any to any dst-port 465 in via $vif setup keep-state
$cmd 56532 allow tcp from any to any dst-port 587 in via $vif setup keep-state

# deny everything else, and log it
# inbound catchall
$cmd 56599 deny log ip from any to any in via $vif

# ipfw built-in default, don't uncomment
# $cmd 65535 deny ip from any to any

Step 4. Startup and testing

System needs vary and different choices of ports to block or unblock are reflected in the ruleset. Once the ruleset is finished, save the file to /usr/local/etc/IPFW.rules, and start the FBSD services:

 # service ipfw start
 # service sshguard start

The augmented firewall should now be running! Check sshguard:

 [user@vultr ~]$ sudo pgrep -lfa ssh

If sshguard is running, its pid and full command line are displayed:

720 /usr/local/sbin/sshguard -b 40:/var/db/sshguard/blacklist.db -l /var/log/auth.log -l /var/log/maillog -a 40 -p 420 -s 1200 -w /usr/local/etc/sshguard.whitelist -i /var/run/sshguard.pid

This shows the firewall ruleset with stats and the last time a packet matched the rule:

 [user@vultr ~]$ sudo ipfw -cat list

After hours or days, addresses of offenders are added to the block list and also table 22. To view all the addresses in the table, use this command:

ipfw table 22 list

The result is printed as:

10.10.10.118/32 0
10.10.10.72/32 0
...

As described above, connections from these addresses are disallowed. Of course, on first running sshguard there won't be any addresses in the list, but over time it can get rather long. One option is creating separate blocking rules for addresses with multiple entries in the table and then deleting them from the block list.

Step 5. Keeping vigilant...

It's a good idea to occasionally check logs to make sure intrusions are controlled. Generally, /var/log/auth.log and /var/log/security are informative. Gaps or errors in covering network services might become apparent. Modifying the firewall ruleset as needed is a normal part of server administration.

In prior sshguard versions, when the /var/db/sshguard/blacklist.db file had grown large, it could prevent sshguard from starting on system boot up. Removing or renaming the block list file allowed sshguard to start. This issue appears to be fixed in the latest sshguard version, so this workaround is probably no longer necessary.

Make sure to allow the IP Address you are connected to the SSH Session from. If you accidentally lock yourself out, you can always connect to the noVNC Console in https://my.vultr.com and allow your IP.

Summing up, using the combination of ipfw and sshguard helps keep your FreeBSD system secure and doing its job. Minimizing intrusive network activity has an added benefit: less "noise" makes it easier to track and tune system operation, contributing to a safer, better-running server.

Effectively protecting a FreeBSD system/server isn't especially complicated. While a modest effort is required to get it up and running, it pays off in substantially greater VPS and project security.