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 (Part II)

Description

<n>The following tip was developed using Ubuntu 9.1x (Hardy Heron) with OpenVPn 2.1rc19. It builds on the the setup from <a href="{app}/view_article.php?id=2336">Part I</a>.</n> <hr> <a href="{app}/view_article.php?id=2336">Part I</a> of this guide to configuring a local firewall for OpenVPN introduced you to using <c>iptables</c> on Linux. It also included a script for OpenVPN that opened and closed the firewall for specific IP addresses. If you haven't read it already, you should probably go do that first. Unfortunately, it turns out that the firewall configuration from part I is not watertight because it still allows <c>FORWARDs</c> for all IP addresses. If you'll recall, we solved this problem for <c>INPUTs</c> by closing them by default and selectively opening them. The first step is to ascertain that the firewall is configured as we expect. A call to <c>sudo iptables -nL</c> elicits the following output: <shell> 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 </shell> As you can see, the default policy for <c>FORWARD</c> is <c>ACCEPT</c>, which allows anyone to access other IP addresses from this machine. In Part I, you created a file named <c>/etc/iptables.uprules</c> in which you stored the default configuration of the firewall. You'll want to change that as shown below (the changes are highlighted): <code> *filter :INPUT DROP [0:0] :FORWARD <hl>DROP</hl> [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -i eth0 -j ACCEPT -A INPUT -i lo -j ACCEPT <hl>-A FORWARD -i eth0 -j ACCEPT -A FORWARD -i lo -j ACCEPT</hl> COMMIT </code> 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 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 <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 OUTPUT (policy ACCEPT) target prot opt source destination </shell> Now all IP forwarding requests are blocked by default. Those for the <c>lo</c> and <c>eth0</c> interfaces are, of course, still enabled, to allow the machine to be reachable both by itself and the local network. The final step is to change the firewall configuration script to open up IP forwarding for employees, but not for strangers. Since this is a <c>FORWARD</c> rule, not an <c>INPUT</c> one, the script has to make sure the remove <i>all</i> firewall rules for the client IP address instead of just the <c>INPUT</c> rules as it did previously. Changes from the script in Part I are highlighted. <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 <hl>channel=$2</hl> RULE="`iptables -L <hl>$channel</hl> -n --line-numbers | grep $ip_address | head -n 1`" } function drop_rule_from_iptables { rule="$1" <hl>channel="$2"</hl> echo " Drop rule [$rule] <hl>for channel [$channel]</hl>" line_number=`echo "$rule" | awk '{print $1}'` iptables -D <hl>$channel</hl> $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 <hl>iptables -A INPUT -i tun0 -s $source_ip -d $destination_ip -p $protocol --dport $port -j ACCEPT</hl> } function add_destination_to_iptables { source_ip=$1 destination_ip=$2 iptables -A INPUT -i tun0 -s $source_ip -d $destination_ip -j ACCEPT <hl>iptables -A FORWARD -i tun0 -s $source_ip -d $destination_ip -j ACCEPT</hl> } 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 <hl>iptables -A FORWARD -i tun0 -s $CLIENTIP -j ACCEPT</hl> 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 } <hl>function close_firewall_channel { channel=$1 get_next_matching_firewall_rule $CLIENTIP $channel while [ -n "$RULE" ] do drop_rule_from_iptables "$RULE" $channel get_next_matching_firewall_rule $CLIENTIP $channel done }</hl> function close_firewall { echo "CloseFirewall for [$CLIENTIP]" <hl>close_firewall_channel "INPUT" close_firewall_channel "FORWARD" close_firewall_channel "OUTPUT"</hl> } # 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> Since you only changed the firewall configuration script, there is no need to restart OpenVPN. <h>Testing the Script</h> You can test to verify that the firewall is updated properly by simply executing the <c>/etc/openvpn/configfirewall.sh</c> script with various parameters. The expected parameters are an operation---"add" or "delete" for testing purposes---a name---matched against the names in your lists---and an IP address, which should be chosen so as not to interfere with any addresses assigned by either OpenVPN or a DHCP server. To test what would happen when an <i>employee</i> connects through OpenVPN, execute the following command: <shell> sudo /etc/openvpn/configfirewall.sh add 192.168.40.3 John_Doe </shell> You should see the following output from the script: <shell> CloseFirewall for [192.168.40.3] OpenFirewall for John_Doe @ [192.168.40.3] Certificate found in employee list Add routes for all ip addresses </shell> This sounds about right and it looks like the script ran as expected. You can check that the firewall was configured as expected with a call to <c>sudo iptables -nL</c>, which should now elicit the following output (the main changes are highlighted): <shell> 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 <hl>ACCEPT all -- 192.168.40.3 0.0.0.0/0</hl> Chain FORWARD (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 <hl>ACCEPT all -- 192.168.40.3 0.0.0.0/0</hl> </shell> As you can see, the firewall accepts all <c>INPUT</c> and <c>FORWARD</c> from employees. Removing this test employee is as simple as executing: <shell> sudo /etc/openvpn/configfirewall.sh <hl>delete</hl> 192.168.40.3 John_Doe </shell> You should see the following output from the script: <shell> CloseFirewall for [192.168.40.3] Drop rule [4 ACCEPT all -- 192.168.40.3 0.0.0.0/0 ] for channel [INPUT] Drop rule [4 ACCEPT all -- 192.168.40.3 0.0.0.0/0 ] for channel [FORWARD] </shell> A call to <c>sudo iptables -nL</c> should now elicit the following output, where the rules for the employee have been removed: <shell> 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 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 </shell> You should really test with one user from each list, so the next user to test is a stranger. Add a stranger by calling the script with the strangers's name instead of the employee's name: <shell> sudo /etc/openvpn/configfirewall.sh add 192.168.40.3 <hl>John_Stranger</hl> </shell> You should see the following output from the script: <shell> CloseFirewall for [192.168.40.3] OpenFirewall for John_Stranger @ [192.168.40.3] Certificate found in strangers list Add route for DNS Add route for Windows shares </shell> A call to <c>sudo iptables -nL</c> should now elicit the following output: <shell> 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 <hl>ACCEPT udp -- 192.168.40.3 192.168.1.1 udp dpt:53 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:139 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:445</hl> Chain FORWARD (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 <hl>ACCEPT udp -- 192.168.40.3 192.168.1.1 udp dpt:53 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:139 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:445</hl> </shell> For strangers, the firewall accepts only requests on the ports and IP addresses explicitly opened by the script and drops all <c>FORWARD</c> requests. Removing this test employee is as simple as executing: <shell> sudo /etc/openvpn/configfirewall.sh <hl>delete</hl> 192.168.40.3 John_Stranger </shell> You should see the following output from the script: <shell> CloseFirewall for [192.168.40.3] Drop rule [4 ACCEPT udp -- 192.168.40.3 192.168.1.1 udp dpt:53 ] for channel [INPUT] Drop rule [4 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:139 ] for channel [INPUT] Drop rule [4 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:445 ] for channel [INPUT] Drop rule [4 ACCEPT udp -- 192.168.40.3 192.168.1.1 udp dpt:53 ] for channel [FORWARD] Drop rule [4 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:139 ] for channel [FORWARD] Drop rule [4 ACCEPT tcp -- 192.168.40.3 192.168.1.5 tcp dpt:445 ] for channel [FORWARD] </shell> A call to <c>sudo iptables -nL</c> should now elicit the following output, where the rules for the stranger have been removed: <shell> 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 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 </shell> You can use the script this way to test the firewall configuration without actually logging in through OpenVPN. When everything is set, you should still log with OpenVPN as a user from each list to verify that the firewall is doing what you think it is doing. In fact, that's exactly why there is a Part II to this article: We tested by adding a user to the strangers list and logging in and noticed that we were able to ping many more servers than we had configured. Don't let that happen to you! <h>Testing via OpenVPN</h> So, there's one more trick that you can use to make testing via OpenVPN easier. Since you have to be outside the network to test tunneling in via VPN, you run into the problem of testing as a <i>stranger</i> because strangers probably won't have rights to open a shell on the OpenVPN server. That is, you need to be able to do this: <ol> Log in with VPN as an employee Add yourself to the strangers list Log out of VPN Log in with VPN as a stranger Test that you cannot access anything that you shouldn't be able to <b>remove yourself from the strangers list</b> (uh oh!) </ol> Since you're a stranger, you can no longer open a shell on the OpenVPN server and alter the configuration. Here are some ways of getting around this problem: <ul> The safest and best way to test via OpenVPN is to create a user exclusively for testing and add that user to each of your lists in turn, logging in and out to test the configuration. If you need to change something, you just log out and log back in using your "employee" user. Another way is to open an OpenVPN session from one machine (it can be a virtual machine) and then add that same user to each of your lists in turn, logging in and out from another machine. Just make sure not to lose the connection from the "main" machine or you may be locked out. <div>Another way around this is to add an exception for the OpenVPN server to all configurations (strangers, employees, etc.) so that you can test almost everything. To do this, just add the a rule for the OpenVPN server (assumed to be on 192.168.1.1) as follows: <code> add_destination_to_iptables $CLIENTIP 192.168.1.1 </code> When you're finished testing, make sure to remove the hack. </div> </ul> <h>Files</h> Finally, here are samples of all of the files modified in this tutorial. See <a href="{app}/view_article.php?id=2336">Part I</a> for the other files. <ul> <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 </ul>