Setting up F5 APM with Google Authenticator

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.

Configure a functioning VPN base

  1. 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.
  2. Start with opening up the Device Wizards
    devicewizard
  3. Choose the Network Access Setup Wizard for Remote Access and click Next
    networkaccesswizard
  4. Choose a policy name and click next.
  5. Then select “No authentication” (we will set that up later) and Next.
  6. 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:
    clientiprange
  7.  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.
  8. 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:splittunnelconfig
  9. Enter the IP of your dns server, click Next
  10. Enter the IP of the virtual server that will serve the VPN clients, click Next
  11. 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.

  1. Go to Access Policy, Local User DB, Manage Instances
    manageinstance
  2.  Click on Create New Instance
  3. Choose a name, click OK
  4. Then go to Access Policy, Local User DB, Manage usersmanageusers
  5. Click on Create user, enter username, password and select the instance you just created.
    createuser
  6. Now go to Access Policy, Access Profiles
    accessprofiles
  7. Click on Edit in the Access Policy column
    Editaccesspolicy
  8. Now the visual policy editor should launch. Click on the plus sign between “Logon page” and “Resource assign”.
    Addgettoken
  9.  Select the Authentication tab and then LocalDB Auth. Then click “Add Item”
    AddLocalDBAuth
  10.  Select your LocalDB Instance and click on “Save”
    LocalDBAuthconfig
  11. Your visual policy editor should look like this now:
    afterlocalauthpolicy
  12. Click on Apply Access Policy
    applyaccesspolicy

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

  1. Go to Local Traffic, Virtual servers
  2. Click on Create
  3. Give the virtual server a name, ie generate_ga_token
  4. Give the virtual server an IP on your local subnet
  5. Assign an HTTP profile
  6. Assign an SSL profile (if you want to use SSL)
  7. Assign generate_ga_code irule to the virtual server
  8. Click on Finished

Generate a token

  1. Surf to the address of the VIP, ie https://generategacode.mydomain.local, or http://192.168.1.50 (if that’s your VIP ip)
  2. You should then see this page:
    generatetokenpage
  3. 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.
  4. Enter a secret if you like, or let the load balancer generate it for you. You will not have to remember this secret later.
  5. Check generate QR code and click on “Submit”
  6. Open up your Google Authenticator app and touch the “plus sign”, select scan barcode and scan the QR code.
  7. Save the secret, we will need it soon.

Save the key in a data group list

  1. Go to Local Traffic, iRules, Data Group Lists
  2. Click on Create
  3. Give the data group list the name google_auth_keys and add your user name and the secret generated earlier
    googleauthkeysdatagrouplist
  4. Click Finished

Update the Virtual server with the verification iRule

  1. Go to Local Traffic, Virtual servers
  2. Find the the Virtual server created during the Wizard (the one the does not have redirect in it’s name) and click on it:
    virtualserversmyvpn
  3. In the following page, choose Resources and click on manage in the iRules section
    Addingirule
  4. 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.
    assigngacodeverify

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.

  1. Now go to Access Policy, Access Profiles
    accessprofiles
  2. Click on Edit in the Access Policy column
    Editaccesspolicy
  3. In the Visual policy editor, click the plus sign between LocalDB Auth and Resource Assignaddinggettoken
  4. In the Logon tab, choose Logon Page and then Add Item
    Addgettokenpage
  5. Change the text as marked in this picture:gettokenpageconfig
  6. Click Save
  7. Then click on Add New Macro
  8. Name it and click Save:emptymacro
  9. Now click on Edit Terminals in the Macro settings
  10. Click on Add Terminal
  11. Name the terminal “Failure”
  12. Rename the terminal called “Out” to successful
  13. Click on the Set default tab and set the default to Failure.
  14. Click on save
  15. Edit the new macro by clicking on the plus sign in the macro settings
    emptymacroaddevent
  16. Go to the General Purpose Tab, click on iRule event and then Add Item
  17. Name: Google Auth verification
    ID: ga_code_verify
    iruleevent
  18. 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 }
    branchrules
  19. Then click Save
  20. Click on the terminals and set Successful to Successful and the rest to Failureterminals
  21. Now we’re going to insert the Macro in the main policy. Click on the plus sign between Get GA Token and resource assign
  22. Click on the Macro tab and select your Verify Google Token macro. click “Add Item”
  23. Now click on Apply Access Policy
    applyaccesspolicy

Your final policy should look like this:

finalvpnpolicy

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.

Related Posts

33 thoughts on “Setting up F5 APM with Google Authenticator

  1. Great Article, Thanks Patrik,
    since long time I have been trying to setup the same but no luck, But this time you made it. 🙂

  2. 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

    1. 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?

  3. 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: &#160][ ]

    Any ideas? I am running APM 12.1.2

      1. Yes, when I try to save it on the APM as an iRule. I just can’t figure out what procedure is undefined.

  4. Yes, when I try to save it on the APM as an iRule. I just can’t figure out what procedure is undefined.

      1. 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?
         
           

        1. 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?

          1. 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?

          2. 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.

  5. 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

  6. 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

  7. 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 .

    1. 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

  8. 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.

  9. I am not able to reply to your above question. I get and error “You are posting comments too quickly. Slow down.”

  10. 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?

    1. 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.

  11. 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?

  12. 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 ?

Leave a Reply

Your email address will not be published. Required fields are marked *