Networking & Content Delivery

Enforcing VPN access policies with AWS Client VPN connection handler

Overview

AWS Client VPN, launched in 2018, enables you to use your OpenVPN-based clients to securely access your AWS and on-premises networks from anywhere. With recent updates, you can also enforce additional security policies on connections to a Client VPN endpoint by configuring a client connect handler (referred to as the “handler” in this post). The handler runs custom logic while establishing a connection. You can use this to authorize the new connection once the Client VPN service has authenticated the device and user. The handler is implemented through an AWS Lambda function, and the terms Lambda and handler are used interchangeably in this blog.

As a refresher, Client VPN is a fully-managed elastic VPN service that scales the number of connections up and down according to demand. It allows you to provide easy connectivity to your workforce and your business partners, along with the ability to monitor and manage connections from one console. (Read Introducing AWS Client VPN to Securely Access AWS and On-Premises Resources to learn more).

The handler protects existing customer investments by taking advantage of the policies defined (and enforced) by Identity Providers and Mobile Device Management (MDM) software. The handler allows enterprise IT administrators to enforce access based on IP address, geolocation, and time (for example: deny access during a maintenance window, or allow access during certain hours). You can now enforce policy by using device, user, or connection attributes (Refer to Table-1 and Table-2 that follow.)

For enterprise customers who do not have an MDM deployment, the handler provides flexibility to define and implement additional security authorization policies. End-users in enterprise organizations might bring their own devices (BYOD). These devices might require additional security authorization checks and posture assessment (example: minimum version of Operating System, etc.), which helps enforce remediation actions. The handler can also be customized for gathering connection establishment auditing information for certain devices (or users).

In this blog post we cover three scenarios that use the client connect handler:

1. SAML 2.0 Authentication using 3rd Party Identity Providers
2. Active Directory Authentication including Multi-factor Authentication (MFA)
3. Certificate-based Mutual Authentication

Prerequisites

  • AWS Account
  • An Amazon VPC with an EC2 instance.
  • In the instance Security Group, allow ICMP traffic from the VPC CIDR range – this is needed for testing.
  • A private certificate imported into AWS Certificate Manager (ACM).
  • Generate and import a certificate to ACM
  • Active Directory or SAML Identity Provider hosting user and group information.
  • Mutual Authentication can also be enabled with AD or SAML. You have the option to use only Mutual Authentication in the AWS Client VPN Endpoint without AD or SAML.

Setting up the client connect handler Lambda function

You will write an AWS Lambda function that is invoked synchronously by the service (after user and device authentication) when a new VPN session connection is attempted by an end user. The Lambda function can be customized to enforce the security policies of the enterprise.

  1. The name for this Lambda function should be prefixed with ‘AWSClientVPN- ‘.
  2. Lambda function should exist in the same AWS account, and the same AWS region that the Client VPN endpoint is deployed.
  3. The following is a sample reference sample AWS Lambda function in Python that allows access only on weekdays:
import datetime

def is_weekday():
	week_index = datetime.datetime.today().weekday()
	if week_index < 5:
		return True
	return False

def lambda_handler(event, context):
	allow = False
	error_msg = "You are not allowed to connect on a weekend day."
	if is_weekday():
		allow = True
	return {
		"allow": allow,
		"error-msg-on-failed-posture-compliance": error_msg,
		"posture-compliance-statuses": [],
		"schema-version": "v1"
	}

(Additional examples of AWS Lambda functions are provided at the bottom of this post.)

The input to the Lambda function from the service uses JSON:

{
    "connection-id": <connection ID>,
    "endpoint-id": <client VPN endpoint ID>,
    "common-name": <cert-common-name>,
    "username": <user identifier>,
    "platform": <OS platform>,
    "platform-version": <OS version>,
    "public-ip": <public IP address>,
    "client-openvpn-version": <client OpenVPN version>,
    "schema-version": "v1"
}

The Lambda function should return the following JSON to the service:

{
    "allow": boolean,
    "error-msg-on-failed-posture-compliance": "",
    "posture-compliance-statuses": [],
    "schema-version": "v1"
}

For additional details refer to client connect handler documentation page.

4. Enable the client connect handler for your Client VPN endpoint and specify the Lambda function using the AWS CLI:

aws ec2 modify-client-vpn-endpoint
--client-vpn-endpoint-id $EID --region $REGION --client-connect-options Enabled=true,LambdaFunctionArn=arn:aws:lambda:us-east-1:243517296738:function:AWSClientVPN-Weekday

 

--client-connect-options (structure)
Shorthand syntax:
Enabled=boolean,LambdaArn=string
JSON syntax:
{
    "Enabled": true|false,
    "LambdaFunctionArn": "string"
}

5. Describe the endpoint to verify that the handler has been enabled on the endpoint using the AWS CLI:

aws ec2 describe-client-vpn-endpoints
[--client-vpn-endpoint-ids <value>]
[--filters <value>]
[--dry-run | --no-dry-run]
[--cli-input-json <value>]
[--starting-token <value>]
[--page-size <value>]
[--max-items <value>]
[--generate-cli-skeleton <value>]
"ClientVpnEndpoints": 
        {
            ...
            "ClientConnectOptions":
            {
                "Enabled": true,
                "LambdaFunctionArn": "arn:aws:lambda:ap-south-1:123456789012:function:AWSClientVPN-MyPostureCompliance",
                "Status": 
	            {
                    "code": "applied|applying"
                }                                                        
            },
            ...
        }

6. Establish a connection to the endpoint using the Desktop (Windows or macOS) AWS Client VPN software.

If both device and user authentication are successful and the configured Lambda function returns “allow”: True for this connection, the connection is allowed. If device and user authentication are successful and the configured Lambda function returns “allow”: False for this connection, the connection will, of course, be denied.

Scenario-1: SAML Authentication using 3rd Party Identity Providers

AWS Client VPN supports both certificate-based and SAML based authentication. When using both Mutual Authentication (based on certificates) and when combined with SAML, customers can now enforce device specific authorization policies prior to opening a VPN connection.

 

Steps for scenario 1:

Step 1: Refer to this blog post, Authenticate AWS Client VPN users with SAML, for details on how to configure SAML with Client VPN. (SAML based Identity providers (IdP) are vendors such as Okta, OneLogin and Duo.)
Step 2: End-user authenticates with the Identity provider.
Step 3: After successfully authenticating with the IdP, a SAML Token is returned.
Step 4: Endpoint invokes the Lambda function
Step 5: Handler enforces the authorization policies and return ‘True or False’
Step 6: the VPN Session is either allowed or denied.

[Note: Steps 4 through 6 are common across all scenarios.]

For this scenario, the username attribute is available on the input of the Lambda function. If mutual authentication is also enabled, then the common-name attribute (based on unique client certificate) will also be available.

Scenario-2: Active Directory based authentication including MFA

AWS Client VPN supports both certificate-based and Active Directory based authentication. Customers can define access control rules based on Active Directory groups and can use security groups to limit access of AWS Client VPN users. Using a single console, you can monitor and manage all of your Client VPN connections. Client VPN allows you to choose from OpenVPN-based clients, including client for Windows, macOS, iOS, Android, and Linux based devices. If you use device-specific certificates with the handler, an additional device authorization check can also be enforced. Client VPN already supports device authentication through certificates when mutual authentication is enabled.

 

Steps for scenario 2:

Step 1: Refer to this blog post, Using Microsoft Active Directory MFA with AWS Client VPN, on how to configure AD with Client VPN. Identity Providers like Duo provide MFA capabilities.
Step 2: End-user successfully authenticates with Active Directory.
Step 3: End-user successfully responds to Multi-Factor-Authentication (MFA).
Step 4: Endpoint invokes the Lambda function
Step 5: Handler enforces the authorization policies and return ‘True or False’
Step 6: the VPN Session is either allowed or denied.

For this scenario, the username attribute will be available on the input the Lambda function. If mutual authentication is also enabled, then the common-name attribute (based on unique client certificate) will also be available.

Scenario-3: Mutual Authentication using certificates

For customers that use device-specific certificates with the handler, an additional device authorization check can also be enforced. Client VPN already supports device authentication through certificates when mutual authentication is enabled.

 

Steps for scenario 3:

Step 1: Refer to online AWS Client VPN documentation for information on how to configure Mutual Authentication.
Step 2: End-user or device successfully verifies server certificate.
Step 3: End-user or device successfully presents client certificate and is verified.
Step 4: Endpoint invokes the Lambda function
Step 5: Handler enforces the authorization policies and return ‘True or False’
Step 6: the VPN Session is either allowed or denied.

For this scenario, the common-name attribute (based on unique client certificate) will be available.

Conclusion

In this blog post I have shown how a connect handler can be customized and used to enforce authorization policies for different authorization scenarios. Below are samples of additional AWS Lambda functions that can be customized to meet your needs.


Table-1 Attributes available to Client Connect Handler

User Attributes

Device Attributes

Connection Attributes

username

common-name (based on unique client certificate)

public-ip address

platform (Operating System) and platform-version

platform (Operating System) and platform-version

Connection
request timestamp (available in Lambda function)

Refer to this documentation page for complete list of attributes available.

Table-2 Attributes from 3rd Party Vendors (Identity Providers or Geolocation lookup Services)

The Lambda function can also be customized to invoke 3rd Party APIs or databases. For example based on the username, the Lambda function can be customized to query the subscribed User-Groups and apply authorization policies based on group membership.

User Attributes

Device Attributes

Connection Attributes

User Group(s): From Identity Provider based on username

Device
Group(s): From Identity Provider (or MDM) based on common-name.

Geolocation
metadata(Country, State, lat/long, zip, zip+4, etc).  Based on Public IP Address Or from Identity Provider to query address based on username.

Client Connect Handler Examples

1. Example-1: Allows access based on pre-defined pool of IP Addresses:

import ipaddress

def is_public_source_ip_valid(public_ip):
    if ipaddress.ip_address(public_ip) in ipaddress.ip_network('72.21.0.0/16'):
        return True
    return False

def lambda_handler(event, context):
    public_ip = event['public-ip']
    allow = False
    error_msg = "Your IP is not allowed to establish a connection. Please contact your administrator."
    if is_public_source_ip_valid(public_ip):
        allow = True
    return {
        "allow": allow,
        "error-msg-on-failed-posture-compliance": error_msg,
        "posture-compliance-statuses": [],
        "schema-version": "v1"        
    }

2. Example-2: Posture Assessment Integration with VMware Airwatch Workspace One UEM:

from botocore.vendored import requests

print('Loading function')

def get_posture_compliance_status(device_id):
    # Airwatch console/API URL
    consoleURL = 'https://xxxxxx.awmdm.com'
    # base64 encoded username:password
    b64EncodedAuth = ''
    # API key provided by Airwatch
    tenantCode = ''
    try:
        response = requests.get(consoleURL + f"/api/mdm/devices?searchby=Udid&id={device_id}", headers={"Authorization": "Basic " + b64EncodedAuth, "aw-tenant-code": tenantCode,"Accept": "application/json"}, timeout=30)
        response.raise_for_status()
        device_details = response.json()
        return device_details['ComplianceStatus']
    except Exception as e:
        print(e)
        return 'Quarantined'

def lambda_handler(event, context):
    device_id = event['common-name']
    posture_compliance_status = get_posture_compliance_status(device_id)
    allow = False
    error_msg = "Device failed posture compliance. Please contact your administrator."
    if posture_compliance_status == 'Compliant':
        allow = True
    return {
        "allow": allow,
        "error-msg-on-failed-posture-compliance": error_msg,
        "posture-compliance-statuses": [posture_compliance_status],
        "schema-version": "v1"        
    }

3. Example-3: Allows access based on approved user-group:

def is_groups_valid(user_groups):
    valid_groups_set = set(['group1', 'group2'])
    user_groups_set = set(user_groups)
    intersection = user_groups_set.intersection(valid_groups_set)
    if len(intersection) > 0:
        return True
    return False

def lambda_handler(event, context):
    user_groups = event['groups']
    allow = False
    error_msg = "You belong to a group that is not allowed to establish a connection. Please contact your administrator."
    if is_groups_valid(user_groups):
        allow = True
    return {
        "allow": allow,
        "error-msg-on-denied-connection": error_msg,
        "posture-compliance-statuses": [],
        "schema-version": "v2"        
    }