I'd like to setup an openvpn server so that clients either use a certificate to authenticate or send username & password. In certain situations I don't want to issue a certificate for clients and would be happy for them to just use username & password.

First thing I tried was to do this:

remote-cert-tls client


plugin /usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn
verify-client-cert optional

script-security 2
auth-user-pass-verify /etc/openvpn/server/auth.py via-file

That doesn't really work because the plugin and auth.py both need to be successful but when the client uses sends only the certificate without username & password, then the PAM plugin fails and even when auth.py (which at that point was just checking the validity of CN field) exits with a return status of 0, the server interprets this as an error.

I found some posts on serverfault that suggested having two server configs running on different ports that implement the different authentication strategies.

I had an idea so that I wouldn't need to do this. I disabled the PAM plugin and left everything else as it is, but I changed my auth.py to do both:

#!/usr/bin/env python3

import sys
import os
import pam

def fail_msg(msg: str):
    fn = os.environ.get("auth_failed_reason_file")
    if fn:
        with open(fn, "w") as fp:

    print("[auth.py]:", msg)

def cert_auth():
    cn = os.environ["X509_0_CN"]
    with open("/etc/openvpn/server/cn_whitelist.txt", "r") as fp:
        white_list = [x.strip() for x in fp.readlines()]

    if cn in white_list:
        fail_msg(f"The CN {cn!r} is in the white list")
        return 0

    fail_msg(f"The CN {cn!r} is not in the white list")
    return 1

def pam_auth():
    with open(sys.argv[1], "rb") as fp:
        lines = [l.strip() for l in fp.readlines()]

    if len(lines) < 2:
        fail_msg("Not enough information about password")
        return 1

    user = lines[0]
    passwd = lines[1]

    if pam.authenticate(user, passwd, service="openvpn", print_failure_messages=True):
        fail_msg(f"User {user!r} authenticated")
        return 0

    fail_msg(f"Failed to authenticate {user!r}")
    return 1

def main():
    if "X509_0_CN" in os.environ:
        return cert_auth()

    return pam_auth()

if __name__ == "__main__":

This works. My client can connect using certificates only or my client can connect using username & password (without certificates) only. However I still have the feeling that this is a very naive implementation, specially because the auth.py has no access to the client certificate.

What do you think about this script? Is it safe or am I just being naive?

  • Does this ensure that the client certificate was issued by the correct CA? Like, could I create my own self-signed certificate and arbitrarily set the CN to be one of your whitelisted ones? Feb 3 at 21:35
  • I don't know that. In fact, that was the reason I posted this question in the first place, hoping that someone would point out how easy would be to exploit my script. Sadly nobody seems to know that. At the end I decided not to use this script and decided to set verify-client-cert to require.
    – Pablo
    Feb 5 at 0:13


You must log in to answer this question.

Browse other questions tagged .