Export 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.2KViews9likes25CommentsWorldTech IT - Who Ya Gonna Call? Scary Hack Story
At WorldTech IT, our specialty for Always-On emergency support means that when things go bump in the night on F5 devices, we're the ones who wake up and investigate. We've seen our fair share of headless entities, killer bugs, gremlins, possessions, zombies, and daemons, but our scariest hack story started like any other day.2.2KViews8likes2CommentsDemystifying 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.3KViews8likes2CommentsServerside 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.2KViews7likes30CommentsPrevent BIG-IP Edge Client VPN Driver to roll back (or forward) during PPP/RAS errors
If you (like some of my customers) want to have the BIG-IP Edge Client packaged and distributed as a software package within your corporate infrastructure and therefore have switched off automatic component updates in your connectivity profiles, you might still get the covpn64.sys file upgraded or downgraded to the same version as the one installed on the BIG-IP APM server. Background We discovered that on some Windows clients the file covpn64.sys file got a newer/older timestamp in and started to investigate what caused this. The conclusion was that sometimes after hibernation or sleep, the Edge Client is unable to open the VPN interface and therefore tries to reinstall the driver. However, instead of using a local copy of the CAB file where the covpn64.sys file resides, it downloads it from the APM server regardless of if the version on the server and client match each other or not. In normal circumstances when you have automatic upgrades on the clients, this might not be a problem, however when you need to have full control on which version is being used on each connected client, this behavior can be a bit of a problem. Removing the Installer Component? Now you might be thinking, hey… Why don't you just remove the Component Installer module from the Edge Client and you won't have this issue. Well the simple answer to this is the fact that the Component Installer module is not only used to install/upgrade the client. In fact, it seems like it's also used when performing the Machine Check Info from the Access Policy when authenticating the user. So by removing the Component Installer module result in other issues. The Solution/workaround The Solution I came up with is to store each version of the urxvpn.cab file in an IFile and then use an iRule to deliver the correct version whenever a client tries to fetch the file for reinstallation. What's needed? In order to make this work we need to Grab a copy of urxvpn.cab from each version of the client Create an IFile for each of these versions Install iRule Attach iRule to the Virtual Server that is running the Access Policy Fetching the file from the apmclients ISOs For every version of the APM client that is available within your organization a corresponding iFile needs to be created. To create the iFiles automatically you can do the following on the APM server. Login to the CLI console with SSH Make sure you are in bash by typing bash Create temporary directories mkdir /tmp/apm-urxvpn mkdir /tmp/apm-iso Run the following (still in bash not TMSH) on the BIG-IP APM server to automatically extract the urxvpn.cab file from each installed image and save them in the folder /tmp/apm-urxvpn. for c in /shared/apm/images/apmclients-* do version="$(echo "$c" | awk -F. \ '{gsub(".*apmclients-","");printf "%04d.%04d.%04d.%04d", $1, $2, $3, $4}')" && \ (mount -o ro $c /tmp/apm-iso cp /tmp/apm-iso/sam/www/webtop/public/download/urxvpn.cab \ /tmp/apm-urxvpn/URXVPN.CAB-$version umount /tmp/apm-iso) done Check the files copied ls -al /tmp/apm-urxvpn Import each file either with tmsh or with GUI. We will cover how to import with tmsh below. If you prefer to do it with the GUI, more information abour how to do it can be found in K13423 You can use the following script to automatically import all files cd /tmp/apm-urxvpn for f in URXVPN.CAB-* do printf "create sys file ifile $f source-path file:$(pwd)/$f\ncreate ltm ifile $f file-name $f\n" | tmsh done Save the new configuration tmsh -c “save sys config” Time to create the iRule when CLIENT_ACCEPTED { ACCESS::restrict_irule_events disable } when HTTP_REQUEST { set uri [HTTP::uri] set ua [HTTP::header "User-Agent"] if {$uri starts_with "/vdesk" || $uri starts_with "/pre"} { set version "" regexp -- {EdgeClient/(\d{4}\.\d{4}\.\d{4}\.\d{4})} $ua var version if {$version != ""} { table set -subtable vpn_client_ip_to_versions [IP::client_addr] $version 86400 86400 } else { log local0.debug "Unable to parse version from: $ua for IP: [IP::client_addr] URI: $uri" } } elseif {$uri == "/public/download/urxvpn.cab"} { set version "" regexp -- {EdgeClient/(\d{4}\.\d{4}\.\d{4}\.\d{4})} $ua var version if {$version == ""} { log local0.warning "Unable to parse version from: $ua, will search session table" set version [table lookup -subtable vpn_client_ip_to_versions [IP::client_addr]] log local0.warning "Version in table: $version" } if {$version == ""} { log local0.warning "Unable to find version session table" HTTP::respond 404 content "Missing version in request" "Content-Type" "text/plain" } else { set out "" catch { set out [ifile get "/Common/URXVPN.CAB-$version"] } if {$out == ""} { log local0.error "Didn't find urxvpn.cab file for Edge Client version: $version" HTTP::respond 404 content "Unable to find requested file for version $version\n" "Content-Type" "text/plain" } else { HTTP::respond 200 content $out "Content-Type" "application/vnd.ms-cab-compressed" } } } } Add the iRule to the APM Virtual Server Known Limitations If multiple clients with different versions of the Edge Client are behind the same IP address, they might download the wrong version. This is due to the fact that the client doesn't present the version when the request for the file urxvpn.cab reaches the iRule. This is why the iRule tries to store IP addresses based on the source IP address of other requests related to the VPN. More information about this problem can be found in K0001327351.8KViews6likes1CommentiRule to assist with CVE-2022-22965 mitigation
Hi there, On March 30, 2022, a remote code execution (RCE) vulnerabilitywas found in the Java Spring Framework, identified by the CVE 2022-22965. I am sharing an example iRule to assist with mitigation of this CVE. This may require further customization but it's a great start 🙂 This iRule is not supported by F5. Link to the iRule.2KViews5likes0CommentsStep-by-step guide to build a F5 AWAF lab on Google Cloud
This is a small step by step guide on how to build a F5 AWAF (Advanced Web Application Firewall) lab environment on GCP (Google Cloud Platform). The purpose of this guide is to provide an easy way for quickly spin up a lab environment which can be used for study or demo purposes. https://github.com/pedrorouremalta/f5-awaf-lab-on-gcp1.3KViews5likes2CommentsPOC: Validate JWT with iRule
Code is community submitted, community supported, and recognized as ‘Use At Your Own Risk’. Short Description This is a proof of concept iRule to decode and validate a JWT submitted in the HTTP Authorization header. It supports only JWT's signed with RS256 and was roughly tested with Azure, Okta and ADFS tokens. It is not designed for production usage, especially there are more checks required to comply with https://datatracker.ietf.org/doc/html/rfc7519#section-7.2. Problem solved by this Code Snippet Decode and validate a JWT with an iRule. How to use this Code Snippet Attach it to a VS and adapt the public key(s) and key id(s). Code Snippet Meta Information Version: POC Coding Language: iRule Full Code Snippet https://github.com/JuergenMang/f5-irules-jwt/blob/main/jwt-validate448Views4likes2CommentsPython script to test if a F5 BIG-IP is vulnerable to cve-2023-46747
Code is community submitted, community supported, and recognized as ‘Use At Your Own Risk’. Short Description Python script to test if a F5 BIG-IP is vulnerable to cve-2023-46747 Problem solved by this Code Snippet This script can help to determine if a F5 BIG-IP is vulernable toK000137353: BIG-IP Configuration utility unauthenticated remote code execution vulnerability CVE-2023-46747. How to use this Code Snippet Download the script and run it with Python 3. This script takes as input the F5 BIG-IP management IP-adres. $ ./test_cve-2023-46747.py 10.23.92.6 Connecting to 10.23.92.6:443. Connected to 10.23.92.6:443. Send HTTP-request trying to add an account. Try to login with new account. Login successful. Server 10.23.92.6 is vulnerable to CVE-2023-46747. $ Code Snippet Meta Information Version: 0.1 Coding Language: Python Full Code Snippet https://github.com/nvansluis/test_cve-2023-46747888Views4likes2Comments