Post-auth programming notes and examples


OpenVPN Access Server supports post-auth scripts to enable you to add additional criteria for allowing users to connect. The steps are programmatically added after a user successfully authenticates, but before the connection starts.

What is a post-auth script?

In OpenVPN Access Server it is possible to load custom code in Python that runs after a user has successfully authenticated to the server, but before a VPN tunnel connection is established. With a post-auth script it is therefore possible to specify additional criteria before allowing the user to connect. For example, you can use a post-auth script to send a request to answer a question asked after the user has entered credentials. This type of script can be used for one-time password systems.

One of the more well-known programs that uses post-auth is Duo Security’s two-factor authentication solution that integrates with OpenVPN Access Server. If you’re looking to integrate Google Authenticator multi-factor authentication, we’ve integrated it directly into Access Server and it doesn’t need a post-auth script. You can simply enable it in the Admin Web UI.

Using post-auth for your own authentication system

You can disable primary authentication using AUTH_NULL. This means that the post-auth script assumes full responsibility for authentication. For more information see Using AUTH_NULL for Custom Authentication.

Hardware address checking script

You can add hardware address checking to your existing authentication system to allow only devices with allowed hardware or MAC addresses to connect.

The VPN client sends the IV_HWADDR flat containing either the MAC address or a UUID.

By default, it supports automatic registration where hardware addresses for first-time connections on a user account are recorded and checked on subsequent connections. It can also run in a manual mode where you must add approved hardware addresses on the server for users and their devices.

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

LDAP group mapping script

This LDAP group mapping script works when Access Server is configured to integrate with an external authentication service using LDAP.

  • 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 you can automate this. 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 works when Access Server is configured to integrate with an external authentication service using RADIUS.

  • 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.
  • The post_auth script 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, and 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, you can automate this. 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.

SAML Group Mapping Script

Groups in Access Server can have specific access rules and subnets that users assigned to these groups inherit. While it is possible to manually add users to  groups in Access Server, this can also be automated using our SAML group mapping script for Access Server.

Users in the SAML IdP can be part of groups. This property must be configured on the SAML IdP and in our guides that show how to do this, it is usually the groups property that can then be read by the post_auth script when a user logs in at the Access Server.

Based on the rules you define in the post_auth script, you can automatically assign users in particular SAML groups to groups in the OpenVPN Access Server. Next time they log in at the Access Server they are automatically assigned to the corresponding group.

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

Script for Duo two-factor authentication

You can integrate OpenVPN Access Server with Duo and add two-factor authentication options such as SMS, push notifications, Yubikey, and so on. For the setup, you'll download a post-auth script from GitHub to upload to your Access Server.

For more information see the Duo two-factor authentication to OpenVPN Access Server page.

Installing a post_auth script

To install a post_auth script, follow this general process:

  1. Download the script from our website.
  2. Modify the script as needed for your configuration.
  3. Load it onto the filesystem of your Access Server.
  4. Install it on Access Server (signed in with root privileges):
    • cd /usr/local/openvpn_as/scripts
    • ./sacli --key "auth.module.post_auth_script" --value_file="<FILE>" ConfigPut
    • ./sacli start
  5. Make sure you replace <FILE> with the full path and filename where you saved the post_auth script.

We recommend you keep a copy of the script for any needed updates. Changing it in Access Server’s configuration database is quite hard to do. Make your changes to the original file then load it again with the above command to refresh it.

Removing an active post_auth script

You may need to remove the post_auth script from your Access server, especially when debugging.

To remove a post_auth script, follow this general process:

  1. Sign in to your Access Server console with root privileges.
  2. Run these commands:
    • 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 provide you with 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. We don’t offer custom post_auth scripts. 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 3.

Post_auth documentation

  • Post_auth scripts must be in the Python programming language.
  • Your post_auth script runs when a login event occurs at the web interface or through a VPN connection.
  • The Access Server looks for a Python "post_auth" function in the script, and calls this function immediately after every successful authentication.
  • On Access Server 2.9 and older the post_auth script does not execute for bootstrap accounts.

Post_auth looks for this function:

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

Note: the post_auth script is only called after successful username and password login, however the script can veto the authentication and fail it. This means that you must first 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’s no effect on the authentication process, i.e. authentication proceeds as if the script isn’t 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. Generate failure strings to the log file ('reason' item) or pushed to the client for display to the end user ('client_reason' item) when authentication fails.
  3. Set or change 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_context (object) — this is a Python LDAP context object that can be used to perform additional LDAP queries (see example script
  • user_dn (string) — the LDAP distinguished name of the user that is authenticating.


  • 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 (


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.


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

$ ./authcli -u test
API METHOD: authenticate
Password: <non-echo password entry>
  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.

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


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:


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 environment 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 for more info.

Note: Support for environment variables on the client side is only present on OpenVPN Connect v2 client software.

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