Building an Active Directory Authenticated and Managed OpenVPN Server Part 2

After I outlined the goals for my Project in the last Article, it is time to get to work. This Article will cover the installation and configuration of OpenVPN. I will also explain how chained certificates can be used with OpenVPN. If you follow the my steps, you will have functioning OpenVPN server at the end. My first step was to create a new KVM machine and install Debian Wheezy. I am going to skip the description and assume, that you already have a functioning Linux to install OpenVPN on.

Installing OpenVPN and Preparing the directories

First I installed OpenVPN:

apt-get install openvpn

Once OpenVPN was installed, I joined the machine to my domain. I described the process in a previous article. After my new server was a domain member, I implemented my folder structure and moved the OpenVPN CA and server certificate to their new home. The files for my OpenVPN server now reside in “/etc/openvpn”. The the folder structure looks like this now:

/etc
--/openvpn
-----/openvpn.skelleton.net
-------/CA_OpenVPN_skelleton.net
-----------/certs
-----------/crl
-----------/newcerts
-----------/private
---------------.rand
---------------cakey_OpenVPN.pem
------------cacert_OpenVPN.pem
------------crl_OpenVPN.pem
------------crlnumber
------------index.txt
------------serial
--------/clients
--------/logs
--------/private
------------openvpn.skelleton.net.key.pem
--------/scripts
--------/templates
---------openvpn.skelleton.net.cert.pem
---------skelleton.net_ca_openvpn.cnf

Preparing the firewall

Next item on the agenda was the firewall. I modified a bunch of custom iptables scripts, that i have been using for years. My modifications mean, that the machine is entirely open. This will change once I have all the kinks of the OpenVPN worked out, but for testing a new installation this is the best option. If your OpenVPN Server is on the same machine as your internet gateway, you really should not use this script in this form. I am using 3 scripts in the folder “/etc/mysysconfig” and one script in “/etc/init.d”.
The main script is “/etc/mysysconfig/my_firewall_vpn”:

#!/bin/bash
set -x
set -v
#firewall start
#reset fiewall

/etc/mysysconfig/my_firewall_reset

IPT=/sbin/iptables

LAN=eth0
#MYIP=$ '/etc/mysysconfig/my_current_ip'

#deny access from internet
$IPT -N wall
$IPT -A wall -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPT -A wall -m state --state NEW -i $LAN -j ACCEPT
$IPT -A wall -m state --state NEW -i tun+ -j ACCEPT
$IPT -A wall -m state --state NEW -i lo -j ACCEPT
$IPT -A wall -j ACCEPT
$IPT -A INPUT -j wall
$IPT -A OUTPUT -j wall

# Allow TUN interface connections to OpenVPN server
$IPT -A INPUT -i tun+ -j ACCEPT
# Allow TUN interface connections to be forwarded through other interfaces
$IPT -A FORWARD -i tun+ -j ACCEPT

# Allow packets from private subnets
$IPT -A INPUT -i $LAN -j ACCEPT
$IPT -A FORWARD -i $LAN -j ACCEPT

echo 1 > /proc/sys/net/ipv4/ip_forward

The script used to initialise the firewall is “/etc/mysysconfig/my_firewall_reset”:

#!/bin/sh
#iptables reset
IPT=/sbin/iptables

#accept all
$IPT -P INPUT ACCEPT
$IPT -P OUTPUT ACCEPT
$IPT -P FORWARD ACCEPT
$IPT -P POSTROUTING ACCEPT -t nat
$IPT -P PREROUTING ACCEPT -t nat
$IPT -P OUTPUT ACCEPT -t nat

#delete all
$IPT -F
$IPT -F -t nat

#delete all user chains
$IPT -X

# disable forwarding
echo 0 > /proc/sys/net/ipv4/ip_forward

There is also a script to completely open the firewall, though that is fairly unnecessary and only still around due to historical reasons. “/etc/mysysconfig/my_firewall_off”:

#!/bin/sh
/sbin/iptables -P INPUT ACCEPT
/sbin/iptables -P OUTPUT ACCEPT
/sbin/iptables -P FORWARD ACCEPT
/sbin/iptables -P POSTROUTING ACCEPT -t nat
/sbin/iptables -P PREROUTING ACCEPT -t nat
/sbin/iptables -P OUTPUT ACCEPT -t nat

The last part of the script collection is the init script, which of course goes into “/etc/init.d/firewall”:

