In general, using CACs (or smartcards, PIV cards, etc.) as the authentication mechanism is pretty straightforward. The certificate is used for authentication, and, if desired, authorization can then be performed using a value in the certificate. The ASA essentially pulls a username from a field to use for a lookup against a backend server, e.g. LDAP or RADIUS.
I’m not going to cover the configuration and setup of the initial VPN group policies, tunnel groups, etc. This is simply going to review authorizing clients based on the username in the certificate using a couple different methods to grab the username. The backend authentication server configuration will not be covered. In this example, LDAP is being used directly as backend source.
Selecting the Username from a Certificate Field
In most cases, the ASA can extract the username from the certificate with ease. The following cases will require a straightforward configuration:
The value of the field is identical to the value in the backend server
This is the most common solution, as the User Principal Name (UPN) generally provides the same value as the UPN assigned value in the backend server when the CAC certificate is issued.
The needed value is easily identifiable in the certificate field using pattern matching, e.g. the only 10 digits in a field, or the 10th through the 15th characters.
These scenarios account for the most common, and simplest, configuration scenarios.
How to Configure
These simple scenarios can be configured right from the ASDM GUI. This can not actually be configured from the CLI. On ASDM, navigate to Remote Access VPN > AnyConnect Connection Profile > [vpn-profile] > Advanced > Authentication > “Use script to select username” > Add. In this prompt, you can select which field to use, as well as some filtering options, if applicable.
Under the Hood
So, under the hood, when selecting which value to select for the username, the ASA is actually creating a script for you. This is stored on the disk0:/ partition in a file called username_from_cert.xml. The scripting is done using a language called Lua; however, the ASA uses some local functions that aren’t exposed to the administrator to perform the specified actions in the ASDM. These can be seen by examining the created script when specifying different parameters.
<TunnelGroupData> <TableEntry> <TunnelGroup>ipsecvpn</TunnelGroup> <Function>CAC_UPN</Function> </TableEntry> <Function> <ScriptName>CAC_UPN</ScriptName> <Custom>false</Custom> <Content>return CSCO_CertificateToUser(cert.subjectaltname.upn)</Content> </Function> </TunnelGroupData>
ciscoasa# more disk0:/username_from_cert.xml <TunnelGroupData> <TableEntry> <TunnelGroup>ipsecvpn</TunnelGroup> <Function>CAC_UPN</Function> </TableEntry> <Function> <ScriptName>CAC_UPN</ScriptName> <Custom>false</Custom> <Content>return CSCO_CertificateToUser(cert.subjectaltname.upn,1,10)</Content> </Function> </TunnelGroupData>
Less Common Scenarios
In some less common scenarios, the use of the script can become challenging. The only scenario I came across requiring a custom script is outlined below.
Appending a Suffix to a Field’s String
The use case for this was a customer who’s CACs had a null value for the UPN field; however, needed the EDIPI value that is normally stored there to perform the LDAP lookup. To make things more complicated, the UPN field should have been in a format similar to email@example.com. Since the UPN was blank, we ended up grabbing the EDIPI from the common name (CN) value which was mixed up with the user’s name full name and lacked the suffix we needed. So, we matched the only continuous string of digits and then appended our domain on the end of it.
The LUA scripting language actually uses its own implementation of pattern matching as opposed to run of the mill regular expressions. This created quite the challenge. In addition, scripts that were working successfully on my machine would throw errors when attempting to use them on the ASA with the built in variable referencing the cert field (cert.subject.cn in this case). In the end, we ended up using the pre-existing Cisco function to match the string of digits and appending (“..”) a string to the end of it.
return (CSCO_CertificateToUser(cert.subject.cn, "%d+")).."@test.somewolfe.com"
Debugging Custom Scripts
When using custom scripts, often times error are thrown that are related to the script not successfully running. This doesn’t require any level of debugging, but generates a warning (4) syslog message as seen below:
%ASA-7-717025: Validating certificate chain containing 1 certificate(s). %ASA-7-717029: Identified client certificate within certificate chain. serial number: 4E819769406A4BEE8D9825C9E1052208, subject name: c=US,st=MD,l=Fort Meade,o=Military,ou=NEC,cn=RYAN.WOLFE.9101161615. %ASA-7-717030: Found a suitable trustpoint ISE_VPN_TrustPoint7 to validate certificate. %ASA-6-717022: Certificate was successfully validated. serial number: 4E819769406A4BEE8D9825C9E1052208, subject name: c=US,st=MD,l=Fort Meade,o=Military,ou=NEC,cn=RYAN.WOLFE.9101161615. %ASA-6-717028: Certificate chain was successfully validated with warning, revocation status was not checked. %ASA-6-725002: Device completed SSL handshake with client inside:10.202.190.133/50536 %ASA-7-717036: Looking for a tunnel group match based on certificate maps for peer certificate with serial number: 4E819769406A4BEE8D9825C9E1052208, subject name: c=US,st=MD,l=Fort Meade,o=Military,ou=NEC,cn=RYAN.WOLFE.9101161615, issuer_name: cn=Certificate Services Endpoint Sub CA - ise20. %ASA-4-717037: Tunnel group search using certificate maps failed for peer certificate: serial number: 4E819769406A4BEE8D9825C9E1052208, subject name: c=US,st=MD,l=Fort Meade,o=Military,ou=NEC,cn=RYAN.WOLFE.9101161615, issuer_name: cn=Certificate Services Endpoint Sub CA - ise20. %ASA-7-113028: Extraction of username from VPN client certificate has been requested. [Request 7] %ASA-7-113028: Extraction of username from VPN client certificate has started. [Request 7] %ASA-4-113026: Error <[string "user_from_cert_xlate_lua"]:264: No function found> while executing Lua script for group <ipsecvpn> %ASA-2-113027: Error activating tunnel-group scripts %ASA-7-113028: Extraction of username from VPN client certificate has finished with error. [Request 7] %ASA-7-113028: Extraction of username from VPN client certificate has completed. [Request 7] %ASA-7-609001: Built local-host management:10.202.192.10 %ASA-6-302013: Built outbound TCP connection 17135 for management:10.202.192.10/389 (10.202.192.10/389) to identity:10.202.192.3/9892 (10.202.192.3/9892)  Session Start %ASA-6-113005: AAA user authorization Rejected : reason = Unspecified : server = 10.202.192.10 : user = <Unknown>
Following that, if the script runs successfully and does get a value, you can debug the LDAP process, or whatever authentication process that is in use, to see what value is being used for the lookup. For LDAP, debugging is enabled using “debug ldap 254”. You can see below that the string being used to filter is displayed:
 Performing Simple authentication for Administrator to 10.202.192.10  LDAP Search: Base DN = [DC=test,DC=somewolfe,DC=com] Filter = [userPrincipalNamefirstname.lastname@example.org] Scope = [SUBTREE]  User DN = [CN=Ryan Wolfe,OU=Test Users,DC=test,DC=somewolfe,DC=com]
