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
ALGORITHM
issha1
,sha256
orsha512
(lowercase);SECRET
is the user's TOTP secret (a Base32-encoded string with length an integer multiple of 8 characters, see below);DIGITS
is the number of digits expected from the user (normally6
or8
);PERIOD
is the frequency at which the code changes (normally30
or60
seconds).
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 =90
seconds) 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
TOTP
to 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 Key
and IDget_totp_key
; add a Branch Rule namedKey Found
with the Expressionmcget -secure {session.custom.totp.key}
.The
fallback
branch from theGet TOTP Key
block will be followed by any user for whom TOTP metadata cannot be found; associate with either theAllow
orDeny
terminal dependent on local policy. -
On the
Key Found
branch add a Logon ➡︎ Logon Page block namedTOTP Code Page
. Field 1 must be configured with Type =Text
, Post Variable Name =totp_code
and 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
fallback
branch of theTOTP Code Page
block, add another General Purpose ➡︎ iRule Event block with NameCheck TOTP Code
and IDcheck_totp_code
. -
On the
fallback
branch of theCheck TOTP Code
block, 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 namedSuccess
with the Expressionexpr {[mcget {session.custom.totp.result}] eq "success"}
.The
Success
branch from theCheck TOTP Code
block will be followed only when the user supplied code matches that calculated from their metadata, and should normally be associated with theAllow
terminal.fallback
should be associated with theDeny
terminal. -
Plumb the
TOTP
macro 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 … }
}