How to configure a local firewall for OpenVPN

  Subscribe
4/20/2010 - Marco (updated on 9/18/2020)

The following tip was developed using Ubuntu 9.1x (Hardy Heron) with OpenVPn 2.1rc19.


Once you're done with this tutorial, make sure to read Part II, which includes some crucial updates.

There are dozens of guides around that describe how to optimally configure the iptables firewall on Linux for OpenVPN. There's even a script installed by default that is extremely well-commented and shows to how close down the firewall, then open up only very selected ports and protocols for optimal browsing. However, all of those guides assume that the machine on which OpenVPN is installed is also the firewall separating an external network (the DMZ) from an internal one. Well, what if you have a dedicated firewall and run the OpenVPN server on a machine running in the internal network?

This tutorial assumes that you've already followed the instructions for setting up OpenVPN and that you've also set up a Public Key Infrastructure (PKI). That means that access to your internal network via OpenVPN is secured and will only authorize users that have a proper certificate and password.

All of the files and scripts mentioned in this tutorial are available for download as files at the end of the article.

Access for all!

Since the external firewall routes requests to OpenVPN directly to the internal machine, it cannot be used to restrict the actions of users that are tunneling into the internal network. Luckily, the default behavior is that users only have access to the OpenVPN server itself, which gives you time to consider how, exactly, you want to open things up.

Here are some questions you need to answer:

  1. To which machines should users have access?
  2. On which ports and protocols should users have access?
  3. Which users should have which access?

For many organizations, the whole point of using OpenVPN is to let users work as if they are on the internal network, but from outside the physical office. In that case, the answers to the questions above will in many cases be:

  1. All of them
  2. All of them
  3. All users should have all access

Let's take care of that trivial case first, then. Execute sudo iptables -nL to show the current firewall configuration. You should see something like the following:

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

This table indicates that all input, output and forward requests are accepted. OUTPUT requests are not interesting for this exercise, as they are generated by software running on the server itself, but INPUT and FORWARD requests bear more scrutiny. It looks like the firewall is already configured to allow access to everything your users need: There are no restrictions on inputs, which means that the firewall will allow requests on all ports and protocols for the local machine. There are likewise no restrictions on forwards, which means that requests to other IP addresses in the same subnet will be forwarded to those machines.

So, if FORWARDS are being, well, forwarded, why can't you ping any other machines in the same subnet? Once you know the answer, it's obvious: It's because the firewall isn't the one blocking forward requests. It's because IP forwarding is a networking feature that must be explicitly enabled in the networking configuration. The article How to enable IP Forwarding will help you get this option configured, but the crux of the change is shown below.

Since you'll probably want to make this change permanent, execute sudo vi /etc/sysctl.conf and remove the comment from the front of the line containing net.ipv4.ip_forward = 1. Restart networking by executing sudo /etc/init.d/networking restart and you'll be good to go.

VIP Members Only.

The default network is now set up for smaller installations where everybody has the same permissions everywhere. What if, however, your needs are a little more complex? What if you have some users on your VPN that should only have access to certain resources i.e. certain ports and protocols?

In that case, you'll have to use a different approach: Perhaps something like the following:

  1. Close the firewall by default, including access to the OpenVPN machine itself. That means that authorized users will be able to establish a tunnel using OpenVPN, but that they won't be able to do anything else until the firewall is opened again in the ensuing steps.
  2. Determine which user has connected/authorized through OpenVPN.
  3. Determine the set of IP addresses/ports/protocols to which that user should be given access.
  4. Open the firewall for only those IP addresses/ports/protocols and only from the client IP address for that user's current tunneling session.
  5. When the user disconnects, close the firewall for that client IP address.

The first step is to close the firewall by default. As you can see from the iptables listing above, the firewall accepts all INPUT connections by default. You're probably not an expert on iptables configuration (or you wouldn't be here). There are two ways to get the settings you need:

  1. Execute shell commands to iptables to set up the firewall
  2. Import an iptables configuration from a dump file

There's really not much difference, but this tutorial opted for the second option. Once you've got a default firewall set up to your liking, use iptables-save to dump out the rules to a file named /etc/iptables.uprules (naturally, you can use whatever file name you like; it just has to match the reference from the script below). If this is all very confusing, the values below set up a closed firewall for you, which is probably what you want.

