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
@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