OpenVPN with FreeRADIUS: How To Use the CN from the User Cert as the Login Name (i.e. the reverse of “username-as-common-name”)

I recently set up handful of OpenVPN servers to provide access to various LAN and AWS VPC resources. Initially I had just the certificate validation configured, but I felt slightly uneasy about not having a password. Especially in the environments where multiple people need access to a resource, in the event one of them no longer should have access (such as when leaving an organization) the only way to block such user would be to add their cert into the CRL. While that should be done anyway when a user’s privilege needs to be revoked, a password would provide a more immediate and easy way to make such changes.

The next step was to install FreeRADIUS which proved to be a very easy task. I’m initially running it with just text-based back-end and will later add MySQL, perhaps with daloRADIUS GUI to make user administration even easier. On Ubuntu/Debian there is a package “openvpn-auth-radius”, which makes it possible to add FreeRADIUS authentication to OpenVPN server with one simple line:

plugin /usr/lib/openvpn/radiusplugin.so /etc/openvpn/endpoint_server_radiusplugin.conf

Of course, the client side also needs the auth-user-pass statement in their OpenVPN client configuration.

But there is a problem: The user cert can be that of Bob while the login username/password is that of Alice, and the login would still be valid. Apparently I’m not the only one who has thought about this. While I didn’t want to hack the pam auth plugin, the post had enough clues to help creating a simple bash script that sets the username based on the common-name from the validated user’s certificate:

#!/bin/bash

# $1 provides the temp file name provided by OpenVPN
# file has two lines: username and password, as entered by the user.
# We get the username from the user cert's CN (available via an envvar).

export PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"

if [ ! -z ${common_name} ]; then
  username=`echo ${common_name}`
else
  username="-"
fi

if [ ! -z $1 ] && [ -f $1 ]; then
  password=`tail -1 $1`
else
  password="-"
fi

radius_server=localhost
# shared secret for localhost (or your RADIUS server) from /etc/freeradius/
shared_secret="XXXXXXXX"

AUTHCHECK=`cat << EOF | /usr/bin/radclient -s ${radius_server} auth ${shared_secret} | grep approved | tr -d 'n' | tail -c 1
User-Name=${username}
User-Password=${password}
EOF`

if [[ $AUTHCHECK = 1 ]]; then
  exit 0
else
  exit 1
fi

To use this script, simply save it to /etc/openvpn/endpoint_server_radius_auth.sh, make it executable, and edit the file to add the shared secret for the RADIUS server from /etc/freeradius/clients. Finally, add the following lines in your OpenVPN server configuration that already authenticates the users by their certificates:

tmp-dir /dev/shm
auth-user-pass-verify /etc/openvpn/endpoint_server_radius_auth.sh via-file

Now the login name for RADIUS authentication is taken from the CommonName (CN) of the user's certificate and, in fact, the username that the user enters when prompted for auth-user-pass username/password is ignored, only the password is significant.

The bottom line of this script: It utilizes RADIUS to provide a server-side password validation for the certificate's CN. A user can always remove the password protection from their private key, so this approach functions as an extra layer of security while making it easier to quickly revoke user's access to a resource.

Note: For this to work, the CommonName set in user certificates obviously must be a valid RADIUS login name. A user can't modify the CN in their certificate (unless they're NSA since they apparently have access to RSA-keys, too 🙁 ), so they're locked to use that specific username.

Also note that I wrote this script on Ubuntu, and did not necessarily observe portability, so you may need to modify the script some for other platforms. It is primarily intended as an example (although it does work), as finding something like this would have saved me a few hours of work.