Skip to content

Instantly share code, notes, and snippets.

@mrizvic
Last active June 15, 2024 11:11
Show Gist options
  • Save mrizvic/ce6353ff46d9b852500a823b004329ac to your computer and use it in GitHub Desktop.
Save mrizvic/ce6353ff46d9b852500a823b004329ac to your computer and use it in GitHub Desktop.
Static password and OTP authentication for OpenVPN in with custom python scripts
#!/usr/bin/env python3
import os
import sys
import datetime
import pyotp
import hashlib
### TO ALLOW ACCSSS CALL sys.exit(0)
### TO DENY ACCESS CALL sys.exit(1)
### PUT THIS IN OPENVPN SERVER CONFIG TO AUTHENTICATE AGAINST THIS SCRIPT
### READ MAN PAGE FOR OPENVPN TO UNDERSTAND IT!
#auth-user-pass-verify /etc/openvpn/auth.py via-env
#client-cert-not-required
#verify-client-cert none
### GENERATE PASSWORDS WITH hashlib.sha512('YourSecretPassword').hexdigest()
static_users={
'user1':'f0e1ee12360a3b2970df88460c19f62e222040d3f376cf239d76d627ab569d6e2e77e7435fd7bc376ab97d9b5066af9041ee347ec1f1d12cee3e4af68d2e2ae5',
'user2':'8fcba3a34bfa1d7c8ad1af6b09c2d20d0edf4ace9042ca761f9d369b7257ab491ab3cd9a34434b54ea6057975f1a0e8466385b4e741201b659f0e9e57559a281',
'user3':'15c8b15e99212cc292a1bc8fd936b1e4b99fd36716e6736072e5aafaeaca2af1ffa8ba01a41e593543da5bf8c67f1ad9e2b2a31fb94a6731af0bb43fe04bc8ad'
}
### GENERATE OTP PASSWORDS WITH base64.b32encode('OTPSecretString')
### THE SAME ENCODED STRING MUST BE PROVIDED TO OTP GENERATOR
otp_users={'user@otp':'J5KFAU3FMNZGK5CTORZGS3TH'}
### IF DEBUGGING IS ON THEN WE DENY ACCESS TO ALL USERS!
debug = 0
def mylogger(message):
timestamp=datetime.datetime.now().strftime('%a %b %e %H:%M:%S %Y us=%f')
processname=__file__
usrname=os.getenv('username')
untrusted_ip = os.getenv('untrusted_ip')
untrusted_port = os.getenv('untrusted_port')
### OPENVPN LOGGING FORMAT
print('{0} {4}:{5} cmd={1} username={2} {3}'.format(timestamp, processname, usrname, message, untrusted_ip, untrusted_port))
return
### CE PREVERJANJE USERJA USPE POTEM VRNI sys.exit(0)
### CE NE USPE VRNI sys.exit(1)
try:
usrname=os.getenv('username')
passwd =os.getenv('password')
if usrname is None:
myerr=('username is missing')
raise Exception(myerr)
if passwd is None:
myerr=('password is missing')
raise Exception(myerr)
### DENY ACCESS WHEN DEBUGGING
if debug == 1:
mylogger(os.getresuid())
for variable in os.environ:
value = os.getenv(variable)
mylogger('ENV {0}={1}'.format(variable,value))
sys.exit(1)
except Exception as e:
mylogger(e)
exit_val=1
else:
### IS OTP USER?
if usrname in otp_users:
### CONVERT STRING TO INT
try:
otpcode=int(passwd)
except ValueError:
myerr=('password not numeric')
raise Exception(myerr)
totp = pyotp.TOTP(otp_users[usrname])
if totp.verify(otpcode):
exit_val=0
mylogger('OTP auth success')
else:
exit_val=1
mylogger('OTP auth failed')
### IS USER WITH PASSWORD?
else:
try:
stored_passwd = static_users[usrname]
except KeyError:
exit_val=1
mylogger('username not configured in auth script')
else:
computed_passwd = hashlib.sha512(str(passwd).encode('utf-8')).hexdigest()
if computed_passwd == stored_passwd:
exit_val=0
mylogger('HASH auth success')
else:
exit_val=1
mylogger('HASH auth failed')
sys.exit(exit_val)
### WE REALLY SHOULDNT BE HERE - DENY ACCESS
mylogger('ERROR: unexpected situation')
sys.exit(1)
#!/usr/bin/env python
import os
import sys
import json
import datetime
def mylogger(message):
timestamp=datetime.datetime.now().strftime('%a %b %e %H:%M:%S %Y us=%f')
processname=__file__
usrname=os.getenv('username')
untrusted_ip = os.getenv('untrusted_ip')
untrusted_port = os.getenv('untrusted_port')
### OPENVPN LOGGING FORMAT
print '{4}:{5} cmd={1} username={2} {3}'.format(timestamp, processname, usrname, message, untrusted_ip, untrusted_port)
return
script_type = os.getenv('script_type')
if script_type == 'client-connect':
mylogger('CLIENT CONNECTED')
elif script_type == 'client-disconnect':
mylogger('CLIENT DISCONNECTED')
CLIENT_DATA = {}
for key in os.environ:
value = os.getenv(key)
#mylogger('ENV {0}={1}'.format(key,value))
CLIENT_DATA[key] = value
CLIENT_JSON = json.dumps(CLIENT_DATA, sort_keys=True)
mylogger('CLIENT_DATA={0}'.format(CLIENT_JSON))
sys.exit(0)
client
proto udp
dev tun
<ca>
-----BEGIN CERTIFICATE-----
{{ vpn_srv_cert }}
-----END CERTIFICATE-----
</ca>
remote {{ vpn_srv_host }} {{ vpn_srv_port }}
#cipher none
auth-user-pass
auth-nocache
user {{ unprivileged_user }}
group {{ unprivileged_group }}
verb 4
keepalive 10 120
persist-key
persist-tun
float
resolv-retry infinite
nobind
comp-lzo
#!/usr/bin/env python
### PARSE OPENVPN STATUS LOG FILE
### EXTRACT AND PRINT CLIENT LIST AND ROUTING TABLE
def sizeof_fmt(num, suffix='B'):
num=float(num)
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
cl_fmt = '{:10}\t{:22}\t{:16}\t{:39}\t{:9}\t{:9}\t{}'
route_fmt = '{:10}\t\t{:22}\t{:39}\t{}'
file=open("/tmp/systemd-private-2f36e5d00c9549a9ad271e5a48ea025a-openvpn@otp.service-cThXQz/tmp/openvpn-status.log","r")
for line in file:
if line.startswith('HEADER\tCLIENT_LIST'):
print("CLIENT LIST:")
print(cl_fmt).format('COMMON NAME', 'REAL ADDRESS', 'VIRTUAL ADDRESS', 'VIRTUAL IPV6 ADDRESS', 'BYTES RECEIVED', 'BYTES SENT', 'CONNECTED SINCE')
if line.startswith('CLIENT_LIST'):
line2=line.rstrip().split('\t')
cn = line2[1]
real_addr = line2[2]
virt_addr = line2[3]
virt_addr6 = line2[4] or 'None'
b_rcvd = line2[5]
b_sent = line2[6]
con_since = line2[7]
con_since_time = line2[8]
username = line2[9]
client_id = line2[10]
peer_id = line2[11]
print(cl_fmt).format(cn, real_addr, virt_addr, virt_addr6, sizeof_fmt(b_rcvd), sizeof_fmt(b_sent), con_since)
if line.startswith('HEADER\tROUTING_TABLE'):
print("")
print("ROUTING TABLE:")
print(route_fmt).format('COMMON NAME', 'REAL ADDRESS', 'VIRTUAL ADDRESS', 'LAST REF')
if line.startswith('ROUTING_TABLE'):
line2=line.rstrip().split('\t')
virt_addr = line2[1]
cn = line2[2]
real_addr = line2[3]
last_ref = line2[4]
print(route_fmt).format(cn, real_addr, virt_addr, last_ref)
file.close()
mode server
port 1194
proto udp
dev tun0
ca /etc/openvpn/ca.crt
cert /etc/openvpn/openvpnserver.crt
key /etc/openvpn/openvpnserver.key
dh /etc/openvpn/dh4096.pem
topology subnet
push "topology subnet"
server 10.1.1.0 255.255.255.0
#ifconfig 10.1.1.0 255.255.255.0
#ifconfig-pool 10.1.1.32 10.1.1.128 255.255.255.0
server-ipv6 2db8::1/64
push "route-ipv6 2000::/3"
cipher AES-256-CBC
comp-lzo adaptive
#push "comp-lzo yes"
user openvpn
group openvpn
verb 4
max-clients 8
keepalive 10 120
persist-key
persist-tun
script-security 3
reneg-sec 90000
### DONT CHECK CERTIFICATE, JUST USER/PASS
client-cert-not-required
verify-client-cert none
### EXTERNAL SCRIPT FOR STATIC PASSWORD AND OTP AUTHENTICATION
auth-user-pass-verify /etc/openvpn/auth.py via-env
### CUSTOM CONNECT/DISCONNECT EVENT LOGGER
client-connect /etc/openvpn/client-handler.py
client-disconnect /etc/openvpn/client-handler.py
### CCD STUFF
client-config-dir /etc/openvpn/ccd
### USER NEEDS CONFIG IN ccd DIR
#ccd-exclusive
### READ SETTINGS FROM FILE IN ccd DIR
username-as-common-name
### WE HAVE SYSTEMD
suppress-timestamps
### STATUS FILE
status /tmp/openvpn-status.log 10
status-version 3
@joltcan
Copy link