*filter
:INPUT DROP [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i eth0 -j ACCEPT
-A INPUT -i lo -j ACCEPT
COMMIT

Though FORWARD and OUTPUT are still accepted unconditionally, all requests to INPUT are dropped. The two rules for eth0 and lo make sure that the machine can communicate with itself. Now that you've got the rules you need, you want to somehow alter the default configuration of the firewall.

If you guessed that the next step is to edit /etc/iptables/default.conf or /etc/default/iptables.conf, you'd be wrong. That's pretty intuitive, but wrong. On the latest versions of Ubuntu, networking setup like firewall configuration is best accomplished by adding a script that is executed just before the networking interface is established. This guarantees that the default firewall rules are in place before the network is in any way accessible. To do this, add a file called iptables.sh to the /etc/network/if-pre-up.d/ folder; Add the following lines to it:

#!/bin/sh

iptables-restore < /etc/iptables.uprules

exit 0

This is a super-simple script that loads the firewall configuration from the file you just created above. The iptables-restore command is convenient because it replaces the whole configuration, so you don't have to do any resetting of your own.

Save the file and execute sudo chmod +x /etc/netwokr/if-pre-up.d/iptables.sh to make it executable. Restart networking by executing sudo /etc/init.d/networking restart.

A call to sudo iptables -nL should now elicit the following output (the main changes are highlighted):

Chain INPUT (policy DROP)
target     prot opt source               destination
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Congratulations! You've succeeded in locking out everybody again, but in a different way.

Separating the Wheat from the Chaff

How do you get back that coveted VIP status that you had just seconds ago? Now you're up to step (2) above: "Determine the user connected through OpenVPN". The basic strategy here is to key on the unique name in the SSL certificate authorized by OpenVPN. For each different group of permissions (IP addresses/ports/protocols) that you want to grant, create a file with the names of people who belong to that group, one name per line. For example:

Joe_Jackson
Phil_Hartman
Jill_Meikenson
Horst_Buchholz
Susan_B_Lazy

This is just one very simple solution to the problem of determining membership. Some installations with much larger user bases might want to instead bind to an external lookup using LDAP or an already existing MySQL database or something similar. That's obviously beyond the scope of this tutorial, though.

You're now going to need a script that will use these lists to determine which firewall rules to execute. I've added the general form of that script below, with matching for "employees" and "strangers" and TODO statements indicating where you need to extend the script for your own purposes:

function inlist
{
  less `dirname $0`/$1 | egrep "^${CLIENTCERT}$" > /dev/null
  if [ $? -eq 0 ]; then
    return 0
  else
    return 1
  fi
}

function get_next_matching_firewall_rule
{
  ip_address=$1

  RULE="`iptables -L INPUT -n --line-numbers | grep $ip_address | head -n 1`"
}

function drop_rule_from_iptables
{
  rule="$1"
  echo "  Drop rule [$rule]"
  line_number=`echo "$rule" | awk '{print $1}'`
  iptables -D INPUT $line_number
}

function add_port_to_iptables
{
  source_ip=$1
  destination_ip=$2
  protocol=$3
  port=$4

  iptables -A INPUT -i tun0 -s $source_ip -d $destination_ip -p $protocol --dport $port -j ACCEPT
}

function add_destination_to_iptables
{
  source_ip=$1
  destination_ip=$2
  iptables -A INPUT -i tun0 -s $source_ip -d $destination_ip -j ACCEPT
}

function open_firewall_for_strangers
{
  echo "  Add route for DNS"
  add_port_to_iptables $CLIENTIP 192.168.1.1 "UDP" 53

  echo "  Add route for Windows shares"
  add_port_to_iptables $CLIENTIP 192.168.1.5 "TCP" 139
  add_port_to_iptables $CLIENTIP 192.168.1.5 "TCP" 445

  return 0
}

function open_firewall_for_employees
{
  echo "  Add routes for all ip addresses"
  iptables -A INPUT -i tun0 -s $CLIENTIP -j ACCEPT
  return 0
}

function open_firewall
{
  echo "Opening firewall for $CLIENTCERT @ [$CLIENTIP]"
# TODO Add filtering for other lists, if desired
# inlist "MYGROUP.list"
#if [ $? -eq 0 ]; then
#  echo "  Certificate found in MYGROUP list"
#  open_firewall_for_MYGROUP
#  return 0
#else
  inlist "strangers.list"
  if [ $? -eq 0 ]; then
    echo "  Certificate found in strangers list"
    open_firewall_for_strangers
    return 0
  else
    inlist "employees.list"
    if [ $? -eq 0 ]; then
      echo "  Certificate found in employee list"
      open_firewall_for_employees
      return 0
    else
      echo "  Certificate not found in any list"
      return 1
    fi
  fi
}

function close_firewall
{
  echo "Closing firewall for [$CLIENTIP]"
  get_next_matching_firewall_rule $CLIENTIP

  while [ -n "$RULE" ]
  do
    drop_rule_from_iptables "$RULE"
    get_next_matching_firewall_rule $ip_address
  done
}

# Main

OPERATION=$1
CLIENTIP=$2
CLIENTCERT=$3

case "$1" in
  add)
    close_firewall
    open_firewall
    ;;

  update)
    close_firewall
    open_firewall
    ;;

  delete)
    close_firewall
    ;;
  *)
    echo "Unknown operation"
    exit 1
esac

exit $?

Some explanation for those who haven't scripted in bash much before:

  • The whole script executes from the case statement at the end of the script. Note that in all recognized cases, the firewall is first closed just to make sure that there are no lingering entries for the given client's IP address.
  • A call to close_firewall simply removes all rules for the given client's IP address, in which case the default DROP action on INPUTS will block all incoming traffic from the address.
  • A call to open_firewall tries to find the user in one of the files. If successful, the rules for that file are applied to the firewall.
  • In this case, if the user is a "stranger", they only have access the DNS server in the internal network (for name lookups) and to the Windows shares on one other machine.
  • If the user is an "employee", then the script adds a rule to allow all ports and all protocols to all IP addresses for that person and restores full access rights.
  • Again, there are a dozen ways of determining membership and it's doubtful that bash is the best language in which to program more complex membership tests. The best language to use is the one you know and the one that runs on your server. ;-)

Finally, you need to tell OpenVPN to run your script whenever it has authorized a connection. Execute sudo vi /etc/openvpn/server.conf and add or modify the following line:

learn-address /etc/openvpn/configfirewall.sh

Restart OpenVPN with sudo /etc/init.d/openvpn retart and you're done! Your OpenVPN server now not only authorizes users but also locks down the firewall to allow only those services for which a user has permission.

Files

Finally, here are samples of all of the files used in this tutorial.

  • iptables.sh: The script to execute when just before the network starts
  • iptables.uprules: The default rules to apply to the firewall
  • configfirewall.sh: The firewall configuration executed when a user connects or disconnects from OpenVPN
  • employees.list: A list of employees against which to match

Once you're done with this tutorial, make sure to read Part II, which includes some crucial updates.

Sign up for our Newsletter