Difference between revisions of "Building a tiered access OpenVPN gateway on OpenBSD"

From WTFwiki
Jump to navigation Jump to search
(→‎OpenVPN: Fix typos)
m (10 revisions)
 
(6 intermediate revisions by one other user not shown)
Line 10: Line 10:
  
 
The other directory, profileoptions, contains the openvpn options for that profile. These can be any of the following openvpn config directives: push, push-reset, iroute, ifconfig-push and config. See the [http://openvpn.net/man.html OpenVPN Man Page] for further details.
 
The other directory, profileoptions, contains the openvpn options for that profile. These can be any of the following openvpn config directives: push, push-reset, iroute, ifconfig-push and config. See the [http://openvpn.net/man.html OpenVPN Man Page] for further details.
 
In addition, the pf.conf file should have a table for each vpn profile. The tables should be named according to the format "vpn<ProfileName>" so client's connecting as an admin have their IP in the vpnadmin table.
 
  
 
== The Scripts ==
 
== The Scripts ==
Line 30: Line 28:
  
 
if [ $? -ne 0 ]; then
 
if [ $? -ne 0 ]; then
  echo "$0: No profile match for $3"
+
        echo "$0: No profile match for ${common_name}"
  exit 1
+
        exit 1
 
fi
 
fi
  
Line 37: Line 35:
  
 
if [ $MatchCount -gt 1 ]; then
 
if [ $MatchCount -gt 1 ]; then
  echo "$0: Multiple profiles matched for $3, this is bad"
+
        echo "$0: Multiple profiles matched for ${common_name}, this is bad"
  exit 1
+
        exit 1
 
fi
 
fi
  
Line 45: Line 43:
  
 
if [ -f $ProfileConfig ]; then
 
if [ -f $ProfileConfig ]; then
  echo "$0: Client ${common_name} configured using the ${Profile} profile"
+
        echo "$0: Client ${common_name} configured using the ${Profile} profile"
  cp $ProfileConfig $1
+
        cp $ProfileConfig $1
 
else
 
else
  echo "$0: Config for profile $Profile not found"
+
        echo "$0: Config for profile $Profile not found"
  exit 1
+
        exit 1
 
fi
 
fi
 
</pre>
 
</pre>
Line 67: Line 65:
  
 
if [ $? -ne 0 ]; then
 
if [ $? -ne 0 ]; then
  echo "$0: No profile matched for $3"
+
        echo "$0: No profile matched for ${common_name}"
  exit 1
+
        exit 1
 
fi
 
fi
  
Line 74: Line 72:
  
 
if [ $MatchCount -gt 1 ]; then
 
if [ $MatchCount -gt 1 ]; then
  echo "$0: Multiple profiles matched for $3, this is bad"
+
        echo "$0: Multiple profiles matched for ${common_name}, this is bad"
  exit 1
+
        exit 1
 
fi
 
fi
  
Line 100: Line 98:
  
 
if [ $1 == "delete" ]; then
 
if [ $1 == "delete" ]; then
  Tables=`ls profiles/* | awk -F / '{ printf "vpn%s ", $NF }'`
+
        Tables=`ls profiles/* | awk -F / '{ printf "vpn%s ", $NF }'`
  echo "removing $2 from PF tables ${Tables}"
+
        echo "removing $2 from PF tables ${Tables}"
  for Table in $Tables; do
+
        for Table in $Tables; do
    sudo /sbin/pfctl -t ${Table} -T delete $2
+
                sudo /sbin/pfctl -t ${Table} -T delete $2
  done
+
        done
  exit 0
+
        exit 0
 
fi
 
fi
  
Line 111: Line 109:
  
 
if [ $? -ne 0 ]; then
 
if [ $? -ne 0 ]; then
  echo "$0: No profile match for $3"
+
        echo "$0: No profile match for $3"
  exit 1
+
        exit 1
 
fi
 
fi
  
Line 118: Line 116:
  
 
if [ $MatchCount -gt 1 ]; then
 
if [ $MatchCount -gt 1 ]; then
  echo "$0: Multiple profiles matched for $3, this is bad"
+
        echo "$0: Multiple profiles matched for $3, this is bad"
  exit 1
+
        exit 1
 
fi
 
fi
  
Line 126: Line 124:
  
 
if [ $1 != "add" ]; then # if we're updating
 
if [ $1 != "add" ]; then # if we're updating
  echo "$0: Deleting user $3 at $2 from PF table ${Table}"
+
        echo "$0: Deleting user $3 at $2 from PF table ${Table}"
  sudo /sbin/pfctl -t ${Table} -T delete $2 > /dev/null
+
        sudo /sbin/pfctl -t ${Table} -T delete $2 > /dev/null
 
fi
 
fi
  
Line 160: Line 158:
  
 
=== PF ===
 
=== PF ===
Just add a "vpn<ProfileName>" table for each profile you setup and add your firewall rules in pf.conf.
 
  
=== PF ===
+
Do ''not'' configure empty tables for the vpn profiles or the tables will be emptied on pf.conf reload. Your firewall rules can safely apply to tables created at runtime.
Just set a "vpn<ProfileName>" for each profile you setup and add your firewall rules in pf.conf.
 

Latest revision as of 22:49, 4 January 2013

Introduction

I recently needed to expand the scope of the OpenVPN gateway I administer to allow clients to have very limited access over it to a server on our network while still allowing certain employees more access. I did an initial quick and dirty config using client-config-directories but I wasn't satisfied with that from a maintenance point of view. This page will document my improved approach, using learn-address and client-connect scripts.

The Design

I wanted to add the concept of 'profiles' to OpenVPN to allow me to assign users access depending on their certificate's common name. I also wanted it to be scripted using only OpenBSD's shell commands so that no additional software with possible security holes or upgrade issues could interfere with the VPN's operation.

I have 2 configuration directories: profile and profileoptions. Profiles contains a set of files, each representing a profile. Each Profile file contains a list of common names, each on a new line, that delineates who is a member of that profile. A common name can only appear in one profile file at any one time, multiple matches are an error.

The other directory, profileoptions, contains the openvpn options for that profile. These can be any of the following openvpn config directives: push, push-reset, iroute, ifconfig-push and config. See the OpenVPN Man Page for further details.

The Scripts

client-connect.sh

#!/bin/sh

# This is an openvpn client-connect script that pushes routes or other
# openvpn client config stuff to a newly connected client depending on
# their detected 'profile'.

# $1 is tempfile
# everything else is passed via environment variables. common_name is
# all we care about in this script.

Profiles=`grep -l "^${common_name}$" profiles/*`

if [ $? -ne 0 ]; then
        echo "$0: No profile match for ${common_name}"
        exit 1
fi

MatchCount=`echo $Profiles | awk '{ printf "%d", NF }'`

if [ $MatchCount -gt 1 ]; then
        echo "$0: Multiple profiles matched for ${common_name}, this is bad"
        exit 1
fi

Profile=`echo $Profiles | awk -F / '{ print $NF }'`
ProfileConfig="profileoptions/${Profile}"

if [ -f $ProfileConfig ]; then
        echo "$0: Client ${common_name} configured using the ${Profile} profile"
        cp $ProfileConfig $1
else
        echo "$0: Config for profile $Profile not found"
        exit 1
fi

client-disconnect.sh

#!/bin/sh

# This is an openvpn client-disconnect script that deletes client IP
# addresses from PF tables on client disconnect.

# everything is passed via environment variables. common_name and
# ifconfig_pool_remote_ip are all we care about in this script.

Profiles=`grep -l "^${common_name}$" profiles/*`

if [ $? -ne 0 ]; then
        echo "$0: No profile matched for ${common_name}"
        exit 1
fi

MatchCount=`echo $Profiles | awk '{ printf "%d", NF }'`

if [ $MatchCount -gt 1 ]; then
        echo "$0: Multiple profiles matched for ${common_name}, this is bad"
        exit 1
fi

Profile=`echo $Profiles | awk -F / '{ print $NF }'`
Table="vpn${Profile}"


echo "$0: Deleting user ${common_name} at ${ifconfig_pool_remote_ip} from PF table ${Table}"
sudo pfctl -t ${Table} -T delete ${ifconfig_pool_remote_ip} > /dev/null

learn-address.sh

#!/bin/sh

# This is an openvpn learn-address script that modifies PF rules dynamically
# and supports 'profiles' to allow you to manage client connections without
# the headache of assigning static IPs.

# $1 is add/update/delete
# $2 is IP/Subnet/MAC
# $3 is certificate's common name

if [ $1 == "delete" ]; then
        Tables=`ls profiles/* | awk -F / '{ printf "vpn%s ", $NF }'`
        echo "removing $2 from PF tables ${Tables}"
        for Table in $Tables; do
                sudo /sbin/pfctl -t ${Table} -T delete $2
        done
        exit 0
fi

Profiles=`grep -l "^${3}$" profiles/*`

if [ $? -ne 0 ]; then
        echo "$0: No profile match for $3"
        exit 1
fi

MatchCount=`echo $Profiles | awk '{ printf "%d", NF }'`

if [ $MatchCount -gt 1 ]; then
        echo "$0: Multiple profiles matched for $3, this is bad"
        exit 1
fi

Profile=`echo $Profiles | awk -F / '{ print $NF }'`
Table="vpn${Profile}"

if [ $1 != "add" ]; then # if we're updating
        echo "$0: Deleting user $3 at $2 from PF table ${Table}"
        sudo /sbin/pfctl -t ${Table} -T delete $2 > /dev/null
fi

# delete doesn't get this far
echo "$0: Adding user $3 at $2 to PF table ${Table}"
sudo /sbin/pfctl -t ${Table} -T add $2 > /dev/null

Service Configuration

Sudoers

You'll have to allow the user openvpn is running as (in my case _openvpn:_openvpn) to add/delete from the vpn PF tables. Here's how I did it:

_openvpn ALL=(root) NOPASSWD: /sbin/pfctl -t vpn* -T add *
_openvpn ALL=(root) NOPASSWD: /sbin/pfctl -t vpn* -T delete *

OpenVPN

You'll need a couple of config options turned on in your openvpn server conf:

learn-address ./learn-address.sh
client-connect ./client-connect.sh
client-disconnect ./client-disconnect.sh
tmp-dir /tmp/openvpn

You'll want to make sure openvpn can write to its tmp-dir.

Also, I recommend you add explicit-exit-notify to your openvpn client configs, otherwise ip addresses can persist in the pf tables for a while after a client disconnects.

PF

Do not configure empty tables for the vpn profiles or the tables will be emptied on pf.conf reload. Your firewall rules can safely apply to tables created at runtime.