#!/bin/sh
### BEGIN INIT INFO
# Provides:          firewall
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       custom firewall script
### END INIT INFO
DESC="firewall"

case "$1" in
start)
    /etc/mysysconfig/my_firewall_vpn
    ;;
stop)
    /etc/mysysconfig/my_firewall_off
    ;;
restart)
    $0 stop
    $0 start
    ;;
*)
    echo "usage: firewall {start|stop|restart}"
    exit1
    ;;
esac
exit 0

After the scripts are in place, you have to make them executable and then you have to ensure the scripts get executed and system startup and shutdown. On Debian this is done with the command “update-rc.d”:

update-rc.d /etc/init.d/firewall defaults

Since the OpenVPN I am describing will be a routed VPN, you will also have to ensure, that all relevant computers in your network know the route to the VPN.

Preparing the Private Key Infrastructure

With OpenVPN installed and the firewall prepared, it was time to move my PKI to the OpenVPN server. If you do not have a Certificate Authority prepared already, you can follow this article to create one. Alternately you can use the easy-rsa script provided by OpenVPN to create and manage your PKI. In that case the scripts provided by me might need some serious tweaking. The “serial” and “crlnumber” files have to be initialized. I did this with the following commands:

echo 00 > /etc/openvpn/openvpn.skelleton.net/CA_OpenVPN_skelleton.net/crlnumber
echo 00 > /etc/openvpn/openvpn.skelleton.net/CA_OpenVPN_skelleton.net/serial

I used a slightly modified version of my OpenSSL configuration from my earlier article on OpenSSL. The most important change was making the CA CA_OpenVPN_skelleton.net the default_ca. But I also cleaned the file up a little:

#
# OpenSSL example configuration file.
# This is mostly being used for generation of certificate requests.
#

# This definition stops the following lines choking if HOME isn't
# defined.
HOME            = .
RANDFILE        = $ENV::HOME/.rnd

# Extra OBJECT IDENTIFIER info:
#oid_file        = $ENV::HOME/.oid
oid_section        = new_oids

# To use this configuration file with the "-extfile" option of the
# "openssl x509" utility, name here the section containing the
# X.509v3 extensions to use:
# extensions        = 
# (Alternatively, use a configuration file that has only
# X.509v3 extensions in its main [= default] section.)

[ new_oids ]
# We can add new OIDs in here for use by 'ca' and 'req'.
# Add a simple OID like this:
# testoid1=1.2.3.4
# Or use config file substitution like this:
# testoid2=${testoid1}.5.6

####################################################################
[ ca ]
default_ca    = CA_OpenVPN_skelleton.net        # The default ca section

####################################################################

[ CA_OpenVPN_skelleton.net ]
dir                = /etc/openvpn/openvpn.skelleton.net/CA_OpenVPN_skelleton.net
certs            = $dir/certs
crl_dir            = $dir/crl
database        = $dir/index.txt
new_certs_dir    = $dir/newcerts
certificate     = $dir/cacert_OpenVPN.pem
serial            = $dir/serial
crlnumber        = $dir/crlnumber
crl                = $dir/crl_OpenVPN.pem
private_key        = $dir/private/cakey_OpenVPN.pem
RANDFILE        = $dir/private/.rand
x509_extensions    = vpnusr_cert
name_opt        = ca_default
cert_opt        = ca_default
crl_extensions    = crl_ext
default_days    = 1095
default_crl_hours    = 48
default_md        = sha1
preserve        = no    
policy            = policy_vpnusr

# For the CA policy
[ policy_match ]
countryName        = match
stateOrProvinceName    = match
organizationName    = match
organizationalUnitName    = optional
commonName        = supplied
emailAddress        = optional

[ policy_vpnusr ]
countryName        = supplied
stateOrProvinceName    = supplied
organizationName    = match
organizationalUnitName    = optional
commonName        = supplied
emailAddress        = supplied

# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName        = optional
stateOrProvinceName    = optional
localityName        = optional
organizationName    = optional
organizationalUnitName    = optional
commonName        = supplied
emailAddress        = optional

####################################################################
[ req ]
default_bits        = 4096
default_keyfile     = privkey.pem
distinguished_name    = req_distinguished_name
attributes        = req_attributes
#x509_extensions    = v3_ca    # The extentions to add to the self signed cert
# Passwords for private keys if not present they will be prompted for
# input_password = secret
# output_password = secret
# This sets a mask for permitted string types. There are several options. 
# default: PrintableString, T61String, BMPString.
# pkix     : PrintableString, BMPString.
# utf8only: only UTF8Strings.
# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
# MASK:XXXX a literal mask value.
# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings
# so use this option with caution!
string_mask = nombstr
# req_extensions = v3_req # The extensions to add to a certificate request

