OpenVPN Community + 2FA with Google Authenticator

Mike R
9 min readMar 16, 2022

March 16, 2022

Background

OpenVPN is a wonderful VPN package — I’ve been running an ec2 micro instance with OpenVPN for my company for 2 years during the pandemic, and its been rock-solid, serving VPN tunnels for over 60 users.

The way users currently authenticate is using a client-based .ovpn profile, along with username and password — they drag their profile into Tunnelblick on their Mac, add their password and connect.

To add another layer of security, I am now adding a 2 Factor Authentication mechanism for each user, they will login as usual, but will need to use a time-based token (provided by Authy app on their phone) to login to our company’s networks.

One way to do this is to install OpenVPN Access Server — an Enterprise level solution that has all this built-in, along with other options like Radius authentication. User licenses are then purchased and users can pull down their profiles from the OpenVPN AS webpage.

There are 2 main problems when I tried doing this:

  1. We have to open up our VPN server to public internet on ports 80,443 (so users can pull down their vpn profiles) — I don’t like this at all from point of security (and we also need to create a public DNS name for this server for proper HTTPS)
  2. its hard to automate configuration of this server via config management tools like Salt or Puppet, OpenVPN-AS uses a sqlite database to hold all configuration data, not flat text files — very cumbersome to automate configs this way (for example, if we want to add some more routes on the server)

what I want is a server that can be controlled from config management, but also has an option to email the profiles to each user directly (without them having to login to a publicly opened website) as well as a 2FA mechanism to authenticate the user.

I havent found any decent tutorials on this relatively complex setup, so maybe this will help others.

To make this work, you’ll need to do 4 things

  1. install and configure OpenVPN server, PAM rules and adjust EasyRSA script
  2. Add/Remove VPN users by using a Management script
  3. send users their profile and connection details (including QR code)
  4. have users login from their laptop and provide 2FA token using Authy app (or other 2FA apps)

Instance

this examples shows how to set this up on an EC2 instance (t2.micro) running Rocky Linux 8 (Redhat)

Linux vpn 4.18.0-348.12.2.el8_5.x86_64Rocky Linux release 8.5 (Green Obsidian)

The following repo contains 2 scripts

1 — install.sh installs OpenVPN on your Rocky Linux 8 instance, and setups a certificate CA

2 — manage.sh is used to create/remove/status users, it also generates a QR code and emails the VPN profile + the QR code to the user

Installation

make sure your Rocky 8 instance has IPv4 forwarding enabled and that SELinux is turned off

vi /etc/selinux/config
SELINUX=disabled

and add IPv4 forwarding

vim /etc/sysctl.conf
net.ipv4.ip_forward=1

reboot server to pick up these changes.

Once up, create a new directory /opt/openvpn

git clone this repository and copy both install.sh and manage.sh to /opt/openvpn

Open the install.sh script and modify the SUBNET variable to match whatever the internal subnet you want your VPN to have.

run the install.sh script, it will install and configure OpenVPN with default encryption options, it uses modern encryption algos and settings.

You can also use this script to uninstall the entire VPN setup

the script will

  1. install all necessary packages including OpenVPN (using Fedora COPR repo)
  2. configure certifcates and CA (with EasyRSA)
  3. create 2 main directories (/etc/openvpn, /opt/openvpn) — etc is for core VPN files, /opt is for user management
  4. add iptables rules
  5. create a client-template.txt file that will be used to create client profiles

Configure Server and PAM

once the installation is complete, open up the /etc/openvpn/server.conf and add the routes you want to propagate with your vpn server, heres a sample server.conf

port 1194
proto udp
dev tun
user nobody
group nobody
persist-key
persist-tun
keepalive 10 120
topology subnet
server 10.8.24.0 255.255.255.0
management 127.0.0.1 5555
ifconfig-pool-persist ipp.txt
push "redirect-gateway def1 bypass-dhcp"
ecdh-curve prime256v1
tls-crypt tls-crypt.key
crl-verify crl.pem
ca ca.crt
cert server_k5gM8Kannu0RXqvW.crt
key server_k5gM8Kannu0RXqvW.key
auth SHA256
cipher AES-128-GCM
ncp-ciphers AES-128-GCM
tls-server
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256
dh none
ecdh-curve prime256v1
client-config-dir /etc/openvpn/ccd
duplicate-cn
plugin /usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so "openvpn login USERNAME password PASSWORD pin OTP"push "dhcp-option DOMAIN mycompany.corp"push "dhcp-option DNS 10.1.2.3"
push "route 120.20.30.40 255.255.255.255" # web servers

this is a standard OpenVPN server config, except that we are using the OpenVPN Auth plugin, which calls the OpenVPN PAM module — it then provides the PAM module with a username, password and OTP token (2FA token)

lets create this OpenVPN PAM file,

create a new /etc/pam.d/openvpn file and paste this,

# OpenVPN 2FA PAMaccount required pam_unix.so
auth required pam_unix.so
auth substack password-auth
auth include postlogin
account required pam_sepermit.so
account required pam_nologin.so
account include password-auth
password include password-auth
auth requisite /usr/lib64/security/pam_google_authenticator.so secret=/opt/openvpn/google-auth/${USER} user=root authtok_prompt=pin

Here we are telling the VPN server to parse an incoming user auth request through the standard Linux auth pam stack (user + password) which will check to see if the incoming user exists on the OS itself, and that the user’s password matches the OS user password

then it checks the pam_google_authenticator.so library (this should be installed by install.sh script (yum install google-authenticator). Check the path to this .so file on your OS, it should match /usr/lib64/security path

the secret= parameter tells the plugin to check the incoming OTP token against the Google Authenticator file that is generated every time you create a new VPN user (see below). If the OTP token matches the Google secret code, it authenticates.

user=root tells the PAM module to look into the /opt/openvpn/google-auth/$USER file as “root” user, so you dont have any permission errors reading that file

authtok_prompt=pin, checks the incoming OTP token from the user’s Authy app

the sequence of the PAM stack is important, so make sure the google_auth PAM line is last in the stack, otherwise it wont work

Manage Users

to create a new user, we need to adjust the out-of-the-box EasyRSA script in order to pass a user’s password into the certificate without any prompts (otherwise we will need to manually paste a password every time we create a user)

To do this, open up /etc/openvpn/easy-rsa/easyrsa file

find the function called gen_req() (should be around line 722, depending on your EasyRSA version)

you need to modify a parameter called “opts” and add “-passout stdin”

your file should match this

save and exit the file.

To create a new User, go to /opt/openvpn and run manage.sh

root@vpn:/opt/openvpn> ./manage.sh create fred

This will do 5 things

  1. check if Fred exists as a VPN user, if he does, script exists with a message
  2. if Fred is not a system user, script will create a /bin/nologin user account on the system, generate a random password and update the system account with this password
  3. once system acct is generated, script will create a new VPN profile and certificates for the user
  4. script will generate a new QR code and secret, and save its a PNG file to /opt/openvpn/google-auth/ directory — using the qrencode binary
  5. email the user with the information that he needs to connect to your server (will also attach the fred.ovpn profile and the QR code PNG file so he can scan the QR code with his phone)

make sure you have Postfix installed and configured on your VPN server so that an email can be sent out containing the client information

To see what users you have active on your server, run

./manage.sh status

this will show any active VPN users

root@vpn:openvpn $ ./manage.sh status
V 240627133623Z AV376CP7EYT22A18F7F369DF0F1DD700 unknown /CN=fred

You can also delete/revoke a user from VPN,

./manage.sh revoke fred

this will remove all certificates from /etc/openvpn/easy-rsa/pki directories, remove any generated VPN profiles and QR codes, and finally, remove the user’s system account (only if this is a /bin/nologin VPN user — not actual human accounts)

You can also Send a profile to user without regenerating it

./manage.sh send fred

This will read the user’s password in /opt/openvpn/clients/fred/pass file, attach the QR code and Ovpn profile the email, and resend all this information to a user in case they forgot their login details.

This greatly automates the management and security of VPN users.

Prior to this, we had to manually create and delete users (we run several regional VPNs across the world, and this was a big security issue, ie, someone forgot to delete an ex-employee, for example)

Client connection (Mac OS)

clients will receive their VPN profile, username, password and QR code in their email inbox

They can then add their profile to their VPN client (ie, Tunnelblick, etc), add their username, password

they will then get a 2FA popup asking for the token from Authy app

and finally a private key passphrase (same as password)

users can save these in keychain, so they wont get prompted again

If the client provides the correct credentials and the correct 2FA code, their access is granted.

Client Connection (Linux)

on an Ubuntu-based distro, there currently is no GUI for 2FA authentication with OpenVPN. If you try to use the Network Manager to create a new VPN connection, it wont be able to connect since it will get stuck waiting for 2FA code

the only workable way is to connect via command line,

place your .ovpn file into ~/vpn
open your .ovpn profile and add these lines

auth-user-pass client.pass
askpass client.priv
script-security 2
up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf
down-pre

This will query the client.pass and client.priv files every time you connect w/out prompting you to constantly enter your VPN password,

also install resolvconf pkg to update your DNS settings

apt install resolvconf

open up client.pass file and enter your username + password

jsmith
xverDF8@9df:!

chown $(whoami):$(whoami) && chmod 600 client.pass

edit client.priv file, and enter just your password, ie

xverDF8@9df:!

run same chown and chmod on this file

Now connect to VPN server form command line,

cd ~/vpn/
sudo openvpn --config jsmith.ovpn

the client will ask for your 2FA code

Tue Apr 26 09:51:04 2022 OpenVPN 2.4.7 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Mar 22 2022
Tue Apr 26 09:51:04 2022 library versions: OpenSSL 1.1.1f 31 Mar 2020, LZO 2.10
CHALLENGE: Enter 2FA Authenticator code:

enter the code from your Authy app, you should now be connected. (VPN password and private key password are seamless as they are stored and read from client.pass and client.priv files

Troubleshooting

PAM issues

see PAM guide here

test Google Authenticator plugin by logging in as a user + their 2FA code,

adjust your /etc/pam.d/openvpn to only have this 1 line,

auth requisite /usr/lib64/security/pam_google_authenticator.so secret=/opt/openvpn/google-auth/${USER} user=root authtok_prompt=pin

Now test with pamtester

yum install pamtesterpamtester openvpn <username> authenticate

If there are additional PAM issues, use the pam_permit to always give access, regardless of failures and watch the system message logs (also change the verb in /etc/openvpn/server.conf to 5 or higher)

Debug PAM by using pam_permit (this allows all authentication to proceed — used only for debug purposes, do not run production with this!)

update /etc/pam.d/openvpn

auth requisite /usr/lib64/security/pam_google_authenticator.so secret=/opt/openvpn/google-auth/${USER} user=root authtok_prompt=pin debug forward_pass
account required pam_permit.so debug

Client connection timeout after 60 min

by default, OpenVPN will attempt to have a client renegotiation every 60 minutes (3600 sec), which will prompt the user to enter their 2FA pin to continue the connection.

If you want unlimited connection without these interruptions, update the /etc/openvpn/client-template.txt file and add “reneg-sec 0” parameter

this file should look like this:

now any new VPN profile that you generate will include this reneg parameter, which tells the client to have infinite connection w/o a renegotiation.

--

--