Is this openvpn auth script secure or am I’m just being too naive?
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
auth-user-pass-optional
username-as-common-name
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:
fp.write(msg+"\n")
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__":
sys.exit(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?