Setting up a secure VPN is easier than you might think. With F5 APM and Google authenticator you’re up and running soon.
There is an article on devcentral doing this but I thought it could be a bit simpler so I wrote my own. Tested on version 12 but should be more or less applicable to version 11 as well. Please let me know if there’s any differences and I’ll update the article.
Table of Contents
Configure a functioning VPN base
- Make sure that you have NTP enabled on both your APM device and your client. If the time is off by even a few seconds it might mean the difference between success and failure.
- Start with opening up the Device Wizards
- Choose the Network Access Setup Wizard for Remote Access and click Next
- Choose a policy name and click next.
- Then select “No authentication” (we will set that up later) and Next.
- Configure lease pool. This is a the addresses that your clients will get when connecting to the VPN.
Enter the IP range you wish to use, and click on Add, then NextExample:
- Click next again to configure the network access.Force all traffic through tunnel
Means that all traffic will go through the tunnelUse split tunnel for traffic
Only the traffic you want to go through the tunnel will go through the tunnel.I would advice using split tunnel and this guide will continue based on that. - Choosing split tunnel will add an additional part of the form where you will choose which traffic should go through the tunnel.
You will have to enter all your internal networks here (or the ones you want VPN clients to access)Also add your local domain if you have one, then click Next.Example: - Enter the IP of your dns server, click Next
- Enter the IP of the virtual server that will serve the VPN clients, click Next
- Review your choices, click Next, then finished.
Add local authentication
Remember when we chose “No authentication in the previous step? This is where we make sure that users needs to authenticate.
- Go to Access Policy, Local User DB, Manage Instances
- Click on Create New Instance
- Choose a name, click OK
- Then go to Access Policy, Local User DB, Manage users
- Click on Create user, enter username, password and select the instance you just created.
- Now go to Access Policy, Access Profiles
- Click on Edit in the Access Policy column
- Now the visual policy editor should launch. Click on the plus sign between “Logon page” and “Resource assign”.
- Select the Authentication tab and then LocalDB Auth. Then click “Add Item”
- Select your LocalDB Instance and click on “Save”
- Your visual policy editor should look like this now:
- Click on Apply Access Policy
Now you have a fully functioning VPN service (provided that you open up the firewall, of course) and technically you could stop here. But let’s add the two factor authentication to make it more secure!
Adding Google authenticator to an access policy
This part is a bit trickier, but you’ll make it.
Create the necessary iRules
These rules are shamelessly stolen from the original article:
https://devcentral.f5.com/articles/two-factor-authentication-with-google-authenticator-and-apm
To create rules, go to Local Traffic, iRules and the click on Create. Copy the code from below, and paste it into the iRule Window.
It’s important that you give the rule ga_code_verify the exact same name as stated here, otherwise it won’t work later.
ga_code_verify
This rule is used to calculate and verify the Google authenticator token
when ACCESS_POLICY_AGENT_EVENT { if { [ACCESS::policy agent_id] eq "ga_code_verify" } { ### Google Authenticator verification settings ### # lock the user out after x attempts for a period of x seconds set static::lockout_attempts 3 set static::lockout_period 30 # logon page session variable name for code attempt form field set static::ga_code_form_field "ga_code_attempt" # key (shared secret) storage method: ldap, ad, or datagroup set static::ga_key_storage "datagroup" # LDAP attribute for key if storing in LDAP (optional) set static::ga_key_ldap_attr "google_auth_key" # Active Directory attribute for key if storing in AD (optional) set static::ga_key_ad_attr "google_auth_key" # datagroup name if storing key in a datagroup (optional) set static::ga_key_dg "google_auth_keys" ##################################### ### DO NOT MODIFY BELOW THIS LINE ### ##################################### # set lockout table set static::lockout_state_table "[virtual name]_lockout_status" # set variables from APM logon page set username [ACCESS::session data get session.logon.last.username] set ga_code_attempt [ACCESS::session data get session.logon.last.$static::ga_code_form_field] # retrieve key from specified storage set ga_key "" switch $static::ga_key_storage { ldap { set ga_key [ACCESS::session data get session.ldap.last.attr.$static::ga_key_ldap_attr] } ad { set ga_key [ACCESS::session data get session.ad.last.attr.$static::ga_key_ad_attr] } datagroup { set ga_key [class lookup $username $static::ga_key_dg] } } # increment the number of login attempts for the user set prev_attempts [table incr -notouch -subtable $static::lockout_state_table $username] table timeout -subtable $static::lockout_state_table $username $static::lockout_period # verification result value: # 0 = successful # 1 = failed # 2 = no key found # 3 = invalid key length # 4 = user locked out # make sure that the user isn't locked out before calculating GA code if { $prev_attempts <= $static::lockout_attempts } { # check that a valid key was retrieved, then proceed if { [string length $ga_key] == 16 } { # begin - Base32 decode to binary # Base32 alphabet (see RFC 4648) array set static::b32_alphabet { A 0 B 1 C 2 D 3 E 4 F 5 G 6 H 7 I 8 J 9 K 10 L 11 M 12 N 13 O 14 P 15 Q 16 R 17 S 18 T 19 U 20 V 21 W 22 X 23 Y 24 Z 25 2 26 3 27 4 28 5 29 6 30 7 31 } set ga_key [string toupper $ga_key] set l [string length $ga_key] set n 0 set j 0 set ga_key_bin "" for { set i 0 } { $i < $l } { incr i } { set n [expr $n << 5] set n [expr $n + $static::b32_alphabet([string index $ga_key $i])] set j [incr j 5] if { $j >= 8 } { set j [incr j -8] append ga_key_bin [format %c [expr ($n & (0xFF << $j)) >> $j]] } } # end - Base32 decode to binary # begin - HMAC-SHA1 calculation of Google Auth token set time [binary format W* [expr [clock seconds] / 30]] set ipad "" set opad "" for { set j 0 } { $j < [string length $ga_key_bin] } { incr j } { binary scan $ga_key_bin @${j}H2 k set o [expr 0x$k ^ 0x5C] set i [expr 0x$k ^ 0x36] append ipad [format %c $i] append opad [format %c $o] } while { $j < 64 } { append ipad 6 append opad \\ incr j } binary scan [sha1 $opad[sha1 ${ipad}${time}]] H* token # end - HMAC-SHA1 calculation of Google Auth hex token # begin - extract code from Google Auth hex token set offset [expr ([scan [string index $token end] %x] & 0x0F) << 1] set ga_code [expr (0x[string range $token $offset [expr $offset + 7]] & 0x7FFFFFFF) % 1000000] set ga_code [format %06d $ga_code] # end - extract code from Google Auth hex token if { $ga_code_attempt eq $ga_code } { # code verification successful set ga_result 0 } else { # code verification failed set ga_result 1 } } elseif { [string length $ga_key] > 0 } { # invalid key length, greater than 0, but not length not equal to 16 chars set ga_result 3 } else { # could not retrieve user's key set ga_result 2 } } else { # user locked out due to too many failed attempts set ga_result 4 } # set code verification result in session variable ACCESS::session data set session.custom.ga_result $ga_result } }
generate_ga_code
This rule is used to generate the tokens for google authenticator.
when HTTP_REQUEST { set account [URI::query [HTTP::uri] "account"] set domain [URI::query [HTTP::uri] "domain"] set secret [URI::query [HTTP::uri] "secret"] set qr_code [URI::query [HTTP::uri] "qr_code"] if { ([HTTP::path] starts_with "/ga_secret_generator") && ($account ne "") && ($domain ne "") } { if { [string length $secret] <= 10 } { set secret [b64encode [md5 [expr rand()]]] } set secret [string range $secret 0 9] array set b32_alphabet_inv { 0 A 1 B 2 C 3 D 4 E 5 F 6 G 7 H 8 I 9 J 10 K 11 L 12 M 13 N 14 O 15 P 16 Q 17 R 18 S 19 T 20 U 21 V 22 W 23 X 24 Y 25 Z 26 2 27 3 28 4 29 5 30 6 31 7 } set secret_b32 "" set l [string length $secret] set n 0 set j 0 # encode loop is outlined in RFC 4648 (http://tools.ietf.org/html/rfc4648#page-8) for { set i 0 } { $i < $l } { incr i } { set n [expr $n << 8] set n [expr $n + [scan [string index $secret $i] %c]] set j [incr j 8] while { $j >= 5 } { set j [incr j -5] append secret_b32 $b32_alphabet_inv([expr ($n & (0x1F << $j)) >> $j]) } } # pad final input group with zeros to form an integral number of 5-bit groups, then encode if { $j > 0 } { append secret_b32 $b32_alphabet_inv([expr $n << (5 - $j) & 0x1F]) } # if the final quantum is not an integral multiple of 40, append "=" padding set pad [expr 8 - [string length $secret_b32] % 8] if { ($pad > 0) && ($pad < 8) } { append secret_b32 [string repeat = $pad] } set ga_qr_code_link "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/" append ga_qr_code_link "$account@$domain" append ga_qr_code_link "%3Fsecret%3D" append ga_qr_code_link $secret_b32 set ga_secret_http_resp {<html> <body> <div align="center"> } if { $qr_code eq "yes" } { append ga_secret_http_resp " <img src=\"$ga_qr_code_link\" />\n" } append ga_secret_http_resp " <p>account: $account@$domain" append ga_secret_http_resp "key (secret): $secret_b32</p>\n </div>\n </body>\n</html>" HTTP::respond 200 content $ga_secret_http_resp } else { HTTP::respond 200 content {<html> <body> <h2><a href="http://goo.gl/edmb2">Google Authenticator</a> key (shared secret) generator</h2> <form action="/ga_secret_generator" method="GET"> <table cellspacing="1" cellpadding="1" border="0"> <tr> <th align="left">account:</th> <td><input name="account" type="text" size="10"> @ <input name="domain" type="text" size="20"></td> </tr> <tr> <th align="left">secret:</th> <td><input name="secret" type="text" size="10"> *optional 10 character key (additional chars truncated), random secret used if blank</td> </tr> <tr> <th align="left">generate QR code?</th> <td><input name="qr_code" type="checkbox" value="yes"> *a request will be made to Google to generate QR code</td> </tr> </table> <input type="submit" value="Submit"> </form> </body> </html>} } }
Generate Google authenticator tokens
Create the VIP used to generate Google authenticator tokens
- Go to Local Traffic, Virtual servers
- Click on Create
- Give the virtual server a name, ie generate_ga_token
- Give the virtual server an IP on your local subnet
- Assign an HTTP profile
- Assign an SSL profile (if you want to use SSL)
- Assign generate_ga_code irule to the virtual server
- Click on Finished
Generate a token
- Surf to the address of the VIP, ie https://generategacode.mydomain.local, or http://192.168.1.50 (if that’s your VIP ip)
- You should then see this page:
- Enter the username of your user and a name for the token after the “@” sign. We need the name to label the token in the google authenticator app.
- Enter a secret if you like, or let the load balancer generate it for you. You will not have to remember this secret later.
- Check generate QR code and click on “Submit”
- Open up your Google Authenticator app and touch the “plus sign”, select scan barcode and scan the QR code.
- Save the secret, we will need it soon.
Save the key in a data group list
- Go to Local Traffic, iRules, Data Group Lists
- Click on Create
- Give the data group list the name google_auth_keys and add your user name and the secret generated earlier
- Click Finished
Update the Virtual server with the verification iRule
- Go to Local Traffic, Virtual servers
- Find the the Virtual server created during the Wizard (the one the does not have redirect in it’s name) and click on it:
- In the following page, choose Resources and click on manage in the iRules section
- Find the ga_code_verify rule in the right list and click on the arrows pointing left. The rule should now be moved to the left side, to the enabled select list.
Update the Access Policy
So now we have a google authentication token in our phone, the irules has been created and assigned. Now we just need to tie it all together in the access policy.
- Now go to Access Policy, Access Profiles
- Click on Edit in the Access Policy column
- In the Visual policy editor, click the plus sign between LocalDB Auth and Resource Assign
- In the Logon tab, choose Logon Page and then Add Item
- Change the text as marked in this picture:
- Click Save
- Then click on Add New Macro
- Name it and click Save:
- Now click on Edit Terminals in the Macro settings
- Click on Add Terminal
- Name the terminal “Failure”
- Rename the terminal called “Out” to successful
- Click on the Set default tab and set the default to Failure.
- Click on save
- Edit the new macro by clicking on the plus sign in the macro settings
- Go to the General Purpose Tab, click on iRule event and then Add Item
- Name: Google Auth verification
ID: ga_code_verify
- Then click on Branch rules, Add Brand Rule
Name and change the expression according to the following image (make sure they’re in the same order)For lazy people that don’t want to type (replace the X with the number):
expr { [mcget {session.custom.ga_result}] == X }
- Then click Save
- Click on the terminals and set Successful to Successful and the rest to Failure
- Now we’re going to insert the Macro in the main policy. Click on the plus sign between Get GA Token and resource assign
- Click on the Macro tab and select your Verify Google Token macro. click “Add Item”
- Now click on Apply Access Policy
Your final policy should look like this:
And now we’re done!
Some notes
- If you want to use mobile phones you might run into trouble with the default anti-virus policy.
- You will want to have a legitimate certificate on the Virtual server serving your VPN. The default setting is a self signed certificate.
Great article
Great Article, Thanks Patrik,
since long time I have been trying to setup the same but no luck, But this time you made it. 🙂
Glad to hear that it helped! 🙂
Patrik…Google auth is free or paid product? Thank you for your time in advance.
Hi
It’s free!
/Patrik
Many thanks. it is a great article. i have tested but unfortunately i faced one issue .
I got username and password page then I entered my username and password then authentication success after logging in I got prompt for the one time password so I got it from my google authenticator on my phone and entered it in the prompt after that it give my the login page again didn’t to enter the username and password again.
So I have done troubleshoot and found that after it fails when policy try to verify the authenticate code. I don’t t if you can help my in this issue
Thank you Malek. Sorry to hear it’s not working out for you. Check out the comment from Ben?
Worked perfectly. The only thing I had to change was the username in the datagroup – I had to add user.name@domain.com
Thanks Ben! Due to renovations my environment has been down for some time so I can’t go through the guide myself. Did you feel that this part was lacking in the guide?
Hi Patrik, I can’t seem to get the iRules to load up. I am getting this on the first iRule.
01070151:3: Rule [/Common/ga_code_verify] error: /Common/ga_code_verify:2: error: [undefined procedure:  ][ ]
Any ideas? I am running APM 12.1.2
Hey Jack. Do you get it when trying to save the rule?
Yes, when I try to save it on the APM as an iRule. I just can’t figure out what procedure is undefined.
Yes, when I try to save it on the APM as an iRule. I just can’t figure out what procedure is undefined.
Sounds like you might have missed the top row when copying the rule. Does it start with when HTTP_REQUEST?
/Patrik
Even thought both iRule fail to load for me, I am working on the ga_code_verify one right now. This one starts with
when ACCESS_POLICY_AGENT_EVENT {
is there another line that I can’t see in the above code window?
My bad. Wrong iRule. Both iRules fail when you try to load them? I tried both just now and they work for me with version 12.1.2.
Verify that you got the 100% of the rules copied?
So I was able to get them to load this morning. I guess I was not clipping the whole iRule. Only thing that makes sense. Thanks for that. I was able to move forward with the configurations. I was able to get this partially working but having troubles with using a VPN client like the Edge Client or the Windows F5 Access. Are you able to get this Google piece to integrate with those clients?
Sorry Jack. I could have sworn that I answered this. When I was using the edge client I had to use the “web mode” when signing in as the native app did not support custom fields when logging in.
Hi Patrik
Many thanks. it is a super article. I pray for you every day :)))
I have some trouble with Verify Google Token Macros.
GA Code work normal but when i apply Verify Google token macros. Auth. with stop and redirect F5 logout screen.
Please help with solution.
P.S: I follow all steps as you advise. and not change anything in iRules
Hi Patrik
I resolved problem with Verify Google token. But now when i insert GA code i have 10 second for insert code. after 10 second if i insert same code i get GA faild and return F5 log out screen
10 seconds sounds a bit short. Have you configured NTP on your device?
Hi Patrik,
We need to store user key in active directory and have around 500 users. it is not possible to add user key in F5 data group .How can I achieve this .
Hi Midhun
I understand your dilemma here and I considered this too when writing the article.
Asked some colleagues in the business for help but it’d seem like there are no existing tools that would take care of this. It could certainly be scripted with ie. powershell though.
/Patrik
Hi Patrik,
I am using this for a web based service not for VPN. the solution seems to be working fine. However I noticed that its not working properly interms of 2FA:
1. If I remove the user binding in the data-group, the user is still able to login based on authentic AD credentials. Although the VPE is calling the Google Authentication Macro.
2. If i type in the wrong Google Authenticator code it still takes me ahead and I have access to the resource.
Am i missing something ? I’ve double checked the irules and there is not different in the ones i have and the ones listed on your post.
Could you please post a screen shot of the VP Editor?
And a screen shot from “Update the Access Policy”, step 18.
I am not able to reply to your above question. I get and error “You are posting comments too quickly. Slow down.”
Strange. Try now that I’ve approved your previous comment?
Hello Patrick,
Great article with detailed instructions. In your case, it’s local DB authentication. How can I apply to AD Auth? Can I get rid of local DB auth? If yes, do I need generate bar code each VPN user in the organization?
Sorry for the late reply. I’m no APM expert by any means, but in the iRule you have this:
set static::ga_key_ldap_attr “google_auth_key”
If you specify the ldap attribute value with the one you guys use and then change set static::ga_key_storage “datagroup” with set static::ga_key_storage “ldap” you should be good to go.
Hello Patrick,
Great article with detailed instructions. In your case, it’s local DB authentication. How can I apply to AD Auth? Can I get rid of local DB auth? If yes, do I need generate car code each VPN user in the organization?
Very useful. Great article
Hello Patrick,
Thanks for your great article. I am testing config APM with Google Authenticator and Always on VPN ( always connected mode on BigIP Edge Client ). At the first time, I logged in with user/pass + Google token, but after I reboot computer I must input Google token again . Do you have solution to by pass it and only input username/password + gg token after timeout ?