[ req_distinguished_name ]
countryName                    = Country Name (2 letter code)
countryName_default            = DE
countryName_min                = 2
countryName_max                = 2
stateOrProvinceName            = State or Province Name (full name)
stateOrProvinceName_default    = Skelletonia
localityName                = Locality Name (eg, city)
0.organizationName            = Organization Name (eg, company)
0.organizationName_default    = skelleton.net
# we can do this but it is not needed normally 🙂
#1.organizationName        = Second Organization Name (eg, company)
#1.organizationName_default    = World Wide Web Pty Ltd
organizationalUnitName        = Organizational Unit Name (eg, section)
#organizationalUnitName_default    =
commonName                    = Common Name (eg, YOUR name)
commonName_max                = 64
emailAddress                = Email Address
emailAddress_max            = 64
# SET-ex3            = SET extension number 3

[ req_attributes ]
challengePassword            = A challenge password
challengePassword_min        = 4
challengePassword_max        = 20
unstructuredName            = An optional company name

[ usr_cert ]

# These extensions are added when 'ca' signs a request.
# This goes against PKIX guidelines but some CAs do it and some software
# requires this to avoid interpreting an end user certificate as a CA.
basicConstraints=CA:FALSE
# Here are some examples of the usage of nsCertType. If it is omitted
# the certificate can be used for anything *except* object signing.
# This is OK for an SSL server.
# nsCertType            = server
# For an object signing certificate this would be used.
# nsCertType = objsign
# For normal client use this is typical
# nsCertType = client, email
# and for everything including object signing:
# nsCertType = client, email, objsign
# This is typical in keyUsage for a client certificate.
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment
# This will be displayed in Netscape's comment listbox.
nsComment            = "skelleton.net signed Certificate"
# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
# This stuff is for subjectAltName and issuerAltname.
# Import the email address.
# subjectAltName=email:copy
# An alternative to produce certificates that aren't
# deprecated according to PKIX.
# subjectAltName=email:move
# Copy subject details
# issuerAltName=issuer:copy
#nsCaRevocationUrl        = http://www.domain.dom/ca-crl.pem
#nsBaseUrl
#nsRevocationUrl
#nsRenewalUrl
#nsCaPolicyUrl
#nsSslServerName

[ server_cert ]
# extensions to add for Servers implemented after Book Linux-Server P 777
basicConstraints        = critical, CA:FALSE
nsCertType                = server
nsComment                = "skelleton.net signed Certificate"
subjectKeyIdentifier    = hash
authorityKeyIdentifier    = keyid,issuer
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = serverAuth
crlDistributionPoints    = URI:http://www.skelleton.net/CA_Dist/skelleton.net-server.crl

[ email_cert ]
# certs for mailusers implemented after Book Linux-Server P 777
basicConstraints        = critical, CA:FALSE
nsComment                = "skelleton.net signed Certificate"
subjectKeyIdentifier    = hash
authorityKeyIdentifier    = keyid,issuer
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = emailProtection

[ vpnusr_cert ]
# certs for mailusers implemented after Book Linux-Server P 777
basicConstraints        = critical, CA:FALSE
nsComment                = "skelleton.net VPN user Certificate"
subjectKeyIdentifier    = hash
authorityKeyIdentifier    = keyid,issuer
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth
nsCertType                = client

