Your browser may have trouble rendering this page. See supported browsers for more information.

This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.

Title

How to configure a local firewall for OpenVPN

Description

<n>The following tip was developed using Ubuntu 9.1x (Hardy Heron) with OpenVPn 2.1rc19. It was originally published on the <a href="http://encodo.com/en/blogs.php?entry_id=196">Encodo blogs</a> and cross-published here.</n> <hr> There are dozens of guides around that describe how to optimally configure the <c>iptables</c> 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 <i>the firewall</i> 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. <n>All of the files and scripts mentioned in this tutorial are available for download as files at the end of the article.</n> <h>Access for all!</h> 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: <ol> To which machines should users have access? On which ports and protocols should users have access? Which users should have which access? </ol> 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: <ol> All of them All of them All users should have all access </ol> Let's take care of that trivial case first, then. Execute <c>sudo iptables -nL</c> to show the current firewall configuration. You should see something like the following: <shell> 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 </shell> This table indicates that all input, output and forward requests are accepted. <c>OUTPUT</c> requests are not interesting for this exercise, as they are generated by software running on the server itself, but <c>INPUT</c> and <c>FORWARD</c> requests bear more scrutiny. It looks like the firewall is already configured to allow access to everything your users need: There are <i>no</i> 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 <i>forwards</i>, which means that requests to other IP addresses in the same subnet will be forwarded to those machines. So, if <c>FORWARDS</c> are being, well, <i>forwarded</i>, 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 <a href="http://linuxpoison.blogspot.com/2008/01/how-to-enable-ip-forwarding.html">How to enable IP Forwarding</a> 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 <c>sudo vi /etc/sysctl.conf</c> and remove the comment from the front of the line containing <c>net.ipv4.ip_forward = 1</c>. Restart networking by executing <c>sudo /etc/init.d/networking restart</c> and you'll be good to go. <h>VIP Members Only.</h> The default network is now set up for smaller installations where <i>everybody</i> has the <i>same</i> permissions <i>everywhere</i>. 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: <ol> 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. Determine which user has connected/authorized through OpenVPN. Determine the set of IP addresses/ports/protocols to which that user should be given access. Open the firewall for <i>only</i> those IP addresses/ports/protocols and only from the client IP address for that user's current tunneling session. When the user disconnects, close the firewall for that client IP address. </ol> The first step is to close the firewall by default. As you can see from the <c>iptables</c> listing above, the firewall accepts all <c>INPUT</c> connections by default. You're probably not an expert on <c>iptables</c> configuration (or you wouldn't be here). There are two ways to get the settings you need: <ol> Execute shell commands to <c>iptables</c> to set up the firewall Import an <c>iptables</c> configuration from a dump file </ol> 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 <c>iptables-save</c> to dump out the rules to a file named <c>/etc/iptables.uprules</c> (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. <code> *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 </code> Though <c>FORWARD</c> and <c>OUTPUT</c> are still accepted unconditionally, all requests to <c>INPUT</c> are dropped. The two rules for <c>eth0</c> and <c>lo</c> 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 <c>/etc/iptables/default.conf</c> or <c>/etc/default/iptables.conf</c>, 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 <c>iptables.sh</c> to the <c>/etc/network/if-pre-up.d/</c> folder; Add the following lines to it: <code> #!/bin/sh iptables-restore < /etc/iptables.uprules exit 0 </code> This is a super-simple script that loads the firewall configuration from the file you just created above. The <c>iptables-restore</c> 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 <c>sudo chmod +x /etc/netwokr/if-pre-up.d/iptables.sh</c> to make it executable. Restart networking by executing <c>sudo /etc/init.d/networking restart</c>. A call to <c>sudo iptables -nL</c> should now elicit the following output (the main changes are highlighted): <shell> Chain INPUT (policy <hl>DROP</hl>) target prot opt source destination <hl>ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0</hl> Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination </shell> Congratulations! You've succeeded in locking out everybody again, but in a different way. <h>Separating the Wheat from the Chaff</h> 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: <code> Joe_Jackson Phil_Hartman Jill_Meikenson Horst_Buchholz Susan_B_Lazy </code> 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: <code> 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]" <hl># 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</hl> 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 $? </code> Some explanation for those who haven't scripted in bash much before: <ul> The whole script executes from the <c>case</c> 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 <c>close_firewall</c> simply removes all rules for the given client's IP address, in which case the default <c>DROP</c> action on <c>INPUTS</c> will block all incoming traffic from the address. A call to <c>open_firewall</c> 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. ;-) </ul> Finally, you need to tell OpenVPN to run your script whenever it has authorized a connection. Execute <c>sudo vi /etc/openvpn/server.conf</c> and add or modify the following line: <code> learn-address /etc/openvpn/configfirewall.sh </code> Restart OpenVPN with <c>sudo /etc/init.d/openvpn retart</c> 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. <h>Files</h> Finally, here are samples of all of the files used in this tutorial. <ul> <a href="{att_link}iptables.sh">iptables.sh</a>: The script to execute when just before the network starts <a href="{att_link}iptables.uprules">iptables.uprules</a>: The default rules to apply to the firewall <a href="{att_link}configfirewall.sh">configfirewall.sh</a>: The firewall configuration executed when a user connects or disconnects from OpenVPN <a href="{att_link}employees.list">employees.list</a>: A list of employees against which to match </ul>