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:https://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:https://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”