If the LDAP query is failing with with that string, you may need to validate that the value in Active Directory matches what you are looking for. After a successful attempt, you should see LDAP return a slew of attributes about the user you looked up.
 Talking to Active Directory server 10.202.192.10 %ASA-6-302014: Teardown TCP connection 17292 for inside:10.202.190.133/50796 to identity:10.202.191.20/443 duration 0:00:00 bytes 10719 TCP FINs %ASA-7-609002: Teardown local-host inside:10.202.190.133 duration 0:00:00 %ASA-7-609002: Teardown local-host identity:10.202.191.20 duration 0:00:00  Reading password policy for email@example.com, dn:CN=Ryan Wolfe,OU=Test Users,DC=test,DC=somewolfe,DC=com  Read bad password count 0  LDAP Search: Base DN = [DC=test,DC=somewolfe,DC=com] Filter = [userPrincipalNamefirstname.lastname@example.org] Scope = [SUBTREE]  Retrieved User Attributes:  objectClass: value = top  objectClass: value = person  objectClass: value = organizationalPerson  objectClass: value = user  cn: value = Ryan Wolfe  sn: value = Wolfe  givenName: value = Ryan  distinguishedName: value = CN=Ryan Wolfe,OU=Test Users,DC=test,DC=somewolfe,DC=com  instanceType: value = 4  whenCreated: value = 20151116202638.0Z  whenChanged: value = 20160226023444.0Z  displayName: value = Ryan Wolfe  uSNCreated: value = 210819  memberOf: value = CN=ISE Admins,OU=Test Groups,DC=test,DC=somewolfe,DC=com  memberOf: value = CN=Domain Admins,CN=Users,DC=test,DC=somewolfe,DC=com  uSNChanged: value = 244462  name: value = Ryan Wolfe  objectGUID: value = .z&d.QjF.`....ny  userAccountControl: value = 66048  badPwdCount: value = 0  codePage: value = 0  countryCode: value = 0  badPasswordTime: value = 0  lastLogoff: value = 0  lastLogon: value = 0  pwdLastSet: value = 131009121452888556  primaryGroupID: value = 513  objectSid: value = ..............M.6.F.R4.Jh...  adminCount: value = 1  accountExpires: value = 9223372036854775807  logonCount: value = 0  sAMAccountName: value = rwolfe  sAMAccountType: value = 805306368  userPrincipalName: value = email@example.com  objectCategory: value = CN=Person,CN=Schema,CN=Configuration,DC=test,DC=somewolfe,DC=com  dSCorePropagationData: value = 20160226021603.0Z  dSCorePropagationData: value = 16010101000000.0Z  lastLogonTimestamp: value = 131009098327345443  Fiber exit Tx=626 bytes Rx=4547 bytes, status=1  Session End
This should be followed, of course, by a successful VPN connection.