[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
crlDistributionPoints    = URI:http://www.skelleton.net/CA_Dist/skelleton.net-root.crl

[ crl_ext ]

# CRL extensions.
# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.

# issuerAltName=issuer:copy
authorityKeyIdentifier=keyid:always,issuer:always

[ proxy_cert_ext ]
# These extensions should be added when creating a proxy certificate

# This goes against PKIX guidelines but some CAs do it and some software
# requires this to avoid interpreting an end user certificate as a CA.

basicConstraints=CA:FALSE

# Here are some examples of the usage of nsCertType. If it is omitted
# the certificate can be used for anything *except* object signing.

# This is OK for an SSL server.
# nsCertType            = server

# For an object signing certificate this would be used.
# nsCertType = objsign

# For normal client use this is typical
# nsCertType = client, email

# and for everything including object signing:
# nsCertType = client, email, objsign

# This is typical in keyUsage for a client certificate.
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment

# This will be displayed in Netscape's comment listbox.
nsComment            = "skelleton.net signed Certificate"

# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer:always

# This stuff is for subjectAltName and issuerAltname.
# Import the email address.
# subjectAltName=email:copy
# An alternative to produce certificates that aren't
# deprecated according to PKIX.
# subjectAltName=email:move

# Copy subject details
# issuerAltName=issuer:copy

#nsCaRevocationUrl        = http://www.domain.dom/ca-crl.pem
#nsBaseUrl
#nsRevocationUrl
#nsRenewalUrl
#nsCaPolicyUrl
#nsSslServerName

# This really needs to be in place for it to be a proxy certificate.
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo

Configuring OpenVPN

Now all that was left, was the OpenVPN configuration. This one was a little tricky though and I had to experiment a little to get it right, mostly because this was the first time that I used chained certificates with OpenVPN. Originally I planned to authenticate the users with the openvpn-auth-ldap plugin and additionally use an authentication script in order to ensure, that the username given in the authentication process and the common name of the client certificate match. Unfortunately this did not work because the current version of the openvpn-auth-ldap plugin is currently broken in Debian Wheezy amd64, in any other distribution you should be able to use the openvpn-auth-ldap plugin.
This was not  a show stopper though. OpenVPN offers many ways to authenticate users. The next best plugins would have been openvpn-auth-radius and openvpn-auth-pam.
For Radius Authentication I would have to set up a Radius server for my Active Directory, but once that is done I could have checked my users for authentication and authorisation in much the same way as I could have done with the LDAP plugin.
The PAM plugin on the other hand had the advantage, that the authentication part was already set up and I just needed configure the authorisation (Only users in the OpenVPN group are Authorised to use OpenVPN).
After a few minutes of asking myself which way would cause the least amount of work, I choose to go with option number three: extending my authorisation script. I went with this option because I was already planning to use a custom script in the authentication process of OpenVPN. This was mostly due to the fact, that I was not able to find build in way to match the username used in the authentication process with the common name of the certificate used.

Authentication script

I actually had the script prepared already. But with the change in my plans, it had to be extended to ensure the following three criteria:

  • Authorize the user –> check that the user is in the group
  • Check that the user uses his own certificate for OpenVPN –> match the username with the common name of the certificate
  • Authenticate the user –> check that the correct password was supplied

This ended up being a very simple script. Before you use this script on your own OpenVPN, there are a few things to be aware of: This script gets the password as environment variable, there are possible security issues there and your OpenVPN machine is required to be a member of the Active Directory.

#!/bin/bash
#username=$1
#password=$2
#common_name=$3
VPN_ACCESS_LOG=/etc/openvpn/openvpn.skelleton.net/logs/openvpn_access.log
VPN_AD_Group=OpenVPN_Users

function func_VPN_get_user_list {
    #get list of VPN Users
    # Samba3 Domain member: wbinfo --group-info groupname
    # output: groupname:x:10020:member1,mebmer2,...,memberx
    VPN_user_list=""
    VPN_user_list=$(wbinfo --group-info $VPN_AD_Group | cut -d ':' -f 4)
    #output is a comma separated list of VPN Users
    if [ -z "$VPN_user_list" ]
    then
        VPN_ERR=1
        echo -e 'There is a problem with the VPN script -- user list is empty' | mutt -s "ERROR the OpenVPN user script encountered a problem" --openvpn@$AD_NAME
        echo $(date +"%d.%m.%Y %T") ERROR user list is empty, aborting login >> $VPN_ACCESS_LOG
        exit 1
    fi
}
#Start the Authentication
func_VPN_get_user_list
for member in ${VPN_user_list//,/ }; do
    if [ $username == $member ]
    then
        if [ $username == $common_name ]
        then
            AUTH=0
            echo $password | kinit $username && AUTH=1 || AUTH=0
            echo status $AUTH
            if [ $AUTH -eq 1 ]
            then
                echo $(date +"%d.%m.%Y %T") GRANTED to $username with cert=$common_name >> $VPN_ACCESS_LOG
                exit 0
            else
                echo $(date +"%d.%m.%Y %T") DENIED  username=$username wrong password >> $VPN_ACCESS_LOG
                exit 1
            fi
            echo still in loop
        elif [ $username != $common_name ]
        then
            echo $(date +"%d.%m.%Y %T") DENIED  username=$username cert=$common_name >> $VPN_ACCESS_LOG
            exit 1
        fi
    fi
done
echo $(date +"%d.%m.%Y %T") DENIED  username=$username is not  in the AD Group $VPN_AD_Group >> $VPN_ACCESS_LOG
exit 1

OpenVPN server configuration

I used the configuration file of my previously existing OpenVPN and added the authentication through the script. It is Important, that script security is set to 3, otherwise the password will not be passed on the the script. Since this configuration was originally created automatically by some web interface, there is probably still room for tweaks. The configuration file belongs in the “/etc/openvpn” directory and should end with “.conf”:

port 1194
proto udp
dev tun0
#any certificate signed by any of the CAs in this is found to be valid
ca /etc/openvpn/openvpn.skelleton.net/CA_OpenVPN_skelleton.net/cacert_OpenVPN.crt
cert /etc/openvpn/openvpn.skelleton.net/openvpn.skelleton.net.cert.pem
key /etc/openvpn/openvpn.skelleton.net/private/openvpn.skelleton.net.key.pem
dh /etc/openvpn/openvpn.skelleton.net/dh4096.pem
server 192.168.91.0 255.255.255.0
crl-verify /etc/openvpn/openvpn.skelleton.net/CA_OpenVPN_skelleton.net/crl_OpenVPN.pem
tls-auth /etc/openvpn/openvpn.skelleton.net/private/ta.key 0
cipher AES-256-CBC
user nobody
group nogroup
status /etc/openvpn/openvpn.skelleton.net/logs/openvpn-status.log
log-append /etc/openvpn/openvpn.skelleton.net/logs/openvpn.log
verb 2
mute 20
max-clients 100
management 127.0.0.1 2222
keepalive 10 120
client-config-dir /etc/openvpn/openvpn.skelleton.net/clients
tls-server
client-to-client
comp-lzo
persist-key
persist-tun
ccd-exclusive
push "route 192.168.80.0 255.255.255.0"
script-security 3
auth-user-pass-verify /etc/openvpn/scripts/auth_kerberos.sh via-env

An additional configuration file was necessary for the TCP OpenVPN server. This file has only three changed lines and one additional line:

port 443
port-share www.skelleton.net 443
proto tcp
dev tun1

Creation of the Diffie-Hellmann and TLS keys

But before I could actually start the OpenVPN service, I still had to generate the Diffie-Hellman key and the key for tls authentication. Since I do not use easy-rsa to manage my CA, I used OpenSSL directly to create my Diffie-Hellmann key.

openssl dhparam -out /etc/openvpn/openvpn.skelleton.net/dh4096.pem 4096

This actually took a while.
After the Diffie-Hellmann key was created, I used OpenVPN to create my tls key:

openvpn --genkey --secret /etc/openvpn/openvpn.skelleton.net/private/ta.key

Using chained certificates with OpenVPN

Due to the fact, that my OpenVPN Certificate Authority is an intermediate Certificate Authority, I had to append the CA certificate of the root CA to the OpenVPN CA certificate for use with OpenVPN. In theory that means all Certificates, that can be linked back to the Root CA, will be treated as valid certificates.
My server Certificate has also been signed by a (different) intermediate Certificate Authority. In this case the intermediate CA certificate has to be appended to the server certificate and the client needs the root CA certificate specified as CA certificate. This has to be done only if you are using chained certificates, since the chain of trust has to be proven.

The client configuration template

The last part of the OpenVPN configuration is the client configuration. Since I want to deliver individual scripts to each client, the script I used here is just a template. It will not work on its own because all certificates and keys are missing. The most common way to configure them, is configuring the path to them. I will however include the keys and certificates directly into the client configuration files later on. Here is my configuration template for udp connections:

client
proto udp
dev tun
remote openvpn.skelleton.net 1194
remote openvpn2.skelleton.net 1194
key-direction 1
ns-cert-type server
cipher AES-256-CBC
verb 2
mute 20
keepalive 10 120
comp-lzo
persist-key
persist-tun
float
resolv-retry infinite
nobind
pull
auth-user-pass

As you can see I specified a second OpenVPN server, even though I currently have only one OpenVPN server. This allows me to easily add another server for failover later on. The template for the tcp server is almost the same. I simply changed the protocol to tcp and the port to 443.
This concludes the OpenVPN configuration. In my next Article I will describe the script I implemented, to automate my OpenVPN Server.

2 thoughts on “Building an Active Directory Authenticated and Managed OpenVPN Server Part 2”

Leave a Reply

Your email address will not be published. Required fields are marked *