So far I’ve seen 2FA/MFA with OpenVPN using a 3rd Party plugin openvpn-otp.so
from evgeny-gridasov/openvpn-otp, but after I got it working I didn’t like the way it implemented HOTP counter storage and the use of otp-secrets
. There has to be another way.
I see that there is a native openvpn-plugin-auth-pam.so
, and also know that on another system we’re using the OATH toolkit for providing OTP for sshd. The OATH toolkit includes pam_oath.so
. This means there is a common link for me to make use of PAM to give me MFA for OpenVPN.
Server Configuration
As I already have a configured OpenVPN server, configured with LDAP auth, all I need to install oathtool
, qrencode
and libpam-oath
.
sudo apt install oathtool qrencode libpam-oath
Create a file for my OTP users /etc/users.oath
and set it, so only nobody can read it.
touch /etc/users.oath
chown nobody: /etc/users.oath
Create an OTP secret using something that will give you a sha1 hash, eg.
openssl rand -hex 64 | sha1sum | cut -d' ' -f1
Copy the hash and then edit the /etc/users.oath
file, so it includes your user’s id and the hash we copied, eg.
#type username pin secret-hash
HOTP/T30/6 user.name - 6776deedd82c054d86cc87bc44324318c4334967
Create /etc/pam.d/openvpn
, owned and writable only by root, readable by all. It only has one line in it.
auth requisite pam_oath.so usersfile=/etc/users.oath window=30 digits=6
Add the plugin to the OpenVPN server.conf
file.
plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so "/etc/pam.d/openvpn login USERNAME password PASSWORD One-time OTP"
For this to work, PAM must be able to look up a user by name/id. If it can’t do this, you will get errors in the log that look like this:
client openvpn: pam_unix(openvpn:account): could not identify user (from getpwnam(user.name))
This means that you must install and configure the LDAP client for PAM. You must be able to use id user.name
in order for the PAM module to work. Even though the OpenVPN plugin for LDAP does not require PAM to succeed, the pam_oath.so
must be able to find the user.
Make sure you restart OpenVPN after these changes.
sudo systemctl restart openvpn@server
User Token Generation
To get the token generation onto a device like FreeOTP or Google Authenticator, you need to get the secret as a base32 hash. The easiest way to do it is to use oathtool
to generate a token with verbose mode, eg.
$ oathtool --totp -v 6776deedd82c054d86cc87bc44324318c4334967
Hex secret: 6776deedd82c054d86cc87bc44324318c4334967
Base32 secret: M53N53OYFQCU3BWMQ66EIMSDDDCDGSLH
Digits: 6
Window size: 0
TOTP mode: SHA1
Step size (seconds): 30
Start time: 1970-01-01 00:00:00 UTC (0)
Current time: 2022-09-01 14:29:49 UTC (1662042589)
Counter: 0x34D5BCB (55401419)
506918
You can then just copy the base32 secret and add it to the string as below, to generate a QR code with.
qrencode -t UTF8 'otpauth://totp/openvpn:user.name?secret=M53N53OYFQCU3BWMQ66EIMSDDDCDGSLH'
Using UTF8 creates a QR code on a terminal you can scan immediately. You can also use PNG and direct the output to a file you can send to the user to scan into FreeOTP, etc.
qrencode -t PNG 'otpauth://totp/openvpn:user.name?secret=M53N53OYFQCU3BWMQ66EIMSDDDCDGSLH' -o user.name.png
server.conf
PAM responds to prompts that are returned by the pam_oath.so
plugin.
The parameters are to use the openvpn pam.d file, and respond to a login
prompt with the USERNAME
, a password
prompt (not used) with the PASSWORD
, and, the final bit of magic that makes it work, a One-time
prompt with the OTP
.
The prompt in the debug log can be seen as:
2022-09-01 11:41:07 us=982761 PLUGIN AUTH-PAM: BACKGROUND: name match found, query/match-string ['One-time password (OATH) for `user.name': ', 'One-time'] = 'OTP'
PAM uses the One-time
as a match to the actual prompt One-time password (OATH) for 'user.name':
and sends it the OTP
.
This is what my server.conf
ends up looking like:
proto udp
port 1194
dev tun0
server 192.168.255.0 255.255.255.0
topology subnet
verb 2
user nobody
group nogroup
tls-server
ca /etc/openvpn/pki/ca.crt
cert /etc/openvpn/pki/issued/server.crt
crl-verify /etc/openvpn/crl.pem
# dh /etc/openvpn/pki/dh.pem
dh none
key /etc/openvpn/pki/private/server.key
key-direction 0
tls-auth /etc/openvpn/pki/ta.key
status /var/log/openvpn/openvpn-status.log
log /var/log/openvpn/openvpn.log
push "route 10.0.0.0 255.255.255.0"
push "route 128.0.0.0 128.0.0.0"
push "redirect-gateway def1 block-local"
push "dhcp-option DNS 10.0.0.254"
push "comp-lzo no"
plugin /usr/lib/openvpn/openvpn-auth-ldap.so /etc/openvpn/server/auth/auth-ldap.conf
auth-gen-token 43200
auth-nocache
client-config-dir ccd
comp-lzo no
float
keepalive 10 60
#opt-verify
persist-key
persist-tun
cipher AES-256-GCM
ecdh-curve secp384r1
ncp-disable
remote-cert-tls client
tls-cert-profile preferred
tls-cipher TLS-ECDHE-ECDSA-WITH-CHACHA20-POLY1305-SHA256:TLS-ECDHE-RSA-WITH-CHACHA20-POLY1305-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256
tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
tls-version-min 1.2
verify-client-cert require
plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so "/etc/pam.d/openvpn login USERNAME password PASSWORD One-time OTP"
auth-ldap.conf
<LDAP>
# LDAP server URL
URL ldap://ldap-01
# Bind DN (If your LDAP server doesn't support anonymous binds)
BindDN "cn=readonly,dc=domain,dc=tld"
# Bind Password
Password SecretKey
# Network timeout (in seconds)
Timeout 15
# Enable Start TLS
TLSEnable yes
# Follow LDAP Referrals (anonymously)
FollowReferrals yes
</LDAP>
<Authorization>
# Base DN
BaseDN "ou=staff,ou=people,dc=domain,dc=tld"
# User Search Filter
SearchFilter "(&(uid=%u)(memberOf=cn=access-service-openvpn,ou=groups,dc=domain,dc=tld))"
# Require Group Membership
RequireGroup false
<Group>
BaseDN "ou=groups,dc=domain,dc=tld"
SearchFilter "(memberOf=cn=access-service-openvpn,ou=groups,dc=domain,dc=tld)"
MemberAttribute uniqueMember
</Group>
</Authorization>
user.ovpn
client
nobind
dev tun
remote-cert-tls server
remote 123.123.0.1 1194 udp
<key>
-----BEGIN ENCRYPTED PRIVATE KEY-----
SuperSecretKey
-----END ENCRYPTED PRIVATE KEY-----
</key>
<cert>
-----BEGIN CERTIFICATE-----
SecretKey
-----END CERTIFICATE-----
</cert>
<ca>
-----BEGIN CERTIFICATE-----
SecretKey
-----END CERTIFICATE-----
</ca>
key-direction 1
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
SecretKey
-----END OpenVPN Static key V1-----
</tls-auth>
redirect-gateway def1
It’s also worth noting that this is a TOTP (Time Based One Time Password) not HOTP (HMAC based One Time Password). This means that the user.auth file is not being used to store HOTP counters.
References
https://blog.securityevaluators.com/hardening-openvpn-in-2020-1672c3c4135a
Known issue with using --opt-verify
https://forums.openvpn.net/viewtopic.php?t=29557
I like this. How are the users entering the OTP? Which client?
LikeLike
The OTP is available on all clients except Linux with Gnome :( Sadly there has been an outstanding issue with the Gnome network manager that has been waiting for someone to add it – for the past 4 years at least!
OTP works fine with Windows and Android. Just with Gnome you must use a CLI terminal to use OTP.
LikeLike
Warl0rd could you provide your auth-ldap.conf? My configuration works with ad but not work with otp, what you do to make both ldap and otp work?
LikeLike
Updated post with auth-ldap.conf that suits our domain structure. Check the ou’s etc. regarding your own setup.
It’s a bit belt and braces as it filters and searches for the same group membership.
LikeLike
Would this work with OpenVPN which uses Mysql for users and passwords?
I have this in my openvpn server configuration which handles the auth:
plugin /usr/lib/libopenvpn-mysql-auth.so -c /etc/openvpn/mysql-auth/mysql-auth.conf
So right now, a user has an .ovpn file which is added to his openvpn client, then it has to enter user + password in order to authenticate. On top of that I want to add 2FA handled by Google Authenticator.
So basically the only difference between your system is mine is that you use LDAP for auth while I’m using MySQL. The thing is that I was not able to find the proper config to make it work.
I can share the current config if needed.
LikeLike
No reason why not. If you can auth with one plugin, you should be able to with the other. The key to this is getting pam to work with the pam-oath library. Which may mean syncing uses from mysql into the users file.
LikeLike
Yeah well I tried in different ways. I can share the current config, maybe you’ll be able to help. It’s a requirement to use 2FA for OpenVPN so we have to accomodate that. We’re using OpenVPN + MySQL since we can add groups of users, groups of servers, ips, routes etc. I don’t mind having the users authenticating via MySQL (user and password) and then having a file on disk with the 2FA stuff. I lost the entire day yesterday but didn’t succeed
LikeLike
First step, get auth working without MFA. I’ve never used sql for auth, and not sure of any password hashing you may need to do to get that going.
We’ve actually moved away from OpenSSL in favour of WireGuard with an OTP add on. https://warlord0blog.wordpress.com/2022/09/29/wireguard-otp-and-acls/
I can lose an entire day simply not spotting a spelling mistake :)
LikeLike
Warlord could you provide me client.ovpn? I just set up auth ldap but otp not working
LikeLike
user.ovpn client config added to post with certs redacted.
LikeLike
Interesting, if I understand, user.name – is it user from ad and client and should be add to users. oath, but I always get error – work only ldap or Google totp but not both
LikeLike
And on the client need to enter password +otp or password with totp separately?
LikeLike
The OTP is entered separately. It should provide an entry prompt after the username/password. But as per my previous comment, as it does not work with Gnome you MUST use command line.
LikeLike
It seems like openvpn-plugin-auth-pam.so module not works correctly
LikeLike
Warlord, what version of libpam-oath do you use? Debian or Ubuntu? Another question is what is your version of libpam-oath?
LikeLike
libpam-oath/stable,now 2.6.6-3 amd64 [installed]
OATH Toolkit libpam_oath PAM module
PRETTY_NAME=”Debian GNU/Linux 11 (bullseye)”
LikeLike
user.name – should be samaccountname from ad domain? Am I right?
LikeLike
2023-06-20 13:47:18 us=796619 46.216.179.13:22839 PLUGIN_CALL: POST /usr/lib/openvpn/openvpn-auth-ldap.so/PLUGIN_AUTH_USER_PASS_VERIFY status=0
2023-06-20 13:47:18 us=796861 PLUGIN AUTH-PAM: BACKGROUND: received command code: 0
2023-06-20 13:47:18 us=796963 PLUGIN AUTH-PAM: BACKGROUND: USER: test-vpn
2023-06-20 13:47:18 us=801579 PLUGIN AUTH-PAM: BACKGROUND: my_conv[0] query=’login:’ style=2
2023-06-20 13:47:18 us=801614 PLUGIN AUTH-PAM: BACKGROUND: name match found, query/match-string [‘login:’, ‘login’] = ‘USERNAME’
2023-06-20 13:47:18 us=801662 PLUGIN AUTH-PAM: BACKGROUND: my_conv[0] query=’One-time password (OATH) for `test-vpn’: ‘ style=1
2023-06-20 13:47:18 us=801668 PLUGIN AUTH-PAM: BACKGROUND: name match found, query/match-string [‘One-time password (OATH) for `test-vpn’: ‘, ‘One-time’] = ‘OTP’
2023-06-20 13:47:18 us=801740 PLUGIN AUTH-PAM: BACKGROUND: user ‘test-vpn’ failed to authenticate: Conversation error
2023-06-20 13:47:18 us=801999 46.216.179.13:22839 PLUGIN_CALL: POST /usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so/PLUGIN_AUTH_USER_PASS_VERIFY status=1
2023-06-20 13:47:18 us=802012 46.216.179.13:22839 PLUGIN_CALL: plugin function PLUGIN_AUTH_USER_PASS_VERIFY failed with status 1: /usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so
2023-06-20 13:47:18 us=802049 46.216.179.13:22839 TLS Auth Error: Auth Username/Password verification failed for peer
LikeLike
Great tutorial! I am however having one issue I am using OpenLDAP and fail to get PAM talking with LDAP on the same host. Are you able to share what ldap client you used along with configuration please?
LikeLiked by 1 person
Sadly, no. I switched to using WireGuard, and the OVPN server is long gone.
The section for auth-ldap.conf should give you all you need, though. If you’re using it with STARTTLS/TLS you will have to have certificates configured on your LDAP server. Even a self-signed should do it. If you can set “REQCERT no”, but best off trying without.
Also, note that this is not using pam auth for LDAP, it’s using the LDAP plugin for OpenVPN. Achieves the same thing, but does not require pam to get involved.
LikeLike
0 Pingbacks
Search this site
Categories
Recent Posts
Archives
Tag Cloud
active directory ajax ansible apache asterisk authentication azure backup bash Bootstrap certificates cloudflare CoffeeScript debian dhcp dkim dns Docker electron electronics email esp32 esp8266 exim4 firewall ftp git gnome horizon html5 iptables java jquery json juniper keycloak kodi kvm Laravel ldap manjaro mssql mysql nginx node.js nzbget oauth2 openvpn owncloud php postgis postgresql proxy python qemu radius raspberry pi ReactJS rsync Security single-sign-on smtp spf ssh ssl synology tomcat updates vmware vpn vue.js webpack wireguard xml xmpp