-
-
Save mrizvic/ce6353ff46d9b852500a823b004329ac to your computer and use it in GitHub Desktop.
#!/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 |
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!!
@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 :)
@mrizvic Absolutely! Again, so very grateful to have found this gem. The code is solid, and your files were enlightening. Y'all rock!!
👍 Thanks for the bug fix and certificate feature! User definitiion in external file is also great idea!
I hope anyone else finds it useful!