This APM iRule implements the RFC 6238 TOTP: Time-Based One-Time Password Algorithm for use with F5 APM access policies; for example, with an F5 SSL-VPN implementation.
- Configurable storage backend (LDAP, Active Directory or Data Group List)
- Per-user TOTP algorithm, digits and period
- Configurable clock-drift tolerance and rate-limiting
- Differentiation between users with and without TOTP metadata
Create a new Local Traffic ➡︎ iRules ➡︎ iRule List iRule named totp_apm, containing the entire totp_apm.tcl TCL script, to be customised for your chosen backend, as below.
The backend used for storage of user metadata is selected by modifying the value of the totp_key_storage in the totp_apm iRule (totp_apm.tcl).
All backends expect the metadata to be in the following format:
ALGORITHM SECRET DIGITS PERIOD
where
ALGORITHMissha1,sha256orsha512(lowercase);SECRETis the user's TOTP secret (a Base32-encoded string with length an integer multiple of 8 characters, see below);DIGITSis the number of digits expected from the user (normally6or8);PERIODis the frequency at which the code changes (normally30or60seconds).
For both ldap and ad backends, the metadata is expected to be accessible in an attribute of the user object, the attribute name configurable via totp_key_ldap_attr or totp_key_ad_attr variables, respectively, and both defaulting to totp_auth_key.
The Data Group List backend requires a String-type datagroup, configurable via the totp_key_dg variable (default: totp_auth_keys), with the key being the user's username and the value being the users' TOTP metadata.
User secrets take the form of a Base32-encoded random value with length a multiple of 8 characters. They can be easily generated using pyotp:
pip3 install pyotp
python3 -c "import pyotp; print(pyotp.random_base32(32))
These are normally communicated in the form of a QR Code encoding of an otpauth:// string unique to the user:
otpauth://totp/USERNAME@DOMAIN?secret=SECRET&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD&issuer=DOMAIN
Note: ALGORITHM is all uppercase in the otpauth:// string.
otpauth://totp/user@example.com?secret=FYNLSPTBTQPZJYEMB3QOZLDW34ZWX7TD&algorithm=SHA256&digits=8&period=30&issuer=example.com
- Client clock-drift tolerance (the width of the window of codes that will be accepted) is configured via
totp_tolerance(default =1, ) - TOTP rate-limiting lockout are configured via
totp_lockout_period(default =90seconds) andtotp_lockout_rate(default =3)
Note: already used codes and lockout state are stored with the totp_used_codes_table and totp_lockout_state_table tables.
The associated access policy consists of three main blocks:
- retrieve the user's TOTP metadata from the configured backend via an iRule Event (
get_totp_key), - prompt for the user's code via a Logon Page,
- verify the supplied code via another iRule Event (
check_totp_code).
The two iRule Event agents can also be integrated into alternate login workflows, for example prompting all users for a TOTP code alongside their main credentials.
-
Add a new Macro named
TOTPto the relevant APM Access Policy through the Visual Policy Editor (VPE). -
Within the TOTP Macro, add a General Purpose ➡︎ iRule Event block with Name
Get TOTP Keyand IDget_totp_key; add a Branch Rule namedKey Foundwith the Expressionmcget -secure {session.custom.totp.key}.The
fallbackbranch from theGet TOTP Keyblock will be followed by any user for whom TOTP metadata cannot be found; associate with either theAlloworDenyterminal dependent on local policy. -
On the
Key Foundbranch add a Logon ➡︎ Logon Page block namedTOTP Code Page. Field 1 must be configured with Type =Text, Post Variable Name =totp_codeand Session Variable Name =totp_code(customisable viatotp_code_form_field). All other fields should have Type =None. Customise Form Header Text, Logon Page Input Field #1 and Logon Button text as appropriate. -
On the
fallbackbranch of theTOTP Code Pageblock, add another General Purpose ➡︎ iRule Event block with NameCheck TOTP Codeand IDcheck_totp_code. -
On the
fallbackbranch of theCheck TOTP Codeblock, add a General Purpose ➡︎ Logging block with nameLog TOTP Result. Set an appropriate Log Message (e.g. "TOTP Verification Complete") and a Custom entry with valuesession.custom.totp.result; add a Branch Rule namedSuccesswith the Expressionexpr {[mcget {session.custom.totp.result}] eq "success"}.The
Successbranch from theCheck TOTP Codeblock will be followed only when the user supplied code matches that calculated from their metadata, and should normally be associated with theAllowterminal.fallbackshould be associated with theDenyterminal. -
Plumb the
TOTPmacro in immediately after the primary authentication block or macro (tested with AD Auth and SAML Auth).
Via tmsh, assuming configuration within the VPN partition for an access policy named vpn, configuration will resemble the following:
apm policy agent irule-event totp_act_irule_event_ag {
id get_totp_key
partition VPN
}
apm policy agent irule-event totp_act_irule_event_1_ag {
id check_totp_code
partition VPN
}
apm policy agent logon-page totp_act_logon_page_ag {
customization-group totp_act_logon_page_ag
field-type2 none
partition VPN
post-var-name1 totp_code
sess-var-name1 totp_code
}
apm policy policy-item totp_act_irule_event {
agents {
totp_act_irule_event_ag {
type irule-event
}
}
caption "Get TOTP Key"
color 1
item-type action
partition VPN
rules {
{
caption "Key Found"
expression "mcget -secure {session.custom.totp.key}"
next-item totp_act_logon_page
}
{
caption fallback
next-item totp_ter_out
}
}
}
apm policy policy-item totp_act_irule_event_1 {
agents {
totp_act_irule_event_1_ag {
type irule-event
}
}
caption "Check TOTP Code"
color 1
item-type action
partition VPN
rules {
{
caption fallback
next-item totp_act_logging
}
}
}
apm policy policy-item totp_act_logging {
agents {
totp_act_logging_ag {
type logging
}
}
caption "Log TOTP Result"
color 1
item-type action
partition VPN
rules {
{
caption Success
expression "expr {[mcget {session.custom.totp.result}] eq \"success\"}"
next-item totp_ter_out
}
{
caption fallback
next-item totp_ter_totp_verification_failed
}
}
}
apm policy policy-item totp_act_logon_page {
agents {
totp_act_logon_page_ag {
type logon-page
}
}
caption "TOTP Code Page"
color 1
item-type action
partition VPN
rules {
{
caption fallback
next-item totp_act_irule_event_1
}
}
}
apm policy policy-item totp_ent_in {
caption In
color 1
partition VPN
rules {
{
caption fallback
next-item totp_act_irule_event
}
}
}
apm policy policy-item totp_ter_out {
caption Allow
color 1
item-type terminal-out
partition VPN
}
apm policy policy-item totp_ter_totp_verification_failed {
caption Deny
color 2
item-type terminal-out
partition VPN
}
apm policy access-policy totp {
caption TOTP
default-ending totp_ter_out
items {
totp_act_irule_event { }
totp_act_irule_event_1 { }
totp_act_logging { }
totp_act_logon_page { }
totp_ent_in { }
totp_ter_out {
priority 8
}
totp_ter_totp_verification_failed {
priority 9
}
}
partition VPN
start-item totp_ent_in
type macro
}
This is then referenced as a macro within the vpn access policy:
apm policy access-policy vpn {
…
macros { … /VPN/totp … }
}
