7 Steps Checklist before upgrading your F5 BIG-IP
Problem this snippet solves: This is a quick summary of steps you need to check before upgrading a BIG-IP. This is valid for version 11.x or later. How to use this snippet: In this example, I will assume that we are upgrading from 11.5.1 to 12.1.2 Step 1 : Check the compatibility matrix a) For appliance, check hardware/software compatibility Link:https://support.f5.com/csp/article/K9476 b) For virtual edition, check the supported hypervisors matrix Link :https://support.f5.com/kb/en-us/products/big-ip_ltm/manuals/product/ve-supported-hypervisor-matrix.html Note : If running vCMP systems, verify also the vCMP host and compatible guest version matrix Link :https://support.f5.com/csp/article/K14088 Step 2 : Check supported BIG-IP upgrade paths and determine if you can upgrade directly Link:https://support.f5.com/csp/article/K13845 In this case, you must be running BIG-IP 10.1.x - 11.x to upgrade directly to BIG-IP 12.x Step 3 : Download .iso files needed for the upgrade from F5 Downloads Link:https://downloads.f5.com/esd/index.jsp Step 4 : Check if you need to re-activate the license before upgrading Link:https://support.f5.com/csp/article/K7727 First, determine the "License Check Date" of the version you want to install. In this case, the version 12.1.2 was released on 2016-03-18 (License Check Date). Then, determine your "Service check date" by executing the following command from CLI : > grep "Service check date" /config/bigip.license The output appears similar to the following example: > Service check date : 20151008 Since the "Service check date" (20151008) is older than the "License Check Date" (2016-03-18), a license a reactivation is needed before upgrading. To reactivate, follow the steps under paragraph "Reactivating the system license" from the link given above. Step 5 : Use "iHealth Upgrade Advisor" to determine if any configuration modification is needed before/after the upgrade <no longer available> Step 6 : Backup the configuration by generating a UCS archive and download it on a safe place Link: https://support.f5.com/csp/article/K13132 a) If are using the "Configuration Utility", follow the procedure under "Backing up configuration data by using the Configuration utility" b) If you prefer using CLI, follow the procedure under "Backing up configuration data using the tmsh utility" Step 7 : From the release note of the version you wish to install read the "Installation checklist" Link:https://support.f5.com/kb/en-us/products/big-ip_ltm/releasenotes/product/relnote-ltm-12-1-2.html Under the paragraph "Installation checklist" of the release note, ensure that you have read and verified listed points. Code : No code Tested this on version: 11.023KViews4likes15CommentsF5 Analytics iApp
Problem this snippet solves: Analytics iApp v3.7.0 You can use this fully supported version of the analytics iApp template to marshal statistical and logging data from the BIG-IP system. The iApp takes this data and formats it as a JSON object which is then exported for consumption by data consumers, such as F5 BIG-IQ or applications such as Splunk. The Analytics iApp allows you to configure several categories of data to be exported. For data consumers like Splunk, the iApp lets you configure the network endpoint to which the data is sent. Version 3.7.0 of the iApp template is fully supported by F5 and available on downloads.f5.com. We recommend all users upgrade to this version. For more information, see https://support.f5.com/csp/article/K07859431. While this version of the iApp is nearly identical to the v3.6.13 which was available on this page, the major difference (other than being fully supported) is that ability to gather APM statistics using the iApp has been removed from BIG-IP versions prior to 12.0. Supported/Tested BIG-IP versions: 11.4.0 - 12.1.2. Data Sources: LTM, GTM, AFM, ASM, APM, SWG, and iHealth (APM statistics require 12.0 or later) Data Output Formats: Splunk, F5 Analytics, F5 Risk Engine Splunk App: https://apps.splunk.com/apps/id/f5 The new deployment guide can be found on F5.com: http://f5.com/pdf/deployment-guides/f5-analytics-dg.pdf Video Demo - https://player.vimeo.com/video/156773835 Solution Architecture - 20s Installation - 1m53s UI Demo Device Dashboard - 6m44s Application Issue Troubleshooting - 9m26s Application Team Self Service - 12m17s Code : https://downloads.f5.com/esd/ecc.sv?sw=BIG-IP&pro=iApp_Templates&ver=iApps&container=iApp-Templates8.8KViews0likes95CommentsExport Virtual Server Configuration in CSV - tmsh cli script
Problem this snippet solves: This is a simple cli script used to collect all the virtuals name, its VIP details, Pool names, members, all Profiles, Irules, persistence associated to each, in all partitions. A sample output would be like below, One can customize the code to extract other fields available too. The same logic can be allowed to pull information's from profiles stats, certificates etc. Update: 5th Oct 2020 Added Pool members capture in the code. After the Pool-Name, Pool-Members column will be found. If a pool does not have members - field not present: "members" will shown in the respective Pool-Members column. If a pool itself is not bound to the VS, then Pool-Name, Pool-Members will have none in the respective columns. Update: 21st Jan 2021 Added logic to look for multiple partitions & collect configs Update: 12th Feb 2021 Added logic to add persistence to sheet. Update: 26th May 2021 Added logic to add state & status to sheet. Update: 24th Oct 2023 Added logic to add hostname, Pool Status,Total-Connections & Current-Connections. Note: The codeshare has multiple version, use the latest version alone. The reason to keep the other versions is for end users to understand & compare, thus helping them to modify to their own requirements. Hope it helps. How to use this snippet: Login to the LTM, create your script by running the below commands and paste the code provided in snippet tmsh create cli script virtual-details So when you list it, it should look something like below, [admin@labltm:Active:Standalone] ~ # tmsh list cli script virtual-details cli script virtual-details { proc script::run {} { puts "Virtual Server,Destination,Pool-Name,Profiles,Rules" foreach { obj } [tmsh::get_config ltm virtual all-properties] { set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all"context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],[tmsh::get_field_value $obj "pool"],$profilelist,[tmsh::get_field_value $obj "rules"]" } } total-signing-status not-all-signed } [admin@labltm:Active:Standalone] ~ # And you can run the script like below, tmsh run cli script virtual-details > /var/tmp/virtual-details.csv And get the output from the saved file, cat /var/tmp/virtual-details.csv Old Codes: cli script virtual-details { proc script::run {} { puts "Virtual Server,Destination,Pool-Name,Profiles,Rules" foreach { obj } [tmsh::get_config ltm virtual all-properties] { set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],[tmsh::get_field_value $obj "pool"],$profilelist,[tmsh::get_field_value $obj "rules"]" } } total-signing-status not-all-signed } ###=================================================== ###2.0 ###UPDATED CODE BELOW ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules" foreach { obj } [tmsh::get_config ltm virtual all-properties] { set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } else { puts "[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } total-signing-status not-all-signed } ###=================================================== ### Version 3.0 ### UPDATED CODE BELOW FOR MULTIPLE PARTITION ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Partition,Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } else { puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"]" } } } } total-signing-status not-all-signed } ###=================================================== ### Version 4.0 ### UPDATED CODE BELOW FOR CAPTURING PERSISTENCE ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Partition,Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules,Persist" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] set persist [lindex [lindex [tmsh::get_field_value $obj "persist"] 0] 1] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist" } } } else { puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"],$persist" } } } } total-signing-status not-all-signed } ###=================================================== ### 5.0 ### UPDATED CODE BELOW ### DO NOT MIX ABOVE CODE & BELOW CODE TOGETHER ###=================================================== cli script virtual-details { proc script::run {} { puts "Partition,Virtual Server,Destination,Pool-Name,Pool-Members,Profiles,Rules,Persist,Status,State" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { foreach { status } [tmsh::get_status ltm virtual [tmsh::get_name $obj]] { set vipstatus [tmsh::get_field_value $status "status.availability-state"] set vipstate [tmsh::get_field_value $status "status.enabled-state"] } set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] set persist [lindex [lindex [tmsh::get_field_value $obj "persist"] 0] 1] if { $poolname != "none" }{ set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate" } } } else { puts "$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate" } } } } total-signing-status not-all-signed } Latest Code: cli script virtual-details { proc script::run {} { set hostconf [tmsh::get_config /sys global-settings hostname] set hostname [tmsh::get_field_value [lindex $hostconf 0] hostname] puts "Hostname,Partition,Virtual Server,Destination,Pool-Name,Pool-Status,Pool-Members,Profiles,Rules,Persist,Status,State,Total-Conn,Current-Conn" foreach all_partitions [tmsh::get_config auth partition] { set partition "[lindex [split $all_partitions " "] 2]" tmsh::cd /$partition foreach { obj } [tmsh::get_config ltm virtual all-properties] { foreach { status } [tmsh::get_status ltm virtual [tmsh::get_name $obj]] { set vipstatus [tmsh::get_field_value $status "status.availability-state"] set vipstate [tmsh::get_field_value $status "status.enabled-state"] set total_conn [tmsh::get_field_value $status "clientside.tot-conns"] set curr_conn [tmsh::get_field_value $status "clientside.cur-conns"] } set poolname [tmsh::get_field_value $obj "pool"] set profiles [tmsh::get_field_value $obj "profiles"] set remprof [regsub -all {\n} [regsub -all " context" [join $profiles "\n"] "context"] " "] set profilelist [regsub -all "profiles " $remprof ""] set persist [lindex [lindex [tmsh::get_field_value $obj "persist"] 0] 1] if { $poolname != "none" }{ foreach { p_status } [tmsh::get_status ltm pool $poolname] { set pool_status [tmsh::get_field_value $p_status "status.availability-state"] } set poolconfig [tmsh::get_config /ltm pool $poolname] foreach poolinfo $poolconfig { if { [catch { set member_name [tmsh::get_field_value $poolinfo "members" ]} err] } { set pool_member $err puts "$hostname,$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_status,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate,$total_conn,$curr_conn" } else { set pool_member "" set member_name [tmsh::get_field_value $poolinfo "members" ] foreach member $member_name { append pool_member "[lindex $member 1] " } puts "$hostname,$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,$pool_status,$pool_member,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate,$total_conn,$curr_conn" } } } else { puts "$hostname,$partition,[tmsh::get_name $obj],[tmsh::get_field_value $obj "destination"],$poolname,none,none,$profilelist,[tmsh::get_field_value $obj "rules"],$persist,$vipstatus,$vipstate,$total_conn,$curr_conn" } } } } } Tested this on version: 13.08.2KViews9likes25CommentsServerside SNI injection iRule
Problem this snippet solves: Hi Folks, the iRule below can be used to inject a TLS SNI extension to the server side based on e.g. HOST-Header values. The iRule is usefull if your pool servers depending on valid SNI records and you don't want to configure dedicated Server SSL Profiles for each single web application. Cheers, Kai How to use this snippet: Attach the iRule to the Virtual Server where you need to insert a TLS SNI expension Tweak the $sni_value variable within the HTTP_REQUEST to meet your requirements or move it to a different event as needed. Make sure you've cleared the "Server Name" option in your Server_SSL_Profile. Code : when HTTP_REQUEST { #Set the SNI value (e.g. HTTP::host) set sni_value [getfield [HTTP::host] ":" 1] } when SERVERSSL_CLIENTHELLO_SEND { # SNI extension record as defined in RFC 3546/3.1 # # - TLS Extension Type = int16( 0 = SNI ) # - TLS Extension Length = int16( $sni_length + 5 byte ) # - SNI Record Length = int16( $sni_length + 3 byte) # - SNI Record Type = int8( 0 = HOST ) # - SNI Record Value Length = int16( $sni_length ) # - SNI Record Value = str( $sni_value ) # # Calculate the length of the SNI value, Compute the SNI Record / TLS extension fields and add the result to the SERVERSSL_CLIENTHELLO SSL::extensions insert [binary format SSScSa* 0 [expr { [set sni_length [string length $sni_value]] + 5 }] [expr { $sni_length + 3 }] 0 $sni_length $sni_value] } Tested this on version: 12.06.1KViews7likes30CommentsHTTP To HTTPS Redirect 301
Problem this snippet solves: Redirects all traffic to same hostname, same URI over https by issuing a redirect with status 301 (Moved Permanently). You can change the status code to a 302 to issue a non-cacheable redirect. Apply to HTTP virtual server to redirect all traffic to same hostname (stripping port if it exists), same URI over HTTPS. (Do not apply to shared/wildcard virtual server responding to HTTPS traffic, or infinite redirect will occur. Create separate virtual servers on port 80 and port 443, and apply this iRule ONLY to the port 80 HTTP-only virtual server. No iRule is needed on the port 443 HTTPS virtual server.) How to use this snippet: when HTTP_REQUEST { HTTP::respond 301 Location "https://[getfield [HTTP::host] : 1][HTTP::uri]" } The above rule may be modified to function on a shared virtual server by testing TCP::local_port and redirecting only if the request came in over port 80: when HTTP_REQUEST { if { [TCP::local_port] == 80 }{ HTTP::respond 301 Location "https://[getfield [HTTP::host] : 1][HTTP::uri]" } } Here is another option which handles HTTP requests which don't have a Host header value. In such a case, the VIP's IP address is used for the host in the redirect. Code : when HTTP_REQUEST { # Check if Host header has a value if {[HTTP::host] ne ""}{ # Redirect to the requested host and URI (minus the port if specified) HTTP::respond 301 Location "https://[getfield [HTTP::host] ":" 1][HTTP::uri]" } else { # Redirect to VIP's IP address HTTP::respond 301 Location "https://[IP::local_addr][HTTP::uri]" } }5.6KViews1like3CommentsGoogle Authenticator Token Verification iRule For APM
Problem this snippet solves: This iRule adds token authentication capabilities for Google Authenticator to APM. The implementation is described in George Watkins' article: Two Factor Authentication With Google Authenticator And APM The iRule should be applied to an access policy-enabled virtual server. In order to provide two-factor authentication, a AAA server must be defined to verify user credentials. The users' Google Authenticator secrets can be mapped to individual users using a data group, an LDAP schema attribute, or an Active Directory attribute. The storage method can be defined in the beginning section of the iRule. Here are a list of all the configurable options: lockout_attempts - number of attempts a user is allowed to make prior to being locked out temporarily lockout_period - duration of lockout period ga_code_form_field - name of HTML form field used in the APM logon page, this field is define in the "Logon Page" access policy object ga_key_storage - key storage method for users' Google Authenticator shared keys, valid options include: datagroup, ldap, or ad ga_key_ldap_attr - name of LDAP schema attribute containing users' key ga_key_ad_attr - name of Active Directory schema attribute containing users' key ga_key_dg - data group containing user := key mappings Code : 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 } }4.8KViews2likes5CommentsAPM Sharepoint authentication
Problem this snippet solves: Updated version to support Webdav with windows explorer after Nicolas's comment. APM is a great authentication service but it does it only with forms. The default behavior is to redirect user to /my.policy to process VPE. this redirect is only supported for GET method. Sharepoint provide 3 different access types: browsing web site with a browser Editing documents with Office browser folder with webdav client (or editing documents with libreoffice through webdav protocol) This irule display best authentication method for each of these access types: browsers authenticate with default authentication method (form based authentication) Microsoft office authenticate with Form based authentication (with support of MS-OFBA protocol) Libreoffice and webdav clients authenticate with 401 basic authentication Form based authentication (browser and Microsoft office) is compatible (validated for one customer) with SAML authentication Editing documents is managed with a persistent cookie expiring after 5 minutes. to be shared between IE and Office, it requires : cookie is persistent (expiration date instead of deleted at the end of session) web site defined as "trusted sites" in IE. How to use this snippet: install this irule and enable it on the VS. Code : when RULE_INIT { array set static::MSOFBA { ReqHeader "X-FORMS_BASED_AUTH_REQUIRED" ReqVal "/sp-ofba-form" ReturnHeader "X-FORMS_BASED_AUTH_RETURN_URL" ReturnVal "/sp-ofba-completed" SizeHeader "X-FORMS_BASED_AUTH_DIALOG_SIZE" SizeVal "800x600" } set static::ckname "MRHSession_SP" set static::Basic_Realm_Text "SharePoint Authentication" } when HTTP_REQUEST { set apmsessionid [HTTP::cookie value MRHSession] set persist_cookie [HTTP::cookie value $static::ckname] set clientless_mode 0 set form_mode 0 # Identify User-Agents type if {[HTTP::header exists "X-FORMS_BASED_AUTH_ACCEPTED"] && (([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t") || ([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f"))} { set clientless_mode 0; set form_mode 1 } else { switch -glob [string tolower [HTTP::header "User-Agent"]] { "*microsoft-webdav-miniredir*" { set clientless_mode 1 } "*microsoft data access internet publishing provider*" - "*office protocol discovery*" - "*microsoft office*" - "*non-browser*" - "msoffice 12*" { set form_mode 1 } "*mozilla/4.0 (compatible; ms frontpage*" { if { [ string range [getfield [string tolower [HTTP::header "User-Agent"]] "MS FrontPage " 2] 0 1] > 12 } { set form_mode 1 } else { set clientless_mode 1 } } "*mozilla*" - "*opera*" { set clientless_mode 0 } default { set clientless_mode 1 } } } if { $clientless_mode || $form_mode } { if { [HTTP::cookie exists "MRHSession"] } {set apmstatus [ACCESS::session exists -state_allow $apmsessionid]} else {set apmstatus 0} if { !($apmstatus) && [HTTP::cookie exists $static::ckname] } {set apmpersiststatus [ACCESS::session exists -state_allow $persist_cookie]} else {set apmpersiststatus 0} if { ($apmpersiststatus) && !($apmstatus) } { # Add MRHSession cookie for non browser user-agent first request and persistent cookie present if { [catch {HTTP::cookie insert name "MRHSession" value $persist_cookie} ] } {log local0. "[IP::client_addr]:[TCP::client_port] : TCL error on HTTP cookie insert MRHSession : URL : [HTTP::host][HTTP::path] - Headers : [HTTP::request]"} else {return} } } else { return } if { $clientless_mode && !($apmstatus)} { if { !([HTTP::header Authorization] == "") } { set clientless(insert_mode) 1 set clientless(username) [ string tolower [HTTP::username] ] set clientless(password) [HTTP::password] binary scan [md5 "$clientless(password)"] H* clientless(hash) set user_key "$clientless(username).$clientless(hash)" set clientless(cookie_list) [ ACCESS::user getsid $user_key ] if { [ llength $clientless(cookie_list) ] != 0 } { set clientless(cookie) [ ACCESS::user getkey [ lindex $clientless(cookie_list) 0 ] ] if { $clientless(cookie) != "" } { HTTP::cookie insert name MRHSession value $clientless(cookie) set clientless(insert_mode) 0 } } if { $clientless(insert_mode) } { HTTP::header insert "clientless-mode" 1 HTTP::header insert "username" $clientless(username) HTTP::header insert "password" $clientless(password) } unset clientless } else { HTTP::respond 401 WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close return } } elseif {$form_mode && !($apmstatus) && !([HTTP::path] equals $static::MSOFBA(ReqVal))}{ HTTP::respond 403 -version "1.1" noserver \ $static::MSOFBA(ReqHeader) "https://[HTTP::host]$static::MSOFBA(ReqVal)" \ $static::MSOFBA(ReturnHeader) "https://[HTTP::host]$static::MSOFBA(ReturnVal)" \ $static::MSOFBA(SizeHeader) $static::MSOFBA(SizeVal) \ "Connection" "Close" return } } when HTTP_RESPONSE { # Insert persistent cookie for html content type and private session if { [HTTP::header "Content-Type" ] contains "text/html" } { HTTP::cookie remove $static::ckname HTTP::cookie insert name $static::ckname value $apmsessionid path "/" HTTP::cookie expires $static::ckname 120 relative HTTP::cookie secure $static::ckname enable } # Insert session cookie if session was recovered from persistent cookie if { ([info exists "apmpersiststatus"]) && ($apmpersiststatus) } { HTTP::cookie insert name MRHSession value $persist_cookie path "/" HTTP::cookie secure MRHSession enable } } when ACCESS_SESSION_STARTED { if {([info exists "clientless_mode"])} { ACCESS::session data set session.clientless $clientless_mode } if { [ info exists user_key ] } { ACCESS::session data set "session.user.uuid" $user_key } } when ACCESS_POLICY_COMPLETED { if { ([info exists "clientless_mode"]) && ($clientless_mode) && ([ACCESS::policy result] equals "deny") } { ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=$static::Basic_Realm_Text" Connection close ACCESS::session remove } } when ACCESS_ACL_ALLOWED { switch -glob [string tolower [HTTP::path]] { "/sp-ofba-form" { ACCESS::respond 302 noserver Location "https://[HTTP::host]$static::MSOFBA(ReturnVal)" } "/sp-ofba-completed" { ACCESS::respond 200 content { Authenticated Good Work, you are Authenticated } noserver } "*/signout.aspx" { # Disconnect session and redirect to APM logout Page ACCESS::respond 302 noserver Location "/vdesk/hangup.php3" return } "/_layouts/accessdenied.aspx" { # Disconnect session and redirect to APM Logon Page if {[string tolower [URI::query [HTTP::uri] loginasanotheruser]] equals "true" } { ACCESS::session remove ACCESS::respond 302 noserver Location "/" return } } default { # No Actions } } } Tested this on version: 11.54.8KViews1like57CommentsSet the SameSite Cookie Attribute for Web Application and BIG-IP Module Cookies
Problem this snippet solves: UPDATE: Note that the work for SameSite is evolving rapidly and this new entry should be considered over the iRule contents below. Chrome (and likely other browsers to follow) will enforce the SameSite attribute on HTTP cookies to Lax beginning soon (initial limited rollout week of Feb 17th, 2020) which could impact sites that don't explicitly set the attribute. This iRule will set the SameSite attribute in all BIG-IP and app cookies found in Set-Cookie headers. Note that this would not modify cookies set on the client using javascript or other methods. Contributed by: hoolio How to use this snippet: Apply the iRule to the appropriate virtual servers. Code : when HTTP_RESPONSE_RELEASE { # Set all BIG-IP and app cookies found in Set-Cookie headers using this iRule to: # none: Cookies will be sent in both first-party context and cross-origin requests; # however, the value must be explicitly set to None and all browser requests must # follow the HTTPS protocol and include the Secure attribute which requires an encrypted # connection. Cookies that don't adhere to that requirement will be rejected. # Both attributes are required together. If just None is specified without Secure or # if the HTTPS protocol is not used, the third-party cookie will be rejected. # # lax: Cookies will be sent automatically only in a first-party context and with HTTP GET requests. # SameSite cookies will be withheld on cross-site sub-requests, such as calls to load images or iframes, # but will be sent when a user navigates to the URL from an external site, e.g., by following a link. # # strict: browser never sends cookies in requests to third party domains # # Above definitions from: https://docs.microsoft.com/en-us/microsoftteams/platform/resources/samesite-cookie-update # # Note: this iRule would not modify cookies set on the client using Javascript or other methods outside of Set-Cookie headers! set samesite_security "none" # Log debug to /var/log/ltm? (1=yes, 0=no) set cookie_debug 1 set cookie_names [HTTP::cookie names] if {$cookie_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: \[HTTP::header values {Set-Cookie}\]: [HTTP::header values {Set-Cookie}]"} if {$cookie_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: \$cookie_names ([llength $cookie_names]): $cookie_names"} foreach a_cookie $cookie_names { # Remove any prior instances of SameSite attributes HTTP::cookie attribute $a_cookie remove {samesite} # Insert a new SameSite attribute HTTP::cookie attribute $a_cookie insert {samesite} $samesite_security # If samesite attribute is set to None, then the Secure flag must be set for browsers to accept the cookie if {[string equal -nocase $samesite_security "none"]} { HTTP::cookie secure $a_cookie enable } } if {$cookie_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Set-Cookie header values: [HTTP::header values {Set-Cookie}]"} } Tested this on version: 13.04.7KViews4likes5CommentsSecurity Headers Insertion
Problem this snippet solves: Centralize the security header management for one or more domains on the recommendation of SecurityHeaders.io. Be warned!! You can really do damage to your availability if you do not understand these headers and their implications to your client browsers, make sure your header values are tested and vetted before applying to any production traffic. Background on the headers: Content-Security-Policy X-Frame-Options X-XSS-Protection X-Content-Type-Options Public-Key-Pins Strict-Transport-Security How to use this snippet: apply this iRule to your virtual servers, once customized for your environment. Code : when RULE_INIT { set static::fqdn_pin1 "X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg=" set static::fqdn_pin2 "MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec=" set static::max_age 15552000 } when HTTP_REQUEST { HTTP::respond 301 Location "https://[HTTP::host][HTTP::uri]" } when HTTP_RESPONSE { #HSTS HTTP::header insert Strict-Transport-Security "max-age=$static::max_age; includeSubDomains" #HPKP HTTP::header insert Public-Key-Pins "pin-sha256=\"$static::fqdn_pin1\" max-age=$static::max_age; includeSubDomains" #X-XSS-Protection HTTP::header insert X-XSS-Protection "1; mode=block" #X-Frame-Options HTTP::header insert X-Frame-Options "DENY" #X-Content-Type-Options HTTP::header insert X-Content-Type-Options "nosniff" #CSP HTTP::header insert Content-Security-Policy "default-src https://devcentral.f5.com/s:443" #CSP for IE HTTP::header insert X-Content-Security-Policy "default-src https://devcentral.f5.com/s:443" } Tested this on version: 12.04.5KViews1like21CommentsDemystifying Time-based OTP
This article is written as an extensive explanation of how a Time-based OTP algorithm works and some guidelines on how to implement this in your F5. What is a TOTP? TOTP (aka Time-based OTP) is a way to use a code that is changing every 30 seconds instead of using a static password. REF - https://en.wikipedia.org/wiki/Time-based_one-time_password REF - https://datatracker.ietf.org/doc/html/rfc6238 So, in summary, every user has one secret associated that is shared between them and a third entity (F5), with this secret, it is possible to generate a 6-digit code that changes every 30 seconds, as Google and other vendors do. Take into account that most of the vendors are using the same algorithm, so, working with Google Authenticator is the same as using any other 6-digits TOTP (Microsoft Authenticator, FortiToken Mobile, etc.). How to implement TOTP in production? TOTP is composed of 3 steps: Generation of the secret Distribution of the secret Validation of the secret How a secret is generated? You can generate the code in many ways, but your goal is to get a 16-digit word (base32) for each user. Next below, we are showing how to get this secret using TCL commands. # Generate a random number as seed set num [expr rand()] # OUTPUT: 0.586026769404 # generate a hash of this seed set num_hash [md5 $num] # OUTPUT: Ï�àD½È�W\ݼú�Uä # Encode this hash using base64 set num_b64 [b64encode $num_hash] # OUTPUT: Cc+e4ES9yJRXXN28+o5V5A== # Take only the first 10 digits of this previous code (10 digits x 8 bits = 80 bits) set secret_raw [string range $num_b64 0 9] # OUTPUT: Cc+e4ES9yJ # Encode the previous code using base32 (80 bits / 5 bits by word = 16 words) set secret_b32 [call b32encode $secret_raw] # OUTPUT: INRSWZJUIVJTS6KK BTW, this is how a Base32 dictionary works, I mean, the equivalence between words and bits. 00000 - A 00001 - B 00010 - C 00011 - D 00100 - E 00101 - F 00110 - G 00111 - H 01000 - I 01001 - J 01010 - K 01011 - L 01100 - M 01101 - N 01110 - O 01111 - P 10000 - Q 10001 - R 10010 - S 10011 - T 10100 - U 10101 - V 10110 - W 10111 - X 11000 - Y 11001 - Z 11010 - 2 11011 - 3 11100 - 4 11101 - 5 11110 - 6 11111 - 7 0000 - A=== 0001 - C=== 0010 - E=== 0011 - G=== 0100 - I=== 0101 - K=== 0110 - M=== 0111 - O=== 1000 - Q=== 1001 - S=== 1010 - U=== 1011 - W=== 1100 - Y=== 1101 - 2=== 1110 - 4=== 1111 - 6=== 000 - A====== 001 - E====== 010 - I====== 011 - M====== 100 - Q====== 101 - U====== 110 - Y====== 111 - 4====== 00 - A= 01 - I= 10 - Q= 11 - Y= 0 - A==== 1 - Q==== REF - https://datatracker.ietf.org/doc/html/rfc4648#page-8 If you are interested, there are other iRules to generate base32 codes. Here are some examples: https://community.f5.com/t5/crowdsrc/tcl-procedures-for-base32-encoding-decoding/ta-p/286602 https://community.f5.com/t5/technical-articles/base32-encoding-and-decoding-with-irules/ta-p/277299 How a secret is distributed? Most of the time, the secret is distributed using QR codes, because it’s an easy way to distribute it to dummy users. Google Authenticator and any other vendors use this scheme: # EXAMPLE: otpauth://totp/ACME:john@acme.com?secret=INRSWZJUIVJTS6KK ## WHERE: ACME - Company john@acme.com - User Account secret=INRSWZJUIVJTS6KK - Secret REF - https://github.com/google/google-authenticator/wiki/Key-Uri-Format So, the best plan is to inject this previous sentence into a QR code. Here is an example: https://rootprojects.org/authenticator/ With the example above, is clear how a user can get the secret in their smartphone, but take into account that both entities (user and F5) have to know the secret in order to be able to perform those authentications. Later on, we will show you some tips to store the key from the F5 perspective. How a secret is validated? When both (the user and the F5) know the secret, they can authenticate using a TOTP. Next below, we are showing the steps required to generate a Time-based code from the secret. # We start knowing the secret (base32) set secret_b32 "INRSWZJUIVJTS6KK" # OUTPUT: INRSWZJUIVJTS6KK # Decode the secret from a b32 code (translating to a 10 digits secret) set secret_raw [call b32decode $secret_b32] # OUTPUT: Cc+e4ES9yJ # ---------------------------------- # There are other ways to decode b32, here is another example set secret_b32 "INRSWZJUIVJTS6KK" # OUTPUT: INRSWZJUIVJTS6KK set secret_binary [string map -nocase $static::b32_to_binary $secret_b32] # OUTPUT: 01000011 01100011 00101011 01100101 00110100 01000101 01010011 00111001 01111001 01001010 set secret_raw [binary format B80 $secret_binary] # OUTPUT: Cc+e4ES9yJ # ---------------------------------- # Get a UNIX timestamp and divide it by 30 (to get gaps of 30 seconds) set clock [expr { [clock seconds] / 30 } ] # OUTPUT: 53704892 # Translate the previous code into binary set clock_raw [binary format W* $clock]] # OUTPUT: 00000000 00000000 00000000 00000000 00000011 00110011 01111000 10111100 # Sign the clock value using the secret value, which means "HMAC-SHA1[secret,clock]" set hmac_raw [CRYPTO::sign -alg hmac-sha1 -key $secret_raw $clock_raw] # OUTPUT: Ùòbàc¹´Í¬{�ü�s)�3 # Translate the previous code to hexadecimal binary scan $hmac_raw H* hmac # OUTPUT: 1cd9f262e063b9b4cd13ac7b8dfc8a7329801733 # Take the last digit of this hexadecimal code ("3" in this case) set last_char [string index $hmac end] # OUTPUT: 3 # Multiply the last value by 2 to generate a range of 16 possible 4-bytes words, as it's shown below # Note that the last two digits are always ignored set offset [expr { "0x$last_char" * 2 } ] # OUTPUT: 6 # Example: # 0: 1cd9f262 e063b9b4cd13ac7b8dfc8a7329801733 # 1: 1c d9f262e0 63b9b4cd13ac7b8dfc8a7329801733 # 2: 1cd9 f262e063 b9b4cd13ac7b8dfc8a7329801733 # 3: [1cd9f2 62e063b9 b4cd13ac7b8dfc8a7329801733] <- This word is selected (last digit = '3') # 4: 1cd9f262 e063b9b4 cd13ac7b8dfc8a7329801733 # 5: 1cd9f262e0 63b9b4cd 13ac7b8dfc8a7329801733 # 6: 1cd9f262e063 b9b4cd13 ac7b8dfc8a7329801733 # 7: 1cd9f262e063b9 b4cd13ac 7b8dfc8a7329801733 # 8: 1cd9f262e063b9b4 cd13ac7b 8dfc8a7329801733 # 9: 1cd9f262e063b9b4cd 13ac7b8d fc8a7329801733 # a: 1cd9f262e063b9b4cd13 ac7b8dfc 8a7329801733 # b: 1cd9f262e063b9b4cd13ac 7b8dfc8a 7329801733 # c: 1cd9f262e063b9b4cd13ac7b 8dfc8a73 29801733 # d: 1cd9f262e063b9b4cd13ac7b8d fc8a7329 801733 # e: 1cd9f262e063b9b4cd13ac7b8dfc 8a732980 1733 # f: 1cd9f262e063b9b4cd13ac7b8dfc8a 73298017 33 # Get the word from the table based on the last digit (see example above) set word [string range $hmac $offset [expr { $offset + 7 } ]] # OUTPUT: 62e063b9 # Translate the previous code to base10 (removing negative values) set us_word [expr { "0x$word" & 0x7FFFFFFF } ] # OUTPUT: 1658872761 (62e063b9) # Apply a modulus 1000000 to get a 6-digits range number [000000 - 999999] set token [format %06d [expr { $us_word % 1000000 } ]] # OUTPUT: 872761 # The previous value is the token that the user should use during authentication # This value is changing every 30 seconds. There are many iRules you can use to validate your user input codes. Here are some examples: https://community.f5.com/t5/crowdsrc/google-authenticator-verification-irule-tmos-v11-1-optimized/ta-p/286672 https://community.f5.com/t5/crowdsrc/apm-google-authenticator-http-api/ta-p/287952 https://community.f5.com/t5/crowdsrc/google-authenticator-token-verification-irule-for-apm/ta-p/277510 How a secret is stored? At this point, the user knows their secret (they already got their QR code with the secret), but the F5 still doesn't know how to get the secret to check if the TOTP provided by the user is correct. There are many ways: Store a key pair of "user-secret" in a data group. It is really simple to implement, but not secure in a production environment because the secrets are stored in cleartext. Store a key pair of "user-encrypted(secret)" in a data group. That solves the problem of storing the secrets in cleartext, but it’s not scalable. AsStan_PIRON_F5 pointed out here. There is a way to store those secrets in AD fields in an encrypted way that could suit a production environment. Here below, we describe those steps, using Powershell scripts that should be running on the Windows Server where the AD resides. 1. Generate a symmetric key to encrypt the secrets. function Create-AesKey($KeySize) { $AesManaged = New-Object "System.Security.Cryptography.AesManaged" $AesManaged.KeySize = $KeySize $AesManaged.GenerateKey() [System.Convert]::ToBase64String($AesManaged.Key) } $size= $Args[0] $key = Create-AesKey $size Write-Output $key Input: .\CreateKey.ps1 256 Output: pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng= 2. Store each user secret in the ‘pager’ field of the AD. function Encrypt-Data($AesKey, $Data) { $Data = [System.Text.Encoding]::UTF8.GetBytes($Data) $AesManaged = New-Object "System.Security.Cryptography.AesManaged" $AesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $AesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 $AesManaged.BlockSize = 128 $AesManaged.KeySize = 256 $AesManaged.Key = [System.Convert]::FromBase64String($AesKey) $Encryptor = $AesManaged.CreateEncryptor() $EncryptedData = $Encryptor.TransformFinalBlock($Data, 0, $Data.Length); [byte[]] $EncryptedData = $AesManaged.IV + $EncryptedData $AesManaged.Dispose() [System.Convert]::ToBase64String($EncryptedData) } $username = $Args[0] $encryptKey = "pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng=" [String]$userkey = "" 1..16 | % { $userkey += $(Get-Random -InputObject A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,2,3,4,5,6,7) } $encrypted = Encrypt-Data $encryptKey $userkey Write-Output "Key: $userkey ; Encrypted: $encrypted" Set-AdUser -Identity $username -replace @{"pager"="$encrypted"} Input: .\EncryptData.ps1 myuser INRSWZJUIVJTS6KK Output: Key: INRSWZJUIVJTS6KK Encrypted: i6GoODygXJ05vG2xWcatNjrl1NubA1xHEZpMTzOlsdx52oeEp1a4891CdM5/aCMg 3. Validate that the secret was stored correctly function Decrypt-Data($AesKey, $Data) { $Data = [System.Convert]::FromBase64String($Data) $AesManaged = New-Object "System.Security.Cryptography.AesManaged" $AesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $AesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7 $AesManaged.BlockSize = 128 $AesManaged.KeySize = 256 $AesManaged.IV = $Data[0..15] $AesManaged.Key = [System.Convert]::FromBase64String($AesKey) $Decryptor = $AesManaged.CreateDecryptor(); $DecryptedData = $Decryptor.TransformFinalBlock($Data, 16, $Data.Length - 16); $aesManaged.Dispose() [System.Text.Encoding]::UTF8.GetString($DecryptedData) } $encryptKey = "pnnqLfua6Mk/Oh3xqWV/6NTLd0r0aYaO4je3irwDbng=" $userkey = $Args[0] $decrypted = Decrypt-Data $encryptKey $userkey Write-Output "Key: $decrypted" Input: .\DecryptData.ps1 i6GoODygXJ05vG2xWcatNjrl1NubA1xHEZpMTzOlsdx52oeEp1a4891CdM5/aCMg Output: INRSWZJUIVJTS6KK How to generate a QR code? There are many ways to generate a QR code from a secret word. 1. Google has an API to generate QR codes, still works but it’s in a deprecated state. REF - https://developers.google.com/chart/infographics/docs/qr_codes ## EXAMPLE: https://chart.googleapis.com/chart?cht=qr&chs=200x200&chld=M|0&chl=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA ## WHERE: cht=qr - QR Code chs=200x200 - Sizing chld=M|0 - Redundancy 'M' and Margin '0' chl=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA - Message 2. Similar to Google, there are other APIs to generate those QR codes, but like with the previous API from Google, using them is a wrong decision because you are sending your secret to an external entity. REF - https://quickchart.io/documentation/#qr ## EXAMPLE https://quickchart.io/qr?size=200&ecLevel=M&margin=1&text=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA ## WHERE: size=200 - Sizing ecLevel=M - Redundancy 'M' margin=1 - Margin '1' text=otpauth://totp/myser@mydomain.com?secret=AAAAAAAAAAAAAAAA - Message 3. The best way to implement this in a production environment is to configure a dedicated server to generate those QR codes. There are many options on the internet, here is an example: REF - https://github.com/edent/QR-Generator-PHP Requirements: yum install php php-mysql php-fpm yum install php-gd4.3KViews8likes2Comments