joltcan commented Apr 29, 2021

Thanks for the auth.py script bro! One thing, with the otp code:

    totp = pyotp.TOTP(users_otp[usrname])

Should be

    totp = pyotp.TOTP(otp_users[usrname]) 

since otp_users are where the users are defined, otherwise I'm happy!

I wanted the users to be in an external file, so I modified my version with an include

import ast

### GENERATE OTP PASSWORDS WITH base64.b32encode('OTPSecretString')
### THE SAME ENCODED STRING MUST BE PROVIDED TO OTP GENERATOR
with open('/etc/openvpn/otp_users.txt') as f:
        data = f.read()
otp_users = ast.literal_eval(data)

Then I use a script in my EasyRsa dir to generate both cert and otp keys (I generate my certs with nopass since the clients are using otp):

#!/bin/bash

if [ "$1" == "" ]
then
    echo "Error: Usage $0 <cert-name>"
    exit 0
fi

./easyrsa build-client-full "$1" nopass
if [ $? -eq 0 ]
then
    ./genconf.sh "$1" client
    BASE64=$(python -c "import base64;  print (base64.b32encode('"$1"'))")
    sed  -i "1a '$1':'$BASE64'," /etc/openvpn/otp_users.txt
fi

I use also added a check for the username == common_name since I have per user certs and per user otp's:

*************** def mylogger(message):
*** 44,47 ****
  try:
!     usrname=os.getenv('username')
!     passwd =os.getenv('password')

--- 46,50 ----
  try:
!     usrname= os.getenv('username')
!     passwd = os.getenv('password')
!     common_name = os.getenv('common_name')

*************** try:
*** 54,55 ****
--- 57,67 ----
          raise Exception(myerr)
+
+     if common_name is None:
+         myerr=('common_name is missing')
+         raise Exception(myerr)
+
+     # valid cert + valid username:
+     if common_name != usrname:
+         myerr=('common_name=%s does not match' % common_name)
+         raise Exception(myerr)

*************** else:

@mrizvic
Copy link
Author

mrizvic commented Apr 30, 2021

👍 Thanks for the bug fix and certificate feature! User definitiion in external file is also great idea!
I hope anyone else finds it useful!

@virtualizer117
Copy link

Really nice work on this--thanks for sharing!

You saved me with my OpenVPN setup. I wrote a Python script to authenticate the user-provided username and password against Azure AD, but the darn thing wouldn't run. I think I spent four hours trying to figure out why before I found you ovpnserver.conf file with the "script-security 3" directive.

Little things, right? : ) Anyways, thanks a million @joltcan and @mrizvic for the fantastic work!!

@mrizvic
Copy link
Author

mrizvic commented Mar 10, 2023

@virtualizer117 I really appreciate your comment! It really sparks the motivation to sharing is caring spirit when one puts so much effort in joining github.com just to show their gratitude to another stranger :)

@virtualizer117
Copy link

@mrizvic Absolutely! Again, so very grateful to have found this gem. The code is solid, and your files were enlightening. Y'all rock!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment