Last active
August 19, 2020 10:24
-
-
Save justmiles/deba3fc680e35719cf98c7533591853e to your computer and use it in GitHub Desktop.
OpenVPN post_auth MAC address check via JumpCloud
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# OpenVPN Access Server MAC address checking post_auth script. | |
# Johan Draaisma | |
# | |
# This script can be used with LOCAL, PAM, LDAP, and RADIUS authentication. | |
# It adds an additional check when authentication is done through the VPN connection. | |
# It applies to all 3 connection profiles types (server-locked, user-locked, auto-login). | |
# Windows, Linux, and macOS will be reporting MAC addresses. However, Android and iOS | |
# devices will not, due to technical reasons. They will instead by reporting UUID. For | |
# simplicity and legacy reasons we will just call it MAC address from here on in, but | |
# it can be in theory any unique hardware-based string that the client reports to us. | |
# | |
# Please note: RADIUS/LDAP case insensitivity may lead to the system recognizing | |
# Billy.Bob and billy.bob as 2 separate accounts, when using RADIUS/LDAP. | |
# | |
# | |
# Full documentation and explanation can be found here: | |
# https://openvpn.net/vpn-server-resources/access-server-post-auth-script-host-checking/ | |
# | |
# Script last updated in January 2020 | |
import re | |
import subprocess | |
import os | |
from pyovpn.plugin import * | |
# Optionally set this string to a known public IP address (such as the | |
# public IP address of machines connecting from a trusted location, such | |
# as the corporate LAN). If set, all users must do the first login from this | |
# IP address, so the machine's hardware (MAC/UUID) address can be recorded. | |
# | |
# If this is empty, which it is by default, then registrations for the first | |
# MAC address for a user account will be done automatically on first login. | |
# | |
# If this is set to "NONE" or "DISABLED" then the server administrator must | |
# always manually register each MAC/UUID address by hand on the command line. | |
# For that, we refer you to our documentation. | |
first_login_ip_addr = "" | |
# If False or undefined, AS will call us asynchronously in a worker thread. | |
# If True, AS will call us synchronously (server will block during call), | |
# however we can assume asynchronous behavior by returning a Twisted | |
# Deferred object. | |
SYNCHRONOUS = False | |
# this function is called by the Access Server after normal VPN or web authentication | |
def post_auth(authcred, attributes, authret, info): | |
print("********** POST_AUTH %s %s %s %s" % | |
(authcred, attributes, authret, info)) | |
# get user's property list, or create it if absent | |
proplist = authret.setdefault('proplist', {}) | |
# user properties to save - we will use this to pass the hw_addr_save property to be | |
# saved in the user property database. | |
proplist_save = {} | |
error = "" | |
# If a VPN client authentication attempt is made, do these steps: | |
# Check if there is a known MAC address for this client | |
# If not, register it | |
# If yes, check it | |
# | |
# An additional optional requirement is that first time registration must occur | |
# from a specific IP address, as specified in the first_login_ip_addr set above | |
# | |
# The 'error' text goes to the VPN client and is shown to the user. | |
# The 'print' lines go to the log file at /var/log/openvpnas.log (by default). | |
# only do this for VPN authentication | |
if attributes.get('vpn_auth'): | |
# MAC address reported by the VPN client | |
hw_addr = authcred.get('client_hw_addr') | |
# User name of the VPN client login attempt | |
username = authcred.get('username') | |
# IP address of VPN client login attempt | |
clientip = authcred.get('client_ip_addr') | |
if hw_addr: | |
hw_addr_save = proplist.get('pvt_hw_addr') # saved MAC address | |
# saved MAC address (secondary) | |
hw_addr_save2 = proplist.get('pvt_hw_addr2') | |
# Check JumpCloud first | |
if authorizedInJumpcloud(username, hwaddr): | |
print("***** POST_AUTH MAC CHECK: account user name : %s" % username) | |
print("***** POST_AUTH MAC CHECK: client IP address : %s" % clientip) | |
print("***** POST_AUTH MAC CHECK: client MAC/UUID address : %s" % hw_addr) | |
print("***** POST_AUTH MAC CHECK: MAC/UUID authorized via : JumpCloud") | |
print("***** POST_AUTH MAC CHECK: connection attempt : SUCCESS") | |
return authret, proplist_save | |
if hw_addr_save: | |
if not hw_addr == hw_addr_save and not hw_addr == hw_addr_save2: | |
error = "The hardware MAC/UUID address reported by this VPN client does not match the registered address." | |
print("***** POST_AUTH MAC CHECK: account user name : %s" % username) | |
print("***** POST_AUTH MAC CHECK: client IP address : %s" % clientip) | |
print("***** POST_AUTH MAC CHECK: client MAC/UUID address : %s" % hw_addr) | |
if hw_addr_save2: | |
print("***** POST_AUTH MAC CHECK: expected MAC/UUID address : %s or %s" % | |
(hw_addr_save, hw_addr_save2)) | |
else: | |
print("***** POST_AUTH MAC CHECK: expected MAC/UUID address : %s" % hw_addr_save) | |
print("***** POST_AUTH MAC CHECK: connection attempt : FAILED") | |
else: | |
print("***** POST_AUTH MAC CHECK: account user name : %s" % username) | |
print("***** POST_AUTH MAC CHECK: client IP address : %s" % clientip) | |
print("***** POST_AUTH MAC CHECK: client MAC/UUID address : %s" % hw_addr) | |
if hw_addr_save2: | |
print("***** POST_AUTH MAC CHECK: expected MAC/UUID address : %s or %s" % | |
(hw_addr_save, hw_addr_save2)) | |
else: | |
print("***** POST_AUTH MAC CHECK: expected MAC/UUID address : %s" % hw_addr_save) | |
print("***** POST_AUTH MAC CHECK: connection attempt : SUCCESS") | |
else: | |
# First login by this user, save MAC addr. | |
if not first_login_ip_addr or first_login_ip_addr == clientip: | |
proplist_save['pvt_hw_addr'] = hw_addr | |
print("***** POST_AUTH MAC CHECK: account user name : %s" % username) | |
print("***** POST_AUTH MAC CHECK: client IP address : %s" % clientip) | |
print("***** POST_AUTH MAC CHECK: client MAC/UUID address : %s" % hw_addr) | |
print("***** POST_AUTH MAC CHECK: action taken : MAC address learned and locked.") | |
print("***** POST_AUTH MAC CHECK: connection attempt : SUCCESS") | |
else: | |
error = "Your attempt to login from an address not approved for MAC/UUID address registration has been denied." | |
print("***** POST_AUTH MAC CHECK: account user name : %s" % username) | |
print("***** POST_AUTH MAC CHECK: client IP address : %s" % clientip) | |
print("***** POST_AUTH MAC CHECK: action taken : attempt to register client MAC/UUID address from restricted address denied.") | |
print("***** POST_AUTH MAC CHECK: connection attempt : FAILED") | |
else: | |
error = "VPN client is not reporting a MAC/UUID address. Please verify that a suitable OpenVPN client is being used." | |
print("***** POST_AUTH MAC CHECK: account user name : %s" % username) | |
print("***** POST_AUTH MAC CHECK: client IP address : %s" % clientip) | |
print("***** POST_AUTH MAC CHECK: client MAC/UUID address : NONE REPORTED") | |
print("***** POST_AUTH MAC CHECK: action taken : VPN connection denied with a suitable error message.") | |
print("***** POST_AUTH MAC CHECK: connection attempt : FAILED") | |
# process error, if one occurred | |
if error: | |
authret['status'] = FAIL | |
# this error string is written to the server log file | |
authret['reason'] = error | |
# this error string is reported to the client user | |
authret['client_reason'] = error | |
return authret, proplist_save | |
def authorizedInJumpcloud(username, hwaddr): | |
completedProc = subprocess.run( | |
'jc user attribute-matches --username %s --key hwaddr --value %s' % (username, hwaddr), shell=True) | |
print(completedProc.returncode) | |
if completedProc.returncode == 0: | |
return True | |
else: | |
return False |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment