OPENVPN CLOUD IS LIVE: TRY TODAY FOR FREE

Post-auth programming notes and examples

What is a post-auth script?

In OpenVPN Access Server it is possible to load custom code in the Python programming language that runs just after a user has successfully authenticated himself to the server, but just before a VPN tunnel connection is established. With a post-auth script it is therefore possible to add additional criteria before allowing the user to connect. For example you can send a request to answer a question that is asked just after the user has entered his credentials. This can be used for one-time password systems. One of the more well-known and used programs that uses post-auth is for example Duo Security’s two-factor authentication solution that integrates with OpenVPN Access Server.

If you’re looking to integrate Google Authenticator multi-factor authentication, that has already been integrated into Access Server itself and doesn’t need to be implemented in a post-auth script. You can simply enable it in the admin UI and enroll your users on the Access Server web interface.

Some other purposes that post-auth can be used for is replacing the entire authentication system. If for example you were to set the authentication system in Access Server to PAM, and alter the PAM authentication system for the openvpnas PAM plugin to allow any credentials at all to successfully authenticate through OpenVPN Access Server, then the next step at which you can check credentials entered by users is in the post_auth script. And this code is completely up to you to write. Anything you can do in Python can then be used to create your completely own authentication system.

Hardware address checking script

This hardware address checking script or MAC address checking script is designed to work as an add-on to the existing authentication systems supported by Access Server. It adds an additional check just before the VPN tunnel fully establishes. The client is required to send IV_HWADDR flag which contains either the MAC address of the interface used to establish the VPN connection from (on Windows, mac OS, and Linux), or in the case of a mobile device (Android, iOS), a UUID.

It can operate in a manual mode where server administrators are required to manually add approved hardware addresses on the server for users and their devices. This allows administrators to lock VPN connectivity down to approved devices only. The script also supports an automatic registration mode, which is the default, where hardware addresses for first-time connections on a user account are recorded and checked on subsequent connections. This automatic registration mode can be restricted to a particular whitelisted IP so for example only people in the office can register new devices.

For more information see the OpenVPN Access Server post_auth hardware address checking script.

LDAP group mapping script

This LDAP group mapping script is designed to work when Access Server is configured to use an LDAP server as authentication backend. Users in the LDAP directory server can be part of groups, the so-called group membership property. This LDAP memberOf property can be read by the post_auth script when a user logs in at the Access Server. Based on rules you define in the post_auth script, you can assign users in particular LDAP groups to groups in the OpenVPN Access Server automatically.

The Group Permissions page in Access Server allows the server administrator to define different access rules to your network resources per group. You can assign users in Access Server to these groups so they inherit that access. Without this script, you would have to add each user account manually and assign it to the correct group. But with the post-auth LDAP group mapping script, this can be automated. You can define a list of groups that exist in the directory server, and map them to groups in the Access Server. Next time they log in, they are automatically added to the Access Server in the correct group.

For more information see the OpenVPN Access Server post_auth LDAP group mapping script.

RADIUS group mapping script

This RADIUS group mapping script is designed to work when Access Server is configured to use an RADIUS server as authentication backend.  Users in the RADIUS server can be part of groups and can contain certain flags that can be read by the post_auth script to apply certain access control settings automatically. It is primarily designed for Windows Server and mapping users to Access Server groups, but may also work on other RADIUS solutions. You can use this post-auth script to translate Active Directory groups into Access Server groups, so that scripts, permissions, IP assignments can correlate to a specific AD group, assign a static IP address to a particular user given their AD profile, and you can have AD user/group specific controls for the AS ‘admin’, ‘autologin’, ‘lzo’, ‘reroute_gw’, and ‘deny-web’ user properties.

The User Permissions and Group Permissions pages in Access Server allow the server administrator to define different access rules, IP settings, and admin access control settings. Without this script, you would have to add each user account manually and give it the desired settings. But with the post-auth RADIUS group mapping script, this can be automated. You can define a list of groups that exist in the directory server, and map them to groups in the Access Server. Next time they log in, they are automatically added to the Access Server in the correct group.

For more information see the OpenVPN Access Server post_auth RADIUS group mapping script.

Installing a post_auth script

You will need to get the post_auth script from our website and onto the filesystem of the OpenVPN Access Server. You can then load this file into the Access Server using the sacli tool. When you use the following command line, it will take the file on the filesystem and load it into the configuration database, and run it every time a user logs in at the Access Server. Note that all commands are assumed to be run as root user.

Install post_auth script on the Access Server:

cd /usr/local/openvpn_as/scripts
./sacli --key "auth.module.post_auth_script" --value_file="<POST_AUTH_SCRIPT_PATH_AND_FILENAME>" ConfigPut
./sacli start

Of course please replace <POST_AUTH_SCRIPT_PATH_AND_FILENAME> with the full path and filename of where you saved the post_auth script that you want to load. We also advise to keep a copy of this script if ever you want to update it, as changing it in the configuration database of the Access Server will be quite hard to do. If you make changes you should do it to the original file and then load it again with the above command to refresh it.

Removing an active post_auth script

In some cases, especially when you are debugging a post_auth script, you may need to remove the post_auth script from the Access Server. You can use the following command line to remove any installed post_auth script from the Access Server. Note that all commands are assumed to be run as root user.

Remove post_auth script from the Access Server:

cd /usr/local/openvpn_as/scripts
./sacli --key "auth.module.post_auth_script" ConfigDel
./sacli start

Will OpenVPN Inc. build a custom post-auth script for us?

No. We have created some example post_auth scripts that extend the functionality of Access Server, and there are third parties that also integrate their solution into Access Server using a post_auth script. But we do not offer the service of customizing a post_auth script for you. However we do have the examples provided and documentation on the post_auth system to allow a developer to create or modify a post_auth script. The programming language used is Python.

Post_auth documentation

The script is expected to be in the Python programming language. When a login event occurs at the web interface or through a VPN connection, the script you have provided and loaded into Access Server will run. The Access Server will look for a Python “post_auth" function in the script, and will call this function immediately after every successful authentication.

Post_auth looks for this function:

def post_auth(authcred, attributes, authret, info):
    ...

Please note that while the post_auth script is only called after successful username and password login, the script can veto the authentication and fail it. This means that you first have to pass the username and password login, and then post_auth can do additional checks that decide whether the user is allowed to log in or not.

Authret — script input parameters

The parameters passed to the post_auth() script call include:

  • authcred : a dictionary containing the following items:
    • username (string) — user name of VPN client provided by end user
    • client_ip_addr (string) — real IP address of VPN client
    • client_hw_addr (string, not always available) — the MAC address of the default gateway interface on the VPN client
    • static_response (string, optional) — a string entered by the user in response to a custom challenge question.
  • attributes : a dictionary containing the following items:
    • client_info — a dictionary of strings provided by the client, including:
      • UV_ASCLI_VER — version number of connecting AS client
      • IV_PLAT — client platform (‘win’, ‘mac’, or ‘linux’, ‘ios’, or ‘android’)
      • UV_PLAT_REL — specific version of client platform
      • UV_APPVER_<APP_NAME> — version number of APP_NAME installed on client
    • vpn_auth (boolean) — true if this is a VPN authentication, false if it is another type of authentication (such as web server access)
    • reauth (boolean) — true if this is a VPN mid-session reauthentication, false if it is an initial VPN authentication, and absent for non-VPN authentications.
  • authret : a dictionary containing the authentication status, and may be modified and returned by the script.

Important note on username matching: some authentication systems (such as LDAP) allow fuzzy matching on the username. This means that an entry for “Joe.User" in the LDAP DB would allow logins for “joe.user", “JOE.USER", etc. When calling the post_auth script, authcred[‘username’] will be set to the actual username entered by the user, but authret[‘user’] will be set to the canonical name of the user, i.e. the exact username string in the LDAP DB. So, for example, if “Joe.User" is listed in the LDAP DB, but the user logs in as “joe.user’, then authcred[‘username’] will be set to “joe.user" but authret[‘user’] will be set to “Joe.User". The important point here for post_auth script developers is that if you are making an authentication decision based on username, always use the canonical username: authret[‘user’].

Authret — script output parameters
If the script returns authret unmodified, there will be no effect on the authentication process, i.e. authentication will proceed as if the script was not present.
However by modifying authret, the script can effect changes in the authentication process including:

  1. Causing authentication to fail by setting ‘status’ item to FAIL.
  2. When failing authentication, generating failure strings that will be shown in the log file (‘reason’ item) or pushed to the client for display to the end user (‘client_reason’ item).
  3. Setting/changing the properties of the client instance object on the server including group, IP address, and other properties.

In detail, the authret dictionary contains the following items:

  • status (int, required) — should be set to SUCCEED or FAIL (these symbols can be imported with the statement:
from pyovpn.plugin import *
    • user (string, required) — the canonical username of the user. In some cases this username may differ from the username in authcred, for example due to LDAP case-insensitive matching. When the LDAP auth module is enabled, this username is the username actually stored in the LDAP DB, while authcred[‘username’] is the username entered by the user.
    • reason (string, optional) — on auth failure, this string will be output to the log file for diagnostic purposes
    • client_reason (string, optional) — on auth failure, this string will be sent to the VPN client and will be shown to the user in an error dialog box
    • proplist (dictionary, optional) — a list of user properties for the connecting user. In most cases, only the conn_group member need be set, since the group can define all other properties.
    • conn_group — (string) designate this user as being a member of the given group. Note — when setting conn_group in the script, you should generally include:
GROUP_SELECT = True

in the top-level, global part of your script. This tells the AS to do late user properties lookup, so that the user properties will be taken from the group chosen by the post-auth script. Additionally, any user properties returned by the script in authret[‘proplist’] will override those read from user properties DB.

  • conn_ip — (string: IP address) dynamic IP address that should be assigned to user — this IP address MUST exist within a group subnet; if conn_group is not specified, AS will try to derive the group by looking at the set of all groups, and finding the group for which this IP address is contained within group_subnets (only in Layer 3 mode)
  • prop_superuser (boolean) — designate as AS administrator
  • prop_autogenerate (boolean) — allow standard userlogin profiles
  • prop_autologin (boolean) — allow autologin profiles
  • prop_deny_web (boolean) — deny access to client web server and XML/REST web services (but not VPN access)
  • prop_lzo (boolean) — enable lzo compression
  • prop_reroute_gw_override (string)
    • disable — disable reroute_gw for this client
    • dns_only — disable reroute_gw for this client, but still route DNS
    • global — use global reroute_gw setting (default)
    • prop_expire (int) — maximum duration of non-autologin sessions (in seconds) before reauth required, 0=infinite
    • prop_expire_halt (bool) — if true, VPN client is halted on prop_expire expiration rather than being given the opportunity to reauth

The info dictionary contains the following members, depending on the current auth_method:

  • auth_method (string) — contains the auth method (‘ldap’, ‘radius’, ‘pam’, or ‘local’) and may contain special auth methods such as ‘autologin’ (certificate-only auth).

LDAP-specific

  • ldap_context (object) — this is a Python LDAP context object that can be used to perform additional LDAP queries (see example script pas.py).
  • user_dn (string) — the LDAP distinguished name of the user that is authenticating.

RADIUS-specific

  • radius_reply (dictionary) — attributes received from the RADIUS server as part of the successful authentication reply

Return Value

The post_auth function must return either authret or (authret, proplist_save) where proplist_save is a set of key/value pairs (dictionary) that should be saved back into the user properties DB for future use. The optional proplist_save dictionary allows the script to save state in the user properties DB record. This can be used for such functionality as enforcing a “hardware lock", i.e. requiring that users only log in from client machines having a known MAC address. This is demonstrated in the sample post_auth script (pas.py).

Exceptions

If a Python exception is thrown by the post_auth function, authentication will fail, and the reason string will be set to the Python error message.

Testing

To test the post_auth script, go to the /usr/local/openvpn_as/scripts directory and use the authcli tool. Load the post_auth script and then run authcli to test authentication.

For example, we will test using the username ‘test’ on the sample post_auth script pas.py:

$ ./authcli -u test
API METHOD: authenticate
Password: <non-echo password entry>
AUTH_RETURN
  status : SUCCEED
  reason : LDAP auth succeeded on ldap://...
  user : test
  proplist : {'prop_autogenerate': 'true', 'prop_deny': 'false',
              'prop_autologin': 'true', 'conn_group': 'default',
              'type': 'user_connect', 'prop_superuser': 'false'}

Note how conn_group is set to default due to the actions of the script.

Notes

The post-auth script will NOT be run for the “bootstrap" user. This is done to prevent admin account lockout in the event that the post-auth script fails to execute.
The bootstrap user is the initially configured admin user and is always authenticated via PAM. See the boot_pam_users.0 parameter in /usr/local/openvpn_as/etc/as.conf for the currently configured bootstrap user.

Host-checker query file

This adds a check that the VPN client must satisfy by providing the correct response. It is possible to request the VPN client to read the list of installed programs on the client computer and then have the client report this to the server. The server can then check it in the post_auth script and based on that information allow the user to login or not.

There are some limitations to this host-checking. At the moment it is only supported by OpenVPN Connect Client v2 on Windows and mac OS. Support for it is being added to OpenVPN Connect v3 as well. But open source OpenVPN clients will not support this business requirement and as such these clients will not be able to pass this test.

While a post-auth script can verify the existence and version numbers of applications on the client, it is first necessary to construct a host-checker query file to enumerate the applications of interest so that the client can report on their status. The host-checker query file uses the following grammar:

# comment
[PLATFORM1|'all']
NAME1=REGEX1
NAME2=REGEX2
...
[PLATFORM2|'all']
NAME1=REGEX1
NAME2=REGEX2
...

Where:

PLATFORM: one of ‘win’, ‘linux’, ‘mac’, or ‘all’ (all matches all platforms)
NAME: short user-defined symbolic name for the application (can contain alpha-numeric and ‘_’)
REGEX: a case-insensitive python regular expression that will be matched against the full application name

The client will use the Host-checker Query File to determine which client apps to report on. If an application name is matched by a REGEX, its version number will be returned to the Access Server and be accessible to the post_auth script via the attributes[‘client_info’] dictionary. For each application NAME, the version of the application will be returned as UV_APPVER_<NAME> (string) in the attributes[‘client_info’] dictionary. If an application lookup error occurs, UV_APPVER_<NAME> will be set to one of the following error strings:

ERR_NOT_FOUND: the application was not found
ERR_NO_VERSION: the application was found, but no version number was specified
ERR_MANY_FOUND: the REGEX is too broad and matches more than one application
ERR_REGEX: the REGEX could not be compiled

For example, the following one-line Host-checker Query File would return the version number of Mozilla Firefox installed on the client to the post-auth function:

FIREFOX=^mozilla firefox

For the purposes of the example, we will assume that the above line is saved in a file called appver.txt on the OpenVPN Access Server. To load the file into the Access Server, use the following commands from /usr/local/openvpn_as/scripts.

Load the host_checker file into Access Server:

./sacli --key "vpn.client.app_verify" --value_file="appver.txt" ConfigPut
./sacli start

The above commands will then cause appver.txt to be embedded as metadata in all client profiles generated from the Access Server after this point. In turn, when these profiles are used to connect to the Access Server, the version number of specified applications will be passed to the post-auth script. In particular, if the Firefox host-checker query file shown above was used, the Firefox version number (or error string) will be accessible as:

attributes['client_info']['UV_APPVER_FIREFOX']

Please note that the client will only provide attributes[‘client_info’] information to trusted servers.

To simplify the process of writing the Host-checker Query File, a command line script is provided on the client to enumerate all known applications and their version numbers. This can be used to craft the REGEX expressions to match the client applications of interest. This script is currently only available on OpenVPN Connect v2.

Show a list of installed programs on the client operating system:

./capicli ShowApps

Setting environmental variables on client

A post-auth script can send data to a client-side connect script in the form of environmental variables. For example, executing the following code fragment in a post-auth script will pass to the client script an environmental variable called “MY_VAR" having a value of “Test Value":

# get user's property list, or create it if absent
proplist = authret.setdefault('proplist', {})
# set client-script environmental variable MY_VAR="Test Value"
authret['proplist']['prop_cli.script_env.all.MY_VAR'] = 'Test Value'

As an alternative, it is possible to pass data to the client script using stdin (standard input). This method is preferred over environmental variables when passing security-sensitive data. See the example script pasvar.py for more info.

Challenge/response in OpenVPN protocol

OpenVPN supports a challenge/response system. The basic idea here is that after the user has passed username and password check, an additional question is asked that the user must answer before they’re allowed to log in. This question will either pop up on the VPN client program, or on the Access Server web interface, depending on where you log in.

A typical use-case for this is multi-factor authentication, where a user enters username and password, and then on top of that, an additional custom question is asked that the user must answer before they are allowed to login. Like for example a TOTP password. We have that system built-in though, we support the Google Authenticator method, and this doesn’t require an additional script. But if you want to build your own solution, that is possible.

There are two variations of the challenge/response model in OpenVPN and these are dynamic and static.

Dynamic protocol details

In dynamic challenge/response when the login starts, the client is not aware there is a requirement for additional authentication. So it will prompt just for username and password, and send that to the server, and then wait for the server to allow us in or give us a failure message.

Since the client was not aware there was a requirement for an additional challenge/response and only username and password were sent, the post_auth script will fail the connection and require the client to also answer the challenge. The challenge text and response input field is shown to the user in a separate dialog box that is raised after a successful initial authentication. The username and password is delivered to the post-auth script in an initial transaction, and the challenge response is delivered later via a second transaction with a state dictionary (crstate) used to link the two transactions.

You can use the dynamic protocol when you want to use different challenge text for each login session (although the dynamic protocol will work fine for static challenge text as well). It is also useful when you have your VPN clients already installed and provisioned with connection profiles, and you wish to enable a challenge/response afterwards.

Static protocol details

In static challenge/response when the login starts, the client is aware that on top of username and password there is a requirement for additional authentication. This information will then be gathered on the VPN client before there is even any communication with the VPN server.

The challenge text is constant across all users and login sessions and is embedded in the client config file. The challenge text and response input field is included in the initial username/password dialog. The username, password, and response is delivered in one transaction to the post-auth script in “authcred". Use the static protocol when the the challenge text is constant for all login sessions.

The static protocol is generally only supported for VPN login. Other types of sessions (such as web sessions) support only the dynamic protocol. For this reason, post-auth scripts that support the static protocol must be able to fall back to the dynamic protocol if authcred[‘static_response’] is undefined. The pascrs.py script demonstrates this. The Static protocol is more efficient than the dynamic protocol because the username, password, and challenge response can be queried from the user in a single flow and then delivered to the server in one transaction.

Client-side support for challenge/response

These days almost every OpenVPN compatible VPN client supports it. On Linux on the command line you may have to add the auth-retry interact directive to tell it that when you enter your username and password and you get a failure with a challenge embedded in it back, that you wish to continue the login process and provide the response to the given challenge, and then connect. But for pretty much all other OpenVPN clients, challenge/response is the method used for MFA, and this is commonly used nowadays.

Share