├── LICENSE ├── README.md ├── devcentral.articles ├── 7 Steps Checklist before upgrading your F5 BIG-IP.devcentral ├── APM - Track clicks on webtop resources.devcentral ├── APM Full Step Up Authentication.devcentral ├── Digest based Single-Sign-On.devcentral ├── DoS and NTLM Brute force protection for HTTP(s) flow.devcentral ├── DoS and NTLM Brute force protection for SIP flow.devcentral ├── Form Based Authentication with external SOAP web services.devcentral ├── Go library to manage BIG-IP iControl REST API.devcentral ├── Google Analytics script injection.devcentral ├── Log click on webtop resources.devcentral ├── Mitigate Intel AMT vulnerability - CVE-2017-5689.devcentral ├── Provision IOS profile for Exchange ActiveSync with client certificate authentication.devcentral ├── Sanitize special characters in AD groups names.devcentral ├── Syncing local repositories and ifiles using iControl REST API.devcentral ├── [icall] kill oldest sessions when reaching xx% of the APM license limit.devcentral └── iRule Event Order.devcentral ├── icall └── [icall] kill oldest sessions when reaching xx% of the APM license limit.tcl └── irules ├── APM Full Step Up Authentication.tcl ├── DoS and NTLM Brute force protection for HTTP(s) flow.tcl ├── DoS and NTLM Brute force protection for SIP flow.tcl ├── Geolocation enforcement and Googlebot whitelisting.tcl ├── Mitigate TokenChpoken attack on PeopleSoft.tcl ├── POST preservation in Multidomain SSO.tcl ├── Provision IOS profile for Exchange ActiveSync with client certificate authentication.zip ├── Rate Limiting based on ACCESS TOKEN.tcl ├── Registration service for OAuth 2.0 client Apps.tcl ├── Sanitize special characters in AD groups names.tcl ├── Simple SAML IDP Selector.tcl └── Sliding session for FedAuth Persistent cookie delivered by ADFS.tcl /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 e-Xpert Solutions SA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /devcentral.articles/7 Steps Checklist before upgrading your F5 BIG-IP.devcentral: -------------------------------------------------------------------------------- 1 | In this example, I will assume that we are upgrading from 11.5.1 to 12.1.2 2 | 3 | **Step 1 : Check the compatibility matrix** 4 | 5 | > a) For appliance, check hardware/software compatibility 6 | > 7 | > Link: [https://support.f5.com/csp/article/K9476](https://support.f5.com/csp/article/K9476) 8 | > 9 | > b) For virtual edition, check the supported hypervisors matrix 10 | > 11 | > Link : [https://support.f5.com/kb/en-us/products/big-ip_ltm/manuals/product/ve-supported-hypervisor-matrix.html](https://support.f5.com/kb/en-us/products/big-ip_ltm/manuals/product/ve-supported-hypervisor-matrix.html) 12 | > 13 | > Note : If running vCMP systems, verify also the vCMP host and compatible guest version matrix 14 | > 15 | > Link : [https://support.f5.com/csp/article/K14088](https://support.f5.com/csp/article/K14088) 16 | 17 | **Step 2 : Check supported BIG-IP upgrade paths and determine if you can upgrade directly** 18 | 19 | > Link: [https://support.f5.com/csp/article/K13845](https://support.f5.com/csp/article/K13845) 20 | > 21 | > In this case, you must be running BIG-IP 10.1.x - 11.x to upgrade directly to BIG-IP 12.x 22 | 23 | **Step 3 : Download .iso files needed for the upgrade from F5 Downloads** 24 | 25 | > Link: [https://downloads.f5.com/esd/index.jsp](https://downloads.f5.com/esd/index.jsp) 26 | 27 | **Step 4 : Check if you need to re-activate the license before upgrading** 28 | 29 | > Link: [https://support.f5.com/csp/article/K7727](https://support.f5.com/csp/article/K7727) 30 | > 31 | > 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 : 32 | > 33 | > `> grep "Service check date" /config/bigip.license` 34 | > 35 | > The output appears similar to the following example: 36 | > 37 | > `> Service check date : 20151008` 38 | > 39 | > As the "Service check date" (20151008) is earlier 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. 40 | 41 | **Step 5 : Use "iHealth Upgrade Advisor" to determine if any configuration modification is needed before/after the upgrade** 42 | 43 | > Link: [https://ihealth.f5.com/](https://ihealth.f5.com/) 44 | > 45 | > Generate a qkview and then upload it to iHealth (link above). 46 | > 47 | > For more info about how to generate a qkview read following article 48 | > 49 | > Link: [https://support.f5.com/csp/article/K12878](https://support.f5.com/csp/article/K12878) 50 | > 51 | > For more info about "iHealth Upgrade Advisor" read following article 52 | > 53 | > Link: [https://devcentral.f5.com/articles/ihealth-upgrade-advisor-making-upgrades-a-little-easier-20001](https://devcentral.f5.com/articles/ihealth-upgrade-advisor-making-upgrades-a-little-easier-20001) 54 | 55 | **Step 6 : Backup the configuration by generating a UCS archive and download it on a safe place** 56 | 57 | > Link: [https://support.f5.com/csp/article/K13132](https://support.f5.com/csp/article/K13132) 58 | > 59 | > a) If are using the "Configuration Utility", follow the procedure under "Backing up configuration data by using the Configuration utility" 60 | > 61 | > b) If you prefer using CLI, follow the procedure under "Backing up configuration data using the tmsh utility" 62 | 63 | **Step 7 : From the release note of the version you wish to install read the "Installation checklist"** 64 | 65 | > Link: [https://support.f5.com/kb/en-us/products/big-ip_ltm/releasenotes/product/relnote-ltm-12-1-2.html](https://support.f5.com/kb/en-us/products/big-ip_ltm/releasenotes/product/relnote-ltm-12-1-2.html) 66 | > 67 | > Under the paragraph "Installation checklist" of the release note, ensure that you have read and verified listed points. 68 | -------------------------------------------------------------------------------- /devcentral.articles/APM - Track clicks on webtop resources.devcentral: -------------------------------------------------------------------------------- 1 | Add a piece of javascript in the Advanced Customization of your Access profile. You can include the code at the end of the hometab.inc javascript file. 2 | 3 | window.onclick = function(e) { 4 | if (e.target.parentNode.className == "favorite") { 5 | var xhttp = new XMLHttpRequest(); 6 | console.log(e.target.parentNode.id); 7 | var uri = "/analytics?t=" + Math.random() + "&r=" + encodeURIComponent(window.btoa(e.target.parentNode.id)) + "&d=" + Date.now(); 8 | xhttp.open("GET", uri, true); 9 | xhttp.send(); 10 | } 11 | }; 12 | 13 | Then, add the irule provided in the attachment of this article to the Virtual Server with the access profile. 14 | 15 | Here is an example of logged accesses : 16 | 17 | time=08/14/17 20:19:24, clientip=10.20.30.5, user=test, session=a7585749, res=/Common/fullvpn-test 18 | time=08/14/17 20:19:22, clientip=10.20.30.5, user=test, session=a7585749, res=/Common/debian-test 19 | -------------------------------------------------------------------------------- /devcentral.articles/APM Full Step Up Authentication.devcentral: -------------------------------------------------------------------------------- 1 | ### Installation ### 2 | 3 | #### ***irule*** #### 4 | 5 | To make it works, you need to install the irule on the Virtual Server that publish your application with APM authentication. 6 | 7 | #### ***datagroup*** #### 8 | 9 | You need to create a datagroup of string type. This dg must contains http path that need an additional authentication step. The dg is named loa3_uri in the irule example. 10 | 11 | #### ***access profile*** #### 12 | 13 | If you already have an existing access profile, you will need to modify it and include some additionnal configuration in your VPE. If you have no access profile, you can starts building your own based on the description we provide below. 14 | 15 | ### Scenarios ### 16 | 17 | #### ***1) User try to reach strong uri after first authentication process*** #### 18 | 19 | In this scenario, the user first authenticate using a standard authentication mecanism. Once authenticated, if the user request content that is behing strong uris, the user restart an authentication process in the "Strong Auth" and "Already Auth" branch of the VPE. 20 | 21 | #### ***2) User try to reach strong uri during the first authentication process*** #### 22 | 23 | If the user try to access a strong uri on its first attempt, he will need to complete the full authentication process. Then, he can access every part of the web application without any additional prompt. 24 | 25 | ### Special considerations ### 26 | 27 | #### ***Client certificate Authentication*** #### 28 | 29 | You may need to use Client certificate authentication as a primary factor or second factor. We highly recommend to use "SSl on-demand authentication" if you need it as primary factor. 30 | Client Certificate is not supported as a second factor, you need to use SSl on-demand authentication. 31 | 32 | #### ***WebSSO*** #### 33 | 34 | When first authentication has already been allowed and the user try to access a protected uri, the system will invite the user to complete the new authentication (second factor). This process will restart a webSSO action on the backend. Basic, NTLM and Kerberos webSSO have been tested with success. 35 | 36 | ### Configuring the Visual Policy Editor ### 37 | 38 | The printscreen below is a minimal Visual Policy Editor used to make Step up Authentication works properly : 39 | 40 | ![VPE Printscreen](/Portals/0/Users/136/00/113800/printscreen.png) 41 | 42 | #### ***Strong Auth*** #### 43 | 44 | The strong Auth block is an "Empty Action" with two branch. 45 | 46 | The branch named "Strong" contains the following condition : `expr { [mcget {session.server.landinguri}] starts_with "/strong" || [mcget {session.custom.last.strong}] == 1 }` 47 | 48 | We check that the uri starts with strong (used in scenario 1) or if a custom variable is set to 1 (second scenario) 49 | 50 | #### ***Already Auth*** #### 51 | This is an empty action with two branch. The branch named "yes" contains the following expression : `expr { [mcget {session.custom.last.authresult}] contains "true" }` 52 | 53 | #### ***2-factor Ending*** #### 54 | 55 | `session.custom.last.authtype` variable must be set to 1 56 | 57 | `session.policy.result.redirect.url` must be changed. The `session.server.landinguri` contains the true origin uri. 58 | 59 | To set this variable, you must use the tcl script below : 60 | 61 | proc urldecode str { 62 | variable map 63 | variable alphanumeric a-zA-Z0-9 64 | for {set i 0} {$i <= 256} {incr i} { set c [format %c $i] 65 | if {![string match \[$alphanumeric\] $c]} { 66 | set map($c) %[format %.2x $i] 67 | } 68 | } 69 | array set map { " " + \n %0d%0a } 70 | set str [string map [list + { } "\\" "\\\\"] $str] 71 | regsub -all -- {%([A-Fa-f0-9][A-Fa-f0-9])} $str {\\u00\1} str 72 | return [subst -novar -nocommand $str] 73 | } 74 | set decoded_uri [urldecode [string range [mcget {session.server.landinguri}] [expr { [string last = [mcget {session.server.landinguri}]] + 1 }] end]] 75 | return $decoded_uri 76 | 77 | #### ***Full strong Ending*** #### 78 | 79 | `session.custom.last.authtype` variable must be set to 1 80 | 81 | #### ***Standard Ending*** #### 82 | 83 | `session.custom.last.authtype` variable must be set to 0 84 | 85 | ### Session variables ### 86 | 87 | The following variables can be used in the 2-factor section of the Visual Policy Editor : 88 | 89 | * session.custom.last.username 90 | * session.custom.last.password 91 | 92 | ### Features ### 93 | 94 | * 2-step authentication 95 | * Retrieve username and password from first authentication 96 | * Encrypt Session1 cookie to avoid session Hijacking 97 | 98 | ### External links ### 99 | 100 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) -------------------------------------------------------------------------------- /devcentral.articles/Digest based Single-Sign-On.devcentral: -------------------------------------------------------------------------------- 1 | # Proof of Concept 2 | 3 | ### Generate a valid Digest response 4 | 5 | when hitting "/test", the irule build the Digest response and log it to the ltm log file. 6 | 7 | when RULE_INIT { 8 | set static::nonce "MDgvMjUvMjAxNyAwOToyNTo0Nw" 9 | set static::user "testuser" 10 | set static::password "testpass" 11 | set static::realm "testrealm" 12 | set static::method "GET" 13 | set static::uri "/testuri" 14 | set static::client_nonce "389db6597243daf2" 15 | set static::nonce_count "00000001" 16 | } 17 | 18 | when HTTP_REQUEST { 19 | 20 | # working test 21 | 22 | if { [HTTP::uri] eq "/test" } { 23 | binary scan [md5 "$static::user:$static::realm:$static::password"] H* ha1 24 | log local0. "HA1 = $ha1" 25 | 26 | binary scan [md5 "$static::method:$static::uri"] H* ha2 27 | 28 | log local0. "HA2 = $ha2" 29 | 30 | binary scan [md5 "$ha1:$static::nonce:$static::nonce_count:$static::client_nonce:auth:$ha2"] H* response 31 | log local0. "response = $response" 32 | 33 | } 34 | } 35 | 36 | 37 | ### Play Digest SSO when receiving a 401 response from the backend 38 | 39 | *note : Client Nonce is currently a static variable. Must be generated within the irule instead.* 40 | 41 | when RULE_INIT { 42 | set static::user "testuser" 43 | set static::password "testpass" 44 | set static::client_nonce "389db6597243daf2" 45 | set static::nonce_count "00000001" 46 | } 47 | 48 | when HTTP_REQUEST { 49 | 50 | # set vars required for Digest SSO 51 | 52 | set uri [HTTP::uri] 53 | set method [HTTP::method] 54 | set retried 0 55 | 56 | # insert a dummy text. Help to inject Digest SSO 57 | 58 | HTTP::header replace Authorization "irule-test-digest-sso" 59 | set request [HTTP::request] 60 | HTTP::header remove Authorization 61 | 62 | } 63 | 64 | when HTTP_RESPONSE { 65 | if { [HTTP::status] contains "401" and [HTTP::header exists "WWW-Authenticate"] and [HTTP::header "WWW-Authenticate"] contains "Digest" and $retried == 0 } { 66 | 67 | set www_auth [HTTP::header "WWW-Authenticate"] 68 | 69 | set fields [split $www_auth ","] 70 | 71 | set realm [lindex [split [lindex $fields 0] "="] 1] 72 | set nonce [lindex [split [lindex $fields 1] "="] 1] 73 | 74 | # retrieve username and password from wherever you want. Can be APM, Basic authentication, ... 75 | binary scan [md5 "$static::user:$realm:$static::password"] H* ha1 76 | 77 | binary scan [md5 "$method:$uri"] H* ha2 78 | 79 | binary scan [md5 "$ha1:$nonce:$static::nonce_count:$static::client_nonce:auth:$ha2"] H* response 80 | 81 | set retried 1 82 | 83 | set auth_value "Digest username=\"$static::user\", realm=\"$realm\", nonce=\"$nonce\", uri=\"$uri\", algorithm=MD5, response=\"$response\", opaque=\"0000000000000000\", qop=auth, nc=$static::nonce_count, cnonce=\"$static::client_nonce\"" 84 | 85 | # insert Authorization header with Digest 86 | set updated_request [string map "$find $auth_value" $request] 87 | 88 | # resend the request with the Authorization header filled 89 | HTTP::retry $updated_request 90 | 91 | } else { 92 | set retried 0 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /devcentral.articles/DoS and NTLM Brute force protection for HTTP(s) flow.devcentral: -------------------------------------------------------------------------------- 1 | This irule should be installed on each Virtual Server that require NTLM protection. In a Microsoft Skype for Business deployment, you may need to protect following services : 2 | 3 | * Web Services 4 | * Conf 5 | * Autodiscover 6 | * Exchange Web Services 7 | 8 | TO BE DONE : Provide a way to unlock blocked users 9 | 10 | ### External links ### 11 | 12 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) 13 | 14 | ### Related Articles ### 15 | 16 | I published the SIP irule here: [DoS and NTLM Brute force protection for SIP flow](https://devcentral.f5.com/codeshare/dos-and-ntlm-brute-force-protection-for-sip-flow) -------------------------------------------------------------------------------- /devcentral.articles/DoS and NTLM Brute force protection for SIP flow.devcentral: -------------------------------------------------------------------------------- 1 | This snippet should be applied on the Virtual Server that handle SIP/TLS traffic. SSL bridging is required to make this irule work properly. Moreover, Skype for Business may require that you change your cipher suite to a weak one. 2 | 3 | The internal domain should be define in "`email_domain`" and "`user_domain`" variables. The script will focus on those domains. 4 | 5 | The max attempts before blocking is defined in the "`max_failures`" variable. This setting should be under the max attempts allowed on the Active Directory. 6 | 7 | The blocking duration is configured in the "`block_duration`" variable. (in seconds) 8 | 9 | The "`fail_memory`" variable define the window that we increase the attempt counter. After reaching the end of this duration, the entry is removed until a new invalid attempt occurs. 10 | 11 | ### External links ### 12 | 13 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) 14 | 15 | ### Related Articles ### 16 | 17 | - [DoS and NTLM Brute force protection for HTTP(s) flow](https://devcentral.f5.com/codeshare/dos-and-ntlm-brute-force-protection-for-https-flow-904) -------------------------------------------------------------------------------- /devcentral.articles/Form Based Authentication with external SOAP web services.devcentral: -------------------------------------------------------------------------------- 1 | ### Installation ### 2 | 3 | #### ***Files*** #### 4 | 5 | You need to upload an html login page using ifiles. 6 | 7 | You need to upload the SOAP body of the external web service using ifiles. 8 | 9 | #### ***irule*** #### 10 | 11 | You need to install the irule on your Virtual Server you need to protect. 12 | 13 | #### ***Variables*** #### 14 | 15 | set static::holdtime 3600 # session timeout 16 | set static::login_url "/login" # login url 17 | set static::sideband_vs "VS_EXTERNAL_AUTH_PROVIDER" # Virtual Server that publish the web service 18 | 19 | ### Features ### 20 | 21 | #### ***Version 1.0*** #### 22 | 23 | * Form based login (provided by a custom ifile) 24 | * Authentication against an external SOAP web service 25 | * Manage Session timeout 26 | 27 | #### ***Backlog*** #### 28 | 29 | * Improve logging 30 | * Allow 2-factor authentication (Challenge) 31 | * Encrypt Session cookie 32 | * Provide internal mecanism to generate a session cookie 33 | * accept Basic Authentication 34 | 35 | ### External links ### 36 | 37 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) 38 | -------------------------------------------------------------------------------- /devcentral.articles/Go library to manage BIG-IP iControl REST API.devcentral: -------------------------------------------------------------------------------- 1 | f5-rest-client implements a REST client to query the F5 BIG-IP iControl REST API. 2 | 3 | ### Installation 4 | 5 | `go get -u github.com/e-XpertSolutions/f5-rest-client/f5` 6 | 7 | ### Available authentication methods 8 | 9 | #### Basic authentication 10 | 11 | f5Client, err := f5.NewBasicClient(base_url, username, password) 12 | 13 | 14 | #### Token based authentication 15 | 16 | f5Client, err := f5.NewTokenClient(base_url, username, password, login_provider_name, skip_ssl_verification) 17 | 18 | 19 | ### Usage 20 | 21 | // Copyright 2017 e-Xpert Solutions SA. All rights reserved. 22 | // Use of this source code is governed by a BSD-style 23 | // license that can be found in the LICENSE file. 24 | 25 | package main 26 | 27 | import ( 28 | "encoding/json" 29 | "log" 30 | 31 | "github.com/e-XpertSolutions/f5-rest-client/f5" 32 | "github.com/e-XpertSolutions/f5-rest-client/f5/net" 33 | ) 34 | 35 | func sexyPrint(label string, a interface{}) { 36 | j, err := json.MarshalIndent(a, "", " ") 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | log.Print("DEBUG ", label, ":\n", string(j)) 41 | } 42 | 43 | func main() { 44 | // 1) Basic Authentication 45 | f5Client, err := f5.NewBasicClient("https://127.0.0.1", "admin", "admin") 46 | 47 | // 2) Token Based Authentication 48 | // f5Client, err := f5.NewTokenClient("https://127.0.0.1", "admin", "admin", "tmos", true) 49 | 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | f5Client.DisableCertCheck() 54 | netClient := net.New(f5Client) 55 | self, err := netClient.Self().ListAll() 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | sexyPrint("SelfIP List:", self) 60 | } 61 | 62 | 63 | ### FEATURES 64 | 65 | * Basic authentication 66 | * Token based authentication 67 | * Manage Virtual Server, pool, node, irules, monitors 68 | * Manage Cluster Management 69 | * Manage interfaces, vlan, trunk, self ip, route, route domains 70 | * Manage virtualization features (/vcmp) 71 | * Manage system related stuffs 72 | * Add Helper functions to enable, disable or force a node offline 73 | * Add Helper functions to enable or disable a Virtual Server 74 | * [new] List expiring certificates 75 | * [new] List expired certificates 76 | * [new] Transaction support 77 | 78 | ### ROADMAP 79 | 80 | * Add support for authentication through external providers 81 | * Manage access policies (/apm) 82 | * Manage security (/security) 83 | * Manage DNS and global load balancing servers (/gtm) 84 | * Manage analytics configuration (/analytics) 85 | * Add support for results pagination 86 | * Add support for API versioning 87 | * Add support for Stats retrieval on node, pool, virtual and profiles 88 | * Add support for new API endpoints coming in [v13](https://devcentral.f5.com/wiki/iControlREST.New-In-Version-13-0-0.ashx) 89 | 90 | ### Examples 91 | 92 | ##### Transactions - Create a simple HTTP service 93 | 94 | f5Client, err := f5.NewBasicClient("https://127.0.0.1", "admin", "admin") 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | f5Client.DisableCertCheck() 99 | 100 | // Start new transaction. 101 | tx, err := f5Client.Begin() 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | ltmClient := ltm.New(tx) 107 | 108 | // Create a HTTP monitor 109 | log.Print("Create a HTTP monitor") 110 | 111 | monitorConfig := ltm.MonitorHTTPConfig{ 112 | Name: "http_monitor_" + tx.TransactionID(), 113 | Send: "GET / HTTP/1.0\r\n\r\n", 114 | Recv: "Hello", 115 | } 116 | 117 | if err := ltmClient.MonitorHTTP().Create(monitorConfig); err != nil { 118 | log.Fatal(err) 119 | } 120 | 121 | // Create a Pool 122 | log.Print("Create a pool") 123 | 124 | poolConfig := ltm.PoolConfig{ 125 | Name: "pool_" + tx.TransactionID(), 126 | Monitor: "/Common/http_monitor_" + tx.TransactionID(), 127 | Members: []string{"10.1.10.10:80", "10.1.10.11:80"}, 128 | } 129 | 130 | if err := ltmClient.Pool().Create(poolConfig); err != nil { 131 | log.Fatal(err) 132 | } 133 | 134 | // Create a Virtual Server 135 | log.Print("Create a Virtual Server") 136 | 137 | vsConfig := ltm.VirtualServerConfig{ 138 | Name: "vs_http_" + tx.TransactionID(), 139 | Destination: "10.1.20.130:80", 140 | IPProtocol: "tcp", 141 | Pool: "pool_" + tx.TransactionID(), 142 | SourceAddressTranslation: ltm.SourceAddressTranslation{ 143 | Type: "automap", 144 | }, 145 | Profiles: []ltm.Profile{ 146 | { 147 | Name: "tcp-mobile-optimized", 148 | Context: "all", 149 | }, 150 | { 151 | Name: "http", 152 | }, 153 | }, 154 | } 155 | 156 | if err := ltmClient.Virtual().Create(vsConfig); err != nil { 157 | log.Fatal(err) 158 | } 159 | 160 | // Commit to make the changes persistent. 161 | if err := tx.Commit(); err != nil { 162 | log.Fatal(err) 163 | } 164 | 165 | 166 | ##### List SSL Certificates 167 | 168 | sysClient := sys.New(f5Client) 169 | certs, err := sysClient.FileSSLCert().ListAll() 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | sexyPrint("Certificates", certs) 174 | 175 | 176 | ##### List expired SSL Certificates 177 | 178 | sysClient := sys.New(f5Client) 179 | 180 | certs, err := sysClient.FileSSLCert().ListExpired() 181 | if err != nil { 182 | log.Fatal(err) 183 | } 184 | sexyPrint("Expired Certificates", certs) 185 | 186 | 187 | ##### List expiring SSL Certificates 188 | 189 | sysClient := sys.New(f5Client) 190 | 191 | // ListExpiring(number_of_seconds) 192 | certs, err := sysClient.FileSSLCert().ListExpiring(60 * 60 * 24 * 15) 193 | if err != nil { 194 | log.Fatal(err) 195 | } 196 | sexyPrint("Expiring Certificates", certs) 197 | 198 | ### Contributing 199 | 200 | We appreciate any form of contribution (feature request, bug report, pull request, ...). We have no special requirements for Pull Request, just follow the standard [GitHub way](https://help.github.com/articles/using-pull-requests/). 201 | 202 | ### License 203 | 204 | The sources are release under a BSD 3-Clause License. The full terms of that license can be found in LICENSE file of this repository. 205 | -------------------------------------------------------------------------------- /devcentral.articles/Google Analytics script injection.devcentral: -------------------------------------------------------------------------------- 1 | ### Installation ### 2 | 3 | #### ***Files*** #### 4 | 5 | The code below has to be imported as an ifile. 6 | 7 | By default, you must name this ifile google.js but you can change it in the irule if required. 8 | 9 | *Google Analytics code :* 10 | 11 | 12 | 17 | 18 | 19 | 20 | *Piwik javascript code :* 21 | 22 | 23 | 35 | 36 | 37 | #### ***irule*** #### 38 | 39 | You need to install the irule on your Virtual Server. 40 | 41 | #### ***Variables*** #### 42 | 43 | set static::tracking_id "UA-XXXXX-Y" # replace the Google Tracking ID by your own 44 | set static::siteid "UA-XXXXX-Y" # replace the Piwik Site ID by your own 45 | set static::piwik_url "https://www.mypiwik.com/piwik/piwik" # replace the Piwik URL by your own 46 | 47 | ### Features ### 48 | 49 | #### ***Version 1.0*** #### 50 | 51 | * Insert Google Analytics JS code within html response 52 | * support for Piwik JS insertion 53 | * Manage Multiple TrackingID by hostname (see Multiple "hostname and TrackingID section") 54 | 55 | #### ***Backlog*** #### 56 | 57 | * Add logging 58 | 59 | ### External links ### 60 | 61 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) 62 | 63 | 64 | ### BONUS : Multiple hostname and TrackingID ### 65 | 66 | #### ***Prerequisite*** #### 67 | 68 | You need to add a string based Datagroup named HOST_TRACKING_MAPPING. 69 | 70 | ltm data-group internal HOST_TRACKING_MAPPING { 71 | records { 72 | blog.e-xpertsolutions.com { 73 | data UA-XXXXX-Z 74 | } 75 | www.e-xpertsolutions.com { 76 | data UA-XXXXX-Y 77 | } 78 | } 79 | type string 80 | } 81 | 82 | The google.js ifile need to be replaced by the following example : 83 | 84 | 85 | 90 | 91 | 92 | 93 | #### ***Irule*** #### 94 | 95 | when RULE_INIT { 96 | set static::default_trackingid "UA-XXXXX-Y" 97 | } 98 | 99 | when HTTP_REQUEST { 100 | HTTP::header remove "Accept-Encoding" 101 | set host [HTTP::host] 102 | } 103 | 104 | when HTTP_RESPONSE { 105 | if { [HTTP::header Content-Type] contains "text/html" } { 106 | if { [HTTP::header exists "Content-Length"] } { 107 | set content_length [HTTP::header "Content-Length"] 108 | } else { 109 | set content_length 1000000 110 | } 111 | if { $content_length > 0 } { 112 | HTTP::collect $content_length 113 | } 114 | } 115 | } 116 | when HTTP_RESPONSE_DATA { 117 | set search "" 118 | set tracking_id [class match -value -- $host equals HOST_TRACKING_MAPPING] 119 | if { $tracking_id eq "" } { 120 | set tracking_id $static::default_trackingid 121 | } 122 | HTTP::payload replace 0 $content_length [string map [list $search "[subst -nocommands -nobackslashes [ifile get google.js]]"] [HTTP::payload]] 123 | HTTP::release 124 | } 125 | -------------------------------------------------------------------------------- /devcentral.articles/Log click on webtop resources.devcentral: -------------------------------------------------------------------------------- 1 | ### Installation ### 2 | 3 | You just need to put this irule in the Virtual Server configuration that handle your access profile. 4 | 5 | ### Logging information ### 6 | 7 | Clicks are logged in the local0 (ltm logs). You can see below examples : 8 | 9 | virtual=/Common/test, apm=1239853, user=testuser, resource_type=portal, resourcename=owa 10 | virtual=/Common/test, apm=1239853, user=testuser, resource_type=remote_desktop, resourcename=ActiveDirectory 11 | 12 | ### Features ### 13 | 14 | You can currently log the following application types : 15 | 16 | * Portal access 17 | * Remote Desktop access 18 | 19 | The irule provide the additional features : 20 | 21 | * Decode Portal access uri 22 | * logging of username, apm session and resource name 23 | 24 | ### Credits ### 25 | 26 | Inspired from an original irule in a reply from Kevin Stewart : [Logging for Portal Access](https://devcentral.f5.com/questions/logging-for-portal-access) 27 | 28 | ### External links ### 29 | 30 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) 31 | -------------------------------------------------------------------------------- /devcentral.articles/Mitigate Intel AMT vulnerability - CVE-2017-5689.devcentral: -------------------------------------------------------------------------------- 1 | I intentionally developed two code snippets that can be used on a "Layer 4 Any Virtual Server". All traffic must go through this Virtual Server to be able to detect the attempts to exploit the Intel AMT vulnerability. 2 | 3 | With the first code snippet, you can trap all attempts to access Intel AMT web services : 4 | 5 | when CLIENT_ACCEPTED { 6 | switch [TCP::remote_port] { 7 | "16992" - 8 | "16993" - 9 | "1699" - 10 | "16995" - 11 | "623" - 12 | "664" { 13 | log local0. "Intel AMT access attempt made by [IP::client_addr]" 14 | discard 15 | return 16 | } 17 | } 18 | } 19 | 20 | 21 | Basically, in this scenario, we are looking for attempts to connect on specific ports used by Intel AMT. But in the other hand, this check is not enough, so I decided to add the second code snippet : 22 | 23 | when SERVER_CONNECTED { 24 | TCP::collect 25 | } 26 | when SERVER_DATA { 27 | set payload [TCP::payload] 28 | if { $payload starts_with "HTTP" and $payload contains "Server: AMT" } { 29 | log local0. "Intel AMT access attempt made by [IP::client_addr]" 30 | discard 31 | return 32 | } 33 | } 34 | 35 | 36 | The main pain point regarding This irule is performance issues. I do not had the opportunity to test it, but I know that TCP::collect will impact performances. 37 | 38 | Now, I enhance the irule by combining both code snippet like this : 39 | 40 | when CLIENT_ACCEPTED { 41 | set attempt 0 42 | switch [TCP::remote_port] { 43 | "16992" - 44 | "16993" - 45 | "1699" - 46 | "16995" - 47 | "623" - 48 | "664" { 49 | log local0. "Intel AMT access attempt made by [IP::client_addr]" 50 | set attempt 1 51 | } 52 | } 53 | } 54 | 55 | when SERVER_CONNECTED { 56 | if { [info exists attempt] and $attempt } { 57 | TCP::collect 58 | } 59 | } 60 | when SERVER_DATA { 61 | set payload [TCP::payload] 62 | if { $payload starts_with "HTTP" and $payload contains "Server: AMT" } { 63 | log local0. "Intel AMT access attempt made by [IP::client_addr]" 64 | discard 65 | return 66 | } 67 | } 68 | 69 | This way, I'm able to activate the TCP collection only when I have a suspicious connection attempt. 70 | -------------------------------------------------------------------------------- /devcentral.articles/Provision IOS profile for Exchange ActiveSync with client certificate authentication.devcentral: -------------------------------------------------------------------------------- 1 | You need to define a Virtual Server and an access profile to publish ActiveSync. Then, you need to assign the irule on the Virtual Server. 2 | 3 | The certificate is retrieved using SCEP protocol on a Microsoft ADCS 2012 R2. The SCEP url should be changed in the Exchange payload. 4 | 5 | We configured APM to protect the access to this service and retrieve attributes from Active Directory but you can change the irule code to retrieve information and protect the service in a different manner. 6 | 7 | When a user reach /enroll uri with Safari browser, the provisioning process starts. 8 | 9 | /!\ I provide an IOS payload as example, but you need to modify it to fit your environment and save it as an ifile. 10 | 11 | Settings that need to be changed in the xml payload : 12 | 13 | * `HOST.DOMAIN.COM` : Activesync FQDN 14 | * `DOMAIN-Issuer-CA` : Issuing CA Name (if exists otherwise related code should be removed) 15 | * `CERTIFICATE` : X.509 certificate in Base64 for Issuing CA 16 | * `DOMAIN-Root-CA` : Root CA Name 17 | * `CERTIFICATE` : X.509 certificate in Base64 for the Root CA 18 | * `DOMAIN` : Organization name to be present in the user certificate 19 | * `http://scep.domain.com/scep` : SCEP url 20 | 21 | ### External links ### 22 | 23 | Github : [github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) -------------------------------------------------------------------------------- /devcentral.articles/Sanitize special characters in AD groups names.devcentral: -------------------------------------------------------------------------------- 1 | ################################## 2 | # DevCentral link: 3 | ################################## 4 | 5 | https://devcentral.f5.com/codeshare/sanitize-special-characters-in-ad-groups-names-1040 6 | 7 | ################################## 8 | # Problem this snippet solves: 9 | ################################## 10 | 11 | With APM, when you query Active Directory to retrieve the groups membership, if an AD group contains one or several special characters, the name of the group is considered not printable by APM and therefore is transformed in hex format. 12 | 13 | For example, if the name of an AD group is **"Comptes_éditeurs"** (in french), the APM session variable after AD query will be **"session.ad.last.attr.memberOf = 0x436f6d707465735fc3a964697465757273"**. This is not convenient for usage in the APM policy. 14 | 15 | This snippet offers an iRule to transform "not printable" group names into printable group names by replacing all not printable chars by printable ones. 16 | Indeed, the previous example **"Comptes_éditeurs"** will be transformed by this snippet into **"Comptes_editeurs"**, which will be printed properly and can be used as usual in an APM policy. 17 | 18 | 19 | ################################## 20 | # How to use this snippet: 21 | ################################## 22 | 23 | # Installation # 24 | 25 | ### *irule* ### 26 | To make it works, you need to install the irule on the Virtual Server that publish your application with APM authentication. 27 | 28 | ### *datagroup* ### 29 | 30 | You need to create a strings datagroup named **"dg_special_chars"** that contains all the not printable chars you want to replace with their replacement char. The following datagroup will replace **"é, è, ê, ë"** with the normal **"e"** : 31 | 32 | **c3a8 : 65** (è => e) 33 | **c3a9 : 65** (é => e) 34 | **c3aa : 65** (ê => e) 35 | **c3ab : 65** (ë => e) 36 | 37 | The original special chars here (keys in the datagroup) are in **hex format of UTF-8**. You can have a look here [http://www.utf8-chartable.de/](http://www.utf8-chartable.de/) to find them. 38 | 39 | The replacement chars (values in the datagroup) are in **hex format of standard ASCII**. You can have a look here in the "ASCII printable characters" table [http://www.rapidtables.com/code/text/ascii-table.htm](http://www.rapidtables.com/code/text/ascii-table.htm). 40 | 41 | For example, if you need to replace "£" with "?", you need the following entry in your datagroup : 42 | 43 | c2a3 : 3f 44 | 45 | 46 | ### *APM Policy* ### 47 | 48 | In your APM policy you need to add a bloc **"iRule Event"** right after you call AD Query and before you test groups membership. In the "iRule Event" bloc, the "Custom iRule Event Agent" needs to be **"clean_group_names"**. 49 | 50 | After this iRule Event, the sanitized groups names will be stored in the APM session variable **"session.custom.ad.memberOf"**. 51 | 52 | To test groups membership, you can use the following condition in an "Empty" bloc : 53 | **expr { [mcget {session.custom.ad.memberOf}] contains "CN=MY_GROUP, CN=Users, DC=MY_DOMAIN" }** 54 | 55 | 56 | -------------------------------------------------------------------------------- /devcentral.articles/Syncing local repositories and ifiles using iControl REST API.devcentral: -------------------------------------------------------------------------------- 1 | I often use the ifile feature to provide customized web content to users targeting my web applications through a BIG-IP device. I already had a request to import 1000+ files to the F5 BIG-IP in order to build a complete web framework full of .css, .js and .html file extensions. 2 | 3 | Uploading those files one by one is really time-consuming and boring. That's why we have developed a small piece of code that automatically watch a folder and create, modify or delete ifiles accordingly. 4 | 5 | # Managing ifiles using Curl tool 6 | 7 | When an administrator decides to manually upload few files to the BIG-IP device using iControl REST API, he has to execute several consecutive commands. 8 | 9 | ## Uploading a file to the BIG-IP 10 | 11 | You need to calculate the size of the file you want to upload: 12 | 13 | du -b testfile.txt 14 | 15 | 930 testfile.txt 16 | 17 | 18 | Then, you can upload the file to the BIG-IP device: 19 | 20 | curl -v -k -X POST -H "Content-Type: application/octet-stream" -H "Content-Range: 0-929/930" -u admin:admin --data-binary "@testfile.txt" https://bigip_host/mgmt/shared/file-transfer/uploads/testfile.txt 21 | 22 | ## Creating an ifile (System level) 23 | 24 | curl -v -k -u admin:admin -X PUT -H "Content-Type: application/json" -d '{"name": "testfile.txt", "source-path": "file:/var/config/rest/downloads/testfile.txt"}' https://bigip_host/mgmt/tm/sys/file/ifile/testfile.txt 25 | 26 | ## Creating an ifile object (LTM level) 27 | 28 | curl -v -k -u admin:admin -X POST -H "Content-Type: application/json" -d '{"name":"testfile.txt", "file-name": "testfile.txt"}' https://bigip_host/mgmt/tm/ltm/ifile 29 | 30 | ## Deleting an ifile object (LTM level) 31 | 32 | curl -v -k -u admin:admin -X DELETE https://bigip_host/mgmt/tm/ltm/ifile/testfile.txt 33 | 34 | ## Deleting an ifile (System level) 35 | 36 | curl -v -k -u admin:admin -X DELETE https://bigip_host/mgmt/tm/sys/file/ifile/testfile.txt 37 | 38 | # Automatically synchronize a local folder with your BIG-IP 39 | 40 | If you already had the request to deal with thousand of files to upload as ifiles in a BIG-IP device, you may know that managing this kind of request using Postman or a shell script based on wget or curl commands is a nightmare. This is the reason why f5-auto-uploader tool came alive. 41 | 42 | f5-auto-uploader creates, modify or deletes ifiles automatically based on watched directories. All changes are enclosed in a transaction. 43 | 44 | # Downloading f5-auto-uploader binary file 45 | 46 | You can download the binary file directly from the github project : 47 | 48 | `https://github.com/e-XpertSolutions/f5-auto-uploader/releases` 49 | 50 | The binary file is available for Linux and Windows OS. 51 | 52 | # Installing f5-auto-uploader from source 53 | 54 | Assuming, you have successfully deployed the golang framework in your environment, you can download the f5-auto-uploader project to your computer by typing the following command : 55 | 56 | `go get github.com/e-XpertSolutions/f5-auto-uploader` 57 | 58 | you should be able to find the binary file in the $GOPATH/bin folder. This is a self contained binary file, so you can put it in another Linux system and even the F5 device itself if you want to. 59 | 60 | # Writing a configuration file 61 | 62 | You can find below an example of a toml formatted configuration file you need to provide as argument to the binary : 63 | 64 | [f5] 65 | auth_method = "token" 66 | url = "https://bigip_host" 67 | user = "admin" 68 | password = "admin" 69 | ssl_check = false 70 | login_provider_name = "tmos" 71 | 72 | [[watch]] 73 | directory = "/tmp/test" 74 | exclude = [".*"] 75 | 76 | [[watch]] 77 | directory = "/tmp/test2" 78 | exclude = [".*"] 79 | 80 | 81 | You can define multiple directories to watch at the same time. Every action made on a file in one of those repositories is automatically synchronized with the BIG-IP device. It is possible to exclude specific files using a wildcard path. The example above get rid of hidden files in a Linux OS. 82 | 83 | You can define either Basic or Token based authentication. If you prefer using the Basic authentication method, you can change the configuration file as defined below : 84 | 85 | [f5] 86 | auth_method = "basic" 87 | url = "https://bigip_host" 88 | user = "admin" 89 | password = "admin" 90 | ssl_check = false 91 | 92 | 93 | # Watch remote repositories 94 | 95 | You can watch remote directories as well. If you are running Linux OS, you can mount external file systems using Samba or SSH for example : 96 | 97 | Create the mount point : 98 | 99 | sudo mkdir -p /mnt/sshfs/ssh-folder 100 | 101 | Mount the remote folder : 102 | 103 | sshfs root@hostname:/home/user/ /mnt/sshfs/ssh-folder -C -p 22 104 | 105 | # Running f5-auto-uploader 106 | 107 | You can then run the service using the following command line : 108 | 109 | ./f5-auto-uploader -config /etc/config/config.toml 110 | 111 | 112 | You can display the help by adding the -help argument to the command line : 113 | 114 | ./f5-auto-uploader -help 115 | 116 | usage: f5-auto-uploader 117 | -config string 118 | path to configuration file (default "config.toml") 119 | -verbose 120 | enable verbose mode 121 | -version 122 | print current version and exit 123 | 124 | # Support 125 | 126 | This project is available on [github](https://github.com/e-XpertSolutions/f5-auto-uploader). This project rely on the [f5-rest-client](https://github.com/e-XpertSolutions/f5-rest-client) library we developed to integrate easily the iControl REST API in a golang project. 127 | -------------------------------------------------------------------------------- /devcentral.articles/[icall] kill oldest sessions when reaching xx% of the APM license limit.devcentral: -------------------------------------------------------------------------------- 1 | ### TMSH command to create an icall script 2 | 3 | `create sys icall script apm_purge_sessions` 4 | 5 | Then copy/paste the content of the icall script and save it. By default, the command create a script named "apm_purge_sessions". You can easily change the name of the script by modifying "apm_purge_sessions" in the command line. 6 | 7 | ### TMSH command to create the icall handler 8 | 9 | The following command trigger the script every 60 seconds. It can be changed to increase the frequency of the execution of the script. 10 | 11 | `create sys icall handler periodic f5-apm-purge-session interval 60 script apm_purge_sessions` 12 | 13 | ### Interesting tcl commands used in the script 14 | 15 | Retrieve the max_access_session variable in the license of the device: 16 | 17 | `[string trim [lindex [split [exec /usr/bin/tmsh show /sys license detail | grep access] " "] 1] "\[\]"]` 18 | 19 | retrieve the ordered list (oldest first) of active APM sessionIDs 20 | 21 | `catch {set output [exec /usr/bin/sessiondump --allkeys | grep starttime | sort -k3 | cut -c1-8]}` 22 | 23 | ### Use cases 24 | 25 | * kill oldest sessions when reaching xx% of the APM license limit 26 | 27 | ### Evolution 28 | 29 | * trigger the icall script based on a specific event (snmptrap, log, ...) 30 | * sort APM sessions by Access Profile and kill sessions based on the criticity of each AP. 31 | -------------------------------------------------------------------------------- /devcentral.articles/iRule Event Order.devcentral: -------------------------------------------------------------------------------- 1 | ### Installation ### 2 | 3 | #### ***irule*** #### 4 | 5 | You need to install the irule on the top of the list in your Virtual Server. 6 | 7 | ### Special considerations ### 8 | 9 | #### ***Irule Generator*** #### 10 | 11 | the displayed irule has been generated with a proprietary tool developed in golang. 12 | 13 | #### ***Zip file Content*** #### 14 | 15 | The downloadable zip file contains : 16 | 17 | - a shell script that generate the irule code for the list of event specified in event_list.tcl file 18 | - a file that contain a list of all irule event (excluded : CLIENT_ACCEPTED, CLIENT_CLOSED, RULE_INIT) 19 | - an irule containing all possible events (generated by the script). Just for demo, can not be used in real life. 20 | 21 | #### ***Compatibility*** #### 22 | 23 | Supported in v12.1.0 24 | 25 | - ACCESS_PER_REQUEST_AGENT_EVENT 26 | - BOTDEFENSE_ACTION 27 | - BOTDEFENSE_REQUEST 28 | - WS_CLIENT_DATA 29 | - WS_CLIENT_FRAME 30 | - WS_CLIENT_FRAME_DONE 31 | - WS_REQUEST 32 | - WS_RESPONSE 33 | - WS_SERVER_DATA 34 | - WS_SERVER_FRAME 35 | - WS_SERVER_FRAME_DONE 36 | 37 | #### ***Irule Events*** #### 38 | 39 | The displayed irule currently support events from CLIENT_*, HTTP_*, LB_* and SSL_*. This irule should fit for most of the standard ltm use cases on http and https. 40 | 41 | ### ***Outputs*** ### 42 | 43 | syslog output : 44 | 45 | virtual=/Common/vs_test_irule_order_event, id=d5f693399, time=0, event_order=0, event_type=CLIENT_ACCEPTED 46 | virtual=/Common/vs_test_irule_order_event, id=d5f693399, time=0, event_order=1, event_type=CLIENTSSL_CLIENTHELLO 47 | virtual=/Common/vs_test_irule_order_event, id=d5f693399, time=6, event_order=2, event_type=CLIENTSSL_HANDSHAKE 48 | virtual=/Common/vs_test_irule_order_event, id=d5f693399, time=6, event_order=3, event_type=HTTP_REQUEST 49 | virtual=/Common/vs_test_irule_order_event, id=d5f693399, time=6, event_order=4, event_type=LB_FAILED 50 | virtual=/Common/vs_test_irule_order_event, id=d5f693399, time=6, event_order=5, event_type=CLIENT_CLOSED 51 | ` 52 | 53 | JSON output : 54 | 55 | { 56 |    "d5f693399": [ 57 |       { 58 |          "virtual": "/Common/vs_test_irule_order_event", 59 |          "id": "d5f693399", 60 |          "time": "0", 61 |          "event_order": "0", 62 |          "event_type": "CLIENT_ACCEPTED" 63 |       }, 64 |       { 65 |          "virtual": "/Common/vs_test_irule_order_event", 66 |          "id": "d5f693399", 67 |          "time": "0", 68 |          "event_order": "1", 69 |          "event_type": "CLIENTSSL_CLIENTHELLO" 70 |       }, 71 |       { 72 |          "virtual": "/Common/vs_test_irule_order_event", 73 |          "id": "d5f693399", 74 |          "time": "6", 75 |          "event_order": "2", 76 |          "event_type": "CLIENTSSL_HANDSHAKE" 77 |       }, 78 |       { 79 |          "virtual": "/Common/vs_test_irule_order_event", 80 |          "id": "d5f693399", 81 |          "time": "6", 82 |          "event_order": "3", 83 |          "event_type": "HTTP_REQUEST" 84 |       }, 85 |       { 86 |          "virtual": "/Common/vs_test_irule_order_event", 87 |          "id": "d5f693399", 88 |          "time": "6", 89 |          "event_order": "4", 90 |          "event_type": "LB_FAILED" 91 |       }, 92 |       { 93 |          "virtual": "/Common/vs_test_irule_order_event", 94 |          "id": "d5f693399", 95 |          "time": "6", 96 |          "event_order": "5", 97 |          "event_type": "CLIENT_CLOSED" 98 |       } 99 |    ] 100 | } 101 | 102 | ### Features ### 103 | 104 | #### ***Version 1.0*** #### 105 | 106 | * List Event order 107 | * Generate a sessionid to track the Client connection until it close 108 | * Display time elapsed since client has been accepted 109 | * Logging in JSON format (to use with third party libraries like d3.js) 110 | 111 | #### ***Backlog*** #### 112 | 113 | * add logging using csv format 114 | * use HSL instead of local logging 115 | * add extra comments for specific events like CLIENT_CERT and LB_FAILED 116 | * add more events (security related events) 117 | * release a script to generate an irule that use events related to the other irules present in a Virtual Server 118 | 119 | ### External links ### 120 | 121 | Github : [https://github.com/e-XpertSolutions/f5](https://github.com/e-XpertSolutions/f5) 122 | 123 | ### iRule Sample ### 124 | 125 | when RULE_INIT { 126 | set static::client_ip "192.168.10.1" 127 | set static::json 1 128 | } 129 | 130 | when CLIENT_ACCEPTED { 131 | set counter 0 132 | set event_type "CLIENT_ACCEPTED" 133 | set sessionid "[IP::client_addr][TCP::client_port][IP::local_addr][TCP::local_port][expr { int(100000000 * rand()) }]" 134 | binary scan [md5 $sessionid] H* md5_string trash 135 | set md5_string [string range $md5_string 12 20] 136 | set start_time [clock clicks -milliseconds] 137 | log local0. "virtual=[virtual], id=$md5_string, time=0, event_order=$counter, event_type=$event_type" 138 | if { $static::json } { 139 | set json_log "\{ \"$md5_string\": \[\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"0\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 140 | } 141 | } 142 | 143 | when CLIENT_CLOSED { 144 | set counter [expr { $counter+1 }] 145 | set event_type "CLIENT_CLOSED" 146 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 147 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 148 | if { $static::json } { 149 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}\]\}" 150 | log local0. "$json_log" 151 | } 152 | } 153 | 154 | when HTTP_REQUEST { 155 | set counter [expr { $counter+1 }] 156 | set event_type "HTTP_REQUEST" 157 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 158 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 159 | if { $static::json } { 160 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 161 | } 162 | } 163 | 164 | when HTTP_REQUEST_RELEASE { 165 | set counter [expr { $counter+1 }] 166 | set event_type "HTTP_REQUEST_RELEASE" 167 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 168 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 169 | if { $static::json } { 170 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 171 | } 172 | } 173 | 174 | when HTTP_RESPONSE { 175 | set counter [expr { $counter+1 }] 176 | set event_type "HTTP_RESPONSE" 177 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 178 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 179 | if { $static::json } { 180 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 181 | } 182 | } 183 | 184 | when HTTP_RESPONSE_RELEASE { 185 | set counter [expr { $counter+1 }] 186 | set event_type "HTTP_RESPONSE_RELEASE" 187 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 188 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 189 | if { $static::json } { 190 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 191 | } 192 | } 193 | 194 | when HTTP_RESPONSE_CONTINUE { 195 | set counter [expr { $counter+1 }] 196 | set event_type "HTTP_RESPONSE_CONTINUE" 197 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 198 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 199 | if { $static::json } { 200 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 201 | } 202 | } 203 | 204 | when HTTP_REQUEST_SEND { 205 | set counter [expr { $counter+1 }] 206 | set event_type "HTTP_REQUEST_SEND" 207 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 208 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 209 | if { $static::json } { 210 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 211 | } 212 | } 213 | 214 | when HTTP_REQUEST_DATA { 215 | set counter [expr { $counter+1 }] 216 | set event_type "HTTP_REQUEST_DATA" 217 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 218 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 219 | if { $static::json } { 220 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 221 | } 222 | } 223 | 224 | when CLIENT_DATA { 225 | set counter [expr { $counter+1 }] 226 | set event_type "CLIENT_DATA" 227 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 228 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 229 | if { $static::json } { 230 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 231 | } 232 | } 233 | 234 | when HTTP_PROXY_REQUEST { 235 | set counter [expr { $counter+1 }] 236 | set event_type "HTTP_PROXY_REQUEST" 237 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 238 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 239 | if { $static::json } { 240 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 241 | } 242 | } 243 | 244 | when HTTP_DISABLED { 245 | set counter [expr { $counter+1 }] 246 | set event_type "HTTP_DISABLED" 247 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 248 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 249 | if { $static::json } { 250 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 251 | } 252 | } 253 | 254 | when SERVER_CLOSED { 255 | set counter [expr { $counter+1 }] 256 | set event_type "SERVER_CLOSED" 257 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 258 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 259 | if { $static::json } { 260 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 261 | } 262 | } 263 | 264 | when SERVER_DATA { 265 | set counter [expr { $counter+1 }] 266 | set event_type "SERVER_DATA" 267 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 268 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 269 | if { $static::json } { 270 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 271 | } 272 | } 273 | 274 | when SERVERSSL_DATA { 275 | set counter [expr { $counter+1 }] 276 | set event_type "SERVERSSL_DATA" 277 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 278 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 279 | if { $static::json } { 280 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 281 | } 282 | } 283 | 284 | when SERVER_CONNECTED { 285 | set counter [expr { $counter+1 }] 286 | set event_type "SERVER_CONNECTED" 287 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 288 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 289 | if { $static::json } { 290 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 291 | } 292 | } 293 | 294 | when CLIENTSSL_DATA { 295 | set counter [expr { $counter+1 }] 296 | set event_type "CLIENTSSL_DATA" 297 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 298 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 299 | if { $static::json } { 300 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 301 | } 302 | } 303 | 304 | when CLIENTSSL_CLIENTCERT { 305 | set counter [expr { $counter+1 }] 306 | set event_type "CLIENTSSL_CLIENTCERT" 307 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 308 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 309 | if { $static::json } { 310 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 311 | } 312 | } 313 | 314 | when CLIENTSSL_CLIENTHELLO { 315 | set counter [expr { $counter+1 }] 316 | set event_type "CLIENTSSL_CLIENTHELLO" 317 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 318 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 319 | if { $static::json } { 320 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 321 | } 322 | } 323 | 324 | when CLIENTSSL_HANDSHAKE { 325 | set counter [expr { $counter+1 }] 326 | set event_type "CLIENTSSL_HANDSHAKE" 327 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 328 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 329 | if { $static::json } { 330 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 331 | } 332 | } 333 | 334 | when CLIENTSSL_SERVERHELLO_SEND { 335 | set counter [expr { $counter+1 }] 336 | set event_type "CLIENTSSL_SERVERHELLO_SEND" 337 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 338 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 339 | if { $static::json } { 340 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 341 | } 342 | } 343 | 344 | when SERVERSSL_CLIENTHELLO_SEND { 345 | set counter [expr { $counter+1 }] 346 | set event_type "SERVERSSL_CLIENTHELLO_SEND" 347 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 348 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 349 | if { $static::json } { 350 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 351 | } 352 | } 353 | 354 | when SERVERSSL_HANDSHAKE { 355 | set counter [expr { $counter+1 }] 356 | set event_type "SERVERSSL_HANDSHAKE" 357 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 358 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 359 | if { $static::json } { 360 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 361 | } 362 | } 363 | 364 | when SERVERSSL_SERVERHELLO { 365 | set counter [expr { $counter+1 }] 366 | set event_type "SERVERSSL_SERVERHELLO" 367 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 368 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 369 | if { $static::json } { 370 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 371 | } 372 | } 373 | 374 | when LB_QUEUED { 375 | set counter [expr { $counter+1 }] 376 | set event_type "LB_QUEUED" 377 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 378 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 379 | if { $static::json } { 380 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 381 | } 382 | } 383 | 384 | when LB_FAILED { 385 | set counter [expr { $counter+1 }] 386 | set event_type "LB_FAILED" 387 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 388 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 389 | if { $static::json } { 390 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 391 | } 392 | } 393 | 394 | when LB_SELECTED { 395 | set counter [expr { $counter+1 }] 396 | set event_type "LB_SELECTED" 397 | set curtime [expr { [clock clicks -milliseconds] - $start_time }] 398 | log local0. "virtual=[virtual], id=$md5_string, time=$curtime, event_order=$counter, event_type=$event_type" 399 | if { $static::json } { 400 | append json_log "\{\"virtual\":\"[virtual]\", \"id\":\"$md5_string\", \"time\":\"$curtime\", \"event_order\":\"$counter\", \"event_type\":\"$event_type\"\}," 401 | } 402 | } -------------------------------------------------------------------------------- /icall/[icall] kill oldest sessions when reaching xx% of the APM license limit.tcl: -------------------------------------------------------------------------------- 1 | # retrieve the ordered list of active APM sessionIDs 2 | catch {set output [exec /usr/bin/sessiondump --allkeys | grep starttime | sort -k3 | cut -c1-8]} 3 | 4 | if {$output != ""} { 5 | # move the output to a list of sessionID 6 | set output [split $output "\n"] 7 | set count [llength $output] 8 | 9 | # determine the max_access_session allowed for the running platform 10 | set max_access [string trim [lindex [split [exec /usr/bin/tmsh show /sys license detail | grep access] " "] 1] "\[\]"] 11 | 12 | # determine acceptable threshold before triggering 13 | set access_threshold [expr round($max_access*0.85)] 14 | set diff [expr $count-$access_threshold] 15 | 16 | # kill oldest APM sessions until reaching 85% of active sessions in the APM device 17 | for {set i 0} {$i < $diff} {incr i} { 18 | catch { [exec /usr/bin/sessiondump --delete [lindex $output $i]] } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /irules/APM Full Step Up Authentication.tcl: -------------------------------------------------------------------------------- 1 | when RULE_INIT { 2 | # to be changed prior to any publishing 3 | set passphrase "hEuoYjmFUpB4PcpO3bUdQtLP4ic7jjm" 4 | } 5 | 6 | when HTTP_REQUEST { 7 | if { [HTTP::cookie exists MRHSession] and [ACCESS::session exists -state_allow -sid [HTTP::cookie MRHSession]] } { 8 | set strong_auth [ACCESS::session data get session.custom.last.authtype] 9 | if { [class match [HTTP::path] starts_with loa3_uri] and $strong_auth == 0 } { 10 | HTTP::cookie encrypt "MRHSession" $passphrase 11 | HTTP::respond 302 noserver "Location" "/strong?return_url=[URI::encode [HTTP::uri]]" "Cache-Control" "no-cache, must-revalidate" Set-Cookie "MRHSession=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "LastMRH_Session=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "Session1=[HTTP::cookie MRHSession];path=/" 12 | } 13 | } 14 | } 15 | 16 | when ACCESS_SESSION_STARTED { 17 | 18 | # decrypt Session1 cookie value 19 | set decrypted [HTTP::cookie decrypt "Session1" $passphrase] 20 | 21 | if { [HTTP::cookie exists Session1] and [ACCESS::session exists -state_allow -sid $decrypted] } { 22 | 23 | ## section : retrieve session variables from the first session 24 | 25 | ACCESS::session data set session.custom.last.username [ACCESS::session data get session.logon.last.username -sid $decrypted] 26 | ACCESS::session data set session.custom.last.password [ACCESS::session data get session.logon.last.password -sid $decrypted] 27 | 28 | ## End section 29 | 30 | ACCESS::session data set session.custom.last.authresult "true" 31 | 32 | # remove the first created session during standard authentication to avoid multiple active sessions 33 | ACCESS::session remove -sid $decrypted 34 | 35 | } elseif { [class match [HTTP::path] starts_with loa3_uri] } { 36 | ACCESS::session data set session.custom.last.strong 1 37 | } 38 | } -------------------------------------------------------------------------------- /irules/DoS and NTLM Brute force protection for HTTP(s) flow.tcl: -------------------------------------------------------------------------------- 1 | when RULE_INIT { 2 | array set NTLMFlags { 3 | unicode 0x00000001 4 | oem 0x00000002 5 | req_target 0x00000004 6 | unknown1 0x00000008 7 | sign 0x00000010 8 | seal 0x00000020 9 | datagram 0x00000040 10 | lmkey 0x00000080 11 | netware 0x00000100 12 | ntlm 0x00000200 13 | unknown2 0x00000400 14 | unknown3 0x00000800 15 | ntlm_domain 0x00001000 16 | ntlm_server 0x00002000 17 | ntlm_share 0x00004000 18 | NTLM2 0x00008000 19 | targetinfo 0x00800000 20 | 128bit 0x20000000 21 | keyexch 0x40000000 22 | 56bit 0x80000000 23 | } 24 | set static::irule_name "irule-ntlm-bruteforce" 25 | set static::email_domain "domain.com" 26 | set static::user_domain "DOMAIN" 27 | set static::log_server "" 28 | set static::log_pri "local0." 29 | set static::fail_tab "NTLMfails" 30 | set static::blacklist_tab "NTLMblackhole" 31 | set static::userfail_tab "NTLMUserfails" 32 | set static::userblacklist_tab "NTLMUserblackhole" 33 | set static::max_failures 5 34 | set static::fail_memory 300 35 | set static::block_duration 300 36 | } 37 | 38 | when CLIENT_ACCEPTED { 39 | 40 | if {[table lookup -subtable $static::blacklist_tab [IP::client_addr]] == 1} { 41 | log $static::log_pri "[virtual] - BLACKHOLED IPADDR [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" 42 | reject 43 | return 44 | } 45 | } 46 | 47 | when HTTP_REQUEST { 48 | 49 | if {[table lookup -subtable $static::blacklist_tab [IP::client_addr]] == 1} { 50 | log $static::log_pri "[virtual] - BLACKHOLED IPADDR [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" 51 | reject 52 | return 53 | } 54 | 55 | if {[HTTP::header Authorization] starts_with "NTLM "} { 56 | set ntlm_msg [ b64decode [split [lindex [HTTP::header Authorization] 1] ] ] 57 | binary scan $ntlm_msg a7ci protocol zero type 58 | if { $type eq 3 } { 59 | binary scan $ntlm_msg @12ssissississississii \ 60 | lmlen lmlen2 lmoff \ 61 | ntlen ntlen2 ntoff \ 62 | dlen dlen2 doff \ 63 | ulen ulen2 uoff \ 64 | hlen hlen2 hoff \ 65 | slen slen2 soff \ 66 | flags 67 | set ntlm_domain {}; binary scan $ntlm_msg @${doff}a${dlen} ntlm_domain 68 | set ntlm_user {}; binary scan $ntlm_msg @${uoff}a${ulen} ntlm_user 69 | set ntlm_host {}; binary scan $ntlm_msg @${hoff}a${hlen} ntlm_host 70 | set unicode [expr {$flags & 0x00000001}] 71 | if {$unicode} { 72 | set ntlm_domain_convert "" 73 | foreach i [ split $ntlm_domain ""] { 74 | scan $i %c c 75 | if {$c>1} { 76 | append ntlm_domain_convert $i 77 | } elseif {$c<128} { 78 | set ntlm_domain_convert $ntlm_domain_convert 79 | } else { 80 | append ntlm_domain_convert \\u[format %04.4X $c] 81 | } 82 | } 83 | set ntlm_domain $ntlm_domain_convert 84 | set ntlm_user_convert "" 85 | foreach i [ split $ntlm_user ""] { 86 | scan $i %c c 87 | if {$c>1} { 88 | append ntlm_user_convert $i 89 | } elseif {$c<128} { 90 | set ntlm_user_convert $ntlm_user_convert 91 | } else { 92 | append ntlm_user_convert \\u[format %04.4X $c] 93 | } 94 | } 95 | set ntlm_user $ntlm_user_convert 96 | set ntlm_host_convert "" 97 | foreach i [ split $ntlm_host ""] { 98 | scan $i %c c 99 | if {$c>1} { 100 | append ntlm_host_convert $i 101 | } elseif {$c<128} { 102 | set ntlm_host_convert $ntlm_host_convert 103 | } else { 104 | append ntlm_host_convert \\u[format %04.4X $c] 105 | } 106 | } 107 | set ntlm_host $ntlm_host_convert 108 | } 109 | binary scan $ntlm_msg @${ntoff}a${ntlen} ntdata 110 | binary scan $ntlm_msg @${lmoff}a${lmlen} lmdata 111 | binary scan $ntdata H* ntdata_h 112 | binary scan $lmdata H* lmdata_h 113 | set interesting 1 114 | 115 | if { ($ntlm_domain equals $static::user_domain or [string tolower $ntlm_user] ends_with $static::email_domain) } { 116 | set attack 1 117 | if {[table lookup -subtable $static::userblacklist_tab $ntlm_user] == 1} { 118 | # Block ntlm_user exceeding the number of failed logons in the timeout period 119 | log $static::log_pri "[virtual] - BLACKHOLED $ntlm_domain\\$ntlm_user from $ntlm_host at [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" 120 | reject 121 | return 122 | } else { 123 | log $static::log_pri "[virtual] - Login attempt by $ntlm_domain\\$ntlm_user from $ntlm_host for [HTTP::uri]." 124 | } 125 | } else { 126 | set attack 0 127 | log $static::log_pri "[virtual] - Not a valid user - Login attempt by $ntlm_domain\\$ntlm_user from $ntlm_host for [HTTP::uri]." 128 | } 129 | 130 | return [list type $type flags [format 0x%08x $flags] \ 131 | ntlm_domain $ntlm_domain ntlm_host $ntlm_host ntlm_user $ntlm_user \ 132 | lmhash $lmdata nthash $ntdata] 133 | } 134 | } 135 | } 136 | when HTTP_RESPONSE { 137 | if {[info exists interesting] && $interesting == 1} { 138 | set client [IP::client_addr]:[TCP::client_port] 139 | set node [IP::server_addr]:[TCP::server_port] 140 | set nodeResp [HTTP::status] 141 | 142 | if { $nodeResp == 401 and ([info exists attack] and $attack == 1)} { 143 | log $static::log_pri "[virtual] - invalid credentials detected for $ntlm_user" 144 | table set -subtable $static::fail_tab -notouch -excl [IP::client_addr] 0 indef $static::fail_memory 145 | table incr -subtable $static::fail_tab [IP::client_addr] 146 | 147 | if {[table lookup -subtable $static::fail_tab [IP::client_addr]] >= $static::max_failures} { 148 | set now [clock seconds] 149 | set now_date [split [clock format $now -format {%X %x}] " "] 150 | 151 | set later [expr {$now + $static::block_duration}] 152 | set later_date [split [clock format $later -format {%X %x}] " "] 153 | 154 | log $static::log_pri "[virtual] - BLACKHOLING IPADDR - [IP::client_addr] (Reputation=[IP::reputation [IP::client_addr]]) at $now_date until $later_date" 155 | table set -subtable $static::blacklist_tab -excl [IP::client_addr] 1 indef $static::block_duration 156 | } 157 | 158 | if {[info exists ntlm_user]} { 159 | table set -subtable $static::userfail_tab -notouch -excl $ntlm_user 0 indef $static::fail_memory 160 | table incr -subtable $static::userfail_tab $ntlm_user 161 | 162 | if {[table lookup -subtable $static::userfail_tab $ntlm_user] >= $static::max_failures} { 163 | set now [clock seconds] 164 | set later [expr {$now + $static::block_duration}] 165 | log $static::log_pri "[virtual] - BLACKHOLING USER - $ntlm_user at $now_date until $later_date" 166 | table set -subtable $static::userblacklist_tab -excl $ntlm_user 1 indef $static::block_duration 167 | } 168 | 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /irules/DoS and NTLM Brute force protection for SIP flow.tcl: -------------------------------------------------------------------------------- 1 | when RULE_INIT { 2 | array set NTLMFlags { 3 | unicode 0x00000001 4 | oem 0x00000002 5 | req_target 0x00000004 6 | unknown1 0x00000008 7 | sign 0x00000010 8 | seal 0x00000020 9 | datagram 0x00000040 10 | lmkey 0x00000080 11 | netware 0x00000100 12 | ntlm 0x00000200 13 | unknown2 0x00000400 14 | unknown3 0x00000800 15 | ntlm_domain 0x00001000 16 | ntlm_server 0x00002000 17 | ntlm_share 0x00004000 18 | NTLM2 0x00008000 19 | targetinfo 0x00800000 20 | 128bit 0x20000000 21 | keyexch 0x40000000 22 | 56bit 0x80000000 23 | } 24 | set static::email_domain "domain.org" 25 | set static::user_domain "DOMAIN" 26 | set static::log_pri "local0." 27 | set static::fail_tab "NTLMfails" 28 | set static::blacklist_tab "NTLMblackhole" 29 | set static::userfail_tab "NTLMUserfails" 30 | set static::userblacklist_tab "NTLMUserblackhole" 31 | set static::max_failures 5 32 | set static::fail_memory 300 33 | set static::block_duration 300 34 | } 35 | 36 | when CLIENT_ACCEPTED { 37 | if {[table lookup -subtable $static::blacklist_tab [IP::client_addr]] == 1} { 38 | log $static::log_pri "[virtual] - BLACKHOLED IPADDR [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" 39 | reject 40 | return 41 | } 42 | } 43 | 44 | when CLIENTSSL_HANDSHAKE { 45 | SSL::collect 46 | } 47 | when CLIENTSSL_DATA { 48 | set payload [SSL::payload] 49 | if { ($payload contains "3 REGISTER") } { 50 | regexp -nocase {gssapi-data=\"([A-Za-z0-9+\/=]*)\",} $payload match gssapi garbage 51 | if { [info exists match] } { 52 | unset match 53 | unset garbage 54 | if { $gssapi != "" } { 55 | set ntlm_msg [ b64decode [string trim $gssapi]] 56 | binary scan $ntlm_msg a7ci protocol zero type 57 | if { $type eq 3} { 58 | binary scan $ntlm_msg @12ssissississississii \ 59 | lmlen lmlen2 lmoff \ 60 | ntlen ntlen2 ntoff \ 61 | dlen dlen2 doff \ 62 | ulen ulen2 uoff \ 63 | hlen hlen2 hoff \ 64 | slen slen2 soff \ 65 | flags 66 | set ntlm_domain {}; binary scan $ntlm_msg @${doff}a${dlen} ntlm_domain 67 | set ntlm_user {}; binary scan $ntlm_msg @${uoff}a${ulen} ntlm_user 68 | set ntlm_host {}; binary scan $ntlm_msg @${hoff}a${hlen} ntlm_host 69 | set unicode [expr {$flags & 0x00000001}] 70 | if {$unicode} { 71 | set ntlm_domain_convert "" 72 | foreach i [ split $ntlm_domain ""] { 73 | scan $i %c c 74 | if {$c>1} { 75 | append ntlm_domain_convert $i 76 | } elseif {$c<128} { 77 | set ntlm_domain_convert $ntlm_domain_convert 78 | } else { 79 | append ntlm_domain_convert \\u[format %04.4X $c] 80 | } 81 | } 82 | set ntlm_domain $ntlm_domain_convert 83 | set ntlm_user_convert "" 84 | foreach i [ split $ntlm_user ""] { 85 | scan $i %c c 86 | if {$c>1} { 87 | append ntlm_user_convert $i 88 | } elseif {$c<128} { 89 | set ntlm_user_convert $ntlm_user_convert 90 | } else { 91 | append ntlm_user_convert \\u[format %04.4X $c] 92 | } 93 | } 94 | set ntlm_user $ntlm_user_convert 95 | set ntlm_host_convert "" 96 | foreach i [ split $ntlm_host ""] { 97 | scan $i %c c 98 | if {$c>1} { 99 | append ntlm_host_convert $i 100 | } elseif {$c<128} { 101 | set ntlm_host_convert $ntlm_host_convert 102 | } else { 103 | append ntlm_host_convert \\u[format %04.4X $c] 104 | } 105 | } 106 | set ntlm_host $ntlm_host_convert 107 | } 108 | binary scan $ntlm_msg @${ntoff}a${ntlen} ntdata 109 | binary scan $ntlm_msg @${lmoff}a${lmlen} lmdata 110 | binary scan $ntdata H* ntdata_h 111 | binary scan $lmdata H* lmdata_h 112 | set interesting 1 113 | 114 | if { ($ntlm_domain equals $static::user_domain or $ntlm_user ends_with $static::email_domain) } { 115 | set attack 1 116 | if {[table lookup -subtable $static::userblacklist_tab $ntlm_user] == 1} { 117 | log $static::log_pri "[virtual] - BLACKHOLED $ntlm_domain\\$ntlm_user from $ntlm_host at [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" 118 | reject 119 | return 120 | } else { 121 | log $static::log_pri "[virtual] - Login attempt by $ntlm_domain\\$ntlm_user from $ntlm_host for SIP." 122 | } 123 | } else { 124 | set attack 0 125 | log $static::log_pri "[virtual] - Not a valid user - Login attempt by $ntlm_domain\\$ntlm_user from $ntlm_host for SIP." 126 | } 127 | } 128 | } 129 | } 130 | } 131 | # Release the payload 132 | SSL::release 133 | SSL::collect 134 | } 135 | 136 | when SERVERSSL_HANDSHAKE { 137 | SSL::collect 138 | SSL::release 0 139 | } 140 | 141 | when SERVERSSL_DATA { 142 | set payload [SSL::payload] 143 | 144 | if {[info exists interesting] && $interesting == 1} { 145 | set client [IP::client_addr]:[TCP::client_port] 146 | set node [IP::server_addr]:[TCP::server_port] 147 | 148 | if { $payload contains "401 Unauthorized ms-user-logon-data" and ([info exists attack] and $attack == 1) } { 149 | table set -subtable $static::fail_tab -notouch -excl [IP::client_addr] 0 indef $static::fail_memory 150 | table incr -subtable $static::fail_tab [IP::client_addr] 151 | 152 | set now [clock seconds] 153 | set now_date [split [clock format $now -format {%X %x}] " "] 154 | 155 | set later [expr {$now + $static::block_duration}] 156 | set later_date [split [clock format $later -format {%X %x}] " "] 157 | 158 | if {[info exists ntlm_user]} { 159 | table set -subtable $static::userfail_tab -notouch -excl $ntlm_user 0 indef $static::fail_memory 160 | table incr -subtable $static::userfail_tab $ntlm_user 161 | 162 | if {[table lookup -subtable $static::userfail_tab $ntlm_user] >= $static::max_failures} { 163 | log $static::log_pri "[virtual] - BLACKHOLING USER - $ntlm_user at $now_date until $later_date" 164 | table set -subtable $static::userblacklist_tab -excl $ntlm_user 1 indef $static::block_duration 165 | } 166 | 167 | } 168 | 169 | if {[table lookup -subtable $static::fail_tab [IP::client_addr]] >= $static::max_failures} { 170 | log $static::log_pri "[virtual] - BLACKHOLING IPADDR - [IP::client_addr] (Reputation=[IP::reputation [IP::client_addr]]) at $now_date until $later_date" 171 | table set -subtable $static::blacklist_tab -excl [IP::client_addr] 1 indef $static::block_duration 172 | } 173 | } 174 | } 175 | 176 | SSL::release 177 | SSL::collect 178 | } -------------------------------------------------------------------------------- /irules/Geolocation enforcement and Googlebot whitelisting.tcl: -------------------------------------------------------------------------------- 1 | when CLIENT_ACCEPTED { 2 | set decrypt 1 3 | } 4 | 5 | ### 6 | # Analyze the first request after an Handshake. 7 | ### 8 | 9 | when CLIENTSSL_HANDSHAKE { 10 | if { [info exists decrypt] and $decrypt } { 11 | SSL::collect 12 | } 13 | } 14 | 15 | when CLIENTSSL_DATA { 16 | if { [SSL::payload] matches_glob "*User-Agent:*googlebot*" } { 17 | set decrypt 0 18 | SSL::release 19 | } elseif { !([whereis [IP::client_addr] country] equals "CH") and ([class search -all AccessIP equals [IP::client_addr]] eq "0" ) } { 20 | log local0. "1 Client [IP::client_addr] from [whereis [IP::client_addr]] rejected due to security policy enforcement !!!" 21 | reject 22 | } else { 23 | set decrypt 0 24 | SSL::release 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /irules/Mitigate TokenChpoken attack on PeopleSoft.tcl: -------------------------------------------------------------------------------- 1 | when CLIENT_ACCEPTED { 2 | set cookiename "PS_TOKEN" 3 | set encryption_passphrase "hEuoYjmFUpB4PcpO3bUdQtLP4ic7jjmD5UB5KOifo5U8BClQfvotmu9LEa949nz" 4 | } 5 | when HTTP_RESPONSE { 6 | if { [HTTP::cookie exists $cookiename] } { 7 | HTTP::cookie encrypt $cookiename $encryption_passphrase 8 | } 9 | } 10 | when HTTP_REQUEST { 11 | if { [HTTP::cookie exists $cookiename] } { 12 | set decrypted [HTTP::cookie decrypt $cookiename $encryption_passphrase] 13 | if { ($decrypted eq "") } { 14 | # Cookie wasn't encrypted, delete it 15 | HTTP::cookie remove $cookiename 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /irules/POST preservation in Multidomain SSO.tcl: -------------------------------------------------------------------------------- 1 | ### 2 | # POST preservation feature 3 | # for Virtual Server with Multidomain SSO configured 4 | # 5 | # require : APM 6 | ### 7 | 8 | ### 9 | # Special notes 10 | # To support POST preservation in v11 and v12, 11 | # the administrator needs to configure special session variable assignment before the Allow ending in a Access policy 12 | # session.server.body = Session Variable session.server.initial_req_body 13 | # session.policy.result.redirect.url = Session Variable session.server.landinguri_base64 14 | ### 15 | 16 | ### 17 | # Release notes 18 | # 19 | # 2017/11/23 20 | # * Basic support for POST preservation in v13 21 | # * Add support for v11 and v12 environments 22 | # 23 | # 2017/11/24 24 | # * Replace static::idp_host by [PROFILE::access primary_auth_service] 25 | # * Add a static var to enable or disable the dummy form designed for testing purposes 26 | # * Avoid POSTing real body multiple times. A dummy var is used to retrieve the original POST content 27 | # 28 | # 2017/11/25 29 | # * Remove some coding errors 30 | # * Refactoring of some parts of the irule 31 | # * Remove unecessary checks on the SP hostname 32 | ### 33 | 34 | when RULE_INIT { 35 | set static::md_start_uri "/F5Networks-SSO-Req?SSO_ORIG_URI=" 36 | 37 | # for v11.x and v12.x deployment 38 | # set static::body_var "session.server.body" 39 | 40 | # for v13.x deployment 41 | set static::body_var "session.server.initial_req_body" 42 | 43 | # enable or disable autogenerated testing forms 44 | set static::dummy_form 1 45 | } 46 | 47 | when HTTP_REQUEST { 48 | if { ![ACCESS::session exists [HTTP::cookie MRHSession]] and !([HTTP::path] eq "/F5Networks-SSO-Resp") } { 49 | if { [HTTP::method] eq "POST" } { 50 | # save post data 51 | set ct [HTTP::header Content-Type] 52 | set uri [HTTP::uri] 53 | if { [URI::query $uri] != "" } { 54 | set uri $uri&ct=[URI::encode $ct]&f5-mdsso-post=1 55 | } else { 56 | set uri $uri?ct=[URI::encode $ct]&f5-mdsso-post=1 57 | } 58 | HTTP::respond 307 noserver Location "[PROFILE::access primary_auth_service]$static::md_start_uri[URI::encode [b64encode https://[HTTP::host]$uri]]" Connection Close 59 | return 60 | } else { 61 | HTTP::respond 302 noserver Location "[PROFILE::access primary_auth_service]$static::md_start_uri[URI::encode [b64encode https://[HTTP::host][HTTP::uri]]]" Connection Close 62 | return 63 | } 64 | } 65 | 66 | if { [ACCESS::session exists [HTTP::cookie MRHSession]] and [HTTP::query] contains "f5-mdsso-post=1" and [ACCESS::session data get $static::body_var] != "" } { 67 | set ct [URI::decode [URI::query [HTTP::uri] ct]] 68 | set dummy [getfield [expr {rand()}] "." 2] 69 | ACCESS::session data set session.server.dummy $dummy 70 | ACCESS::session data set session.server.ct $ct 71 | HTTP::respond 200 content " this page is used to hold your data while you are being authorized for your request.

you will be forwarded to continue the authorization process. if this does not happen automatically, please click the continue button below.
" noserver Content-Type "text/html" 72 | return 73 | } 74 | 75 | if { [ACCESS::session exists [HTTP::cookie MRHSession]] and [HTTP::method] eq "POST" and [HTTP::payload] contains "dummy" and [ACCESS::session data get session.server.dummy] eq [URI::query "/?[HTTP::payload]" dummy] } { 76 | HTTP::header replace Content-Type [ACCESS::session data get session.server.ct] 77 | HTTP::payload replace 0 [HTTP::header Content-Length] [ACCESS::session data get $static::body_var] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /irules/Provision IOS profile for Exchange ActiveSync with client certificate authentication.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-XpertSolutions/f5/702f20826ea475201f825e469f287ee593b5da14/irules/Provision IOS profile for Exchange ActiveSync with client certificate authentication.zip -------------------------------------------------------------------------------- /irules/Rate Limiting based on ACCESS TOKEN.tcl: -------------------------------------------------------------------------------- 1 | when RULE_INIT { 2 | ### 3 | # rate limit options 4 | ### 5 | 6 | set static::request_limit 1000 7 | set static::window_size 300 8 | 9 | ### 10 | # define URI endpoints 11 | ### 12 | 13 | set static::status_uri "/rate_limit_status" 14 | } 15 | 16 | when HTTP_REQUEST { 17 | 18 | ### 19 | # initialize vars 20 | ### 21 | 22 | set access_token "" 23 | set client_ip "" 24 | 25 | ### 26 | # retrieve the access_token. It will be used as a mandatory key to evaluate rate limiting 27 | ### 28 | 29 | if { [HTTP::header exists Authorization] and [HTTP::header Authorization] contains "Bearer" } { 30 | set access_token [getfield [HTTP::header Authorization] " " 2] 31 | set client_ip [IP::client_addr] 32 | } 33 | 34 | if { !($access_token eq "") } { 35 | 36 | ### 37 | # provide client with rate limit status 38 | ### 39 | set key [sha1 $access_token] 40 | set count [table lookup $key] 41 | set time [table timeout -remaining $key] 42 | 43 | ### 44 | # Provide a status page to the client 45 | ### 46 | 47 | if { [HTTP::path] eq $static::status_uri and [HTTP::method] eq "GET" } { 48 | if { $count > 0 } { 49 | set x_rate_limit_limit "$static::request_limit" 50 | set x_rate_limit_remaining "[expr {$static::request_limit-$count}]" 51 | set x_rate_limit_reset "$time" 52 | } else { 53 | set x_rate_limit_limit "$static::request_limit" 54 | set x_rate_limit_remaining "$static::request_limit" 55 | set x_rate_limit_reset "$static::window_size" 56 | } 57 | HTTP::respond 200 content "{\"x-rate-limit-limit\": $x_rate_limit_limit,\"x-rate-limit-remaining\": $x_rate_limit_remaining,\"x-rate-limit-reset\": $x_rate_limit_reset}" noserver Content-Type "application/json" Connection Close 58 | event disable all 59 | } else { 60 | 61 | ### 62 | # Handle the case where a client reach the rate limit 63 | ### 64 | 65 | if { $count >= $static::request_limit } { 66 | set x_rate_limit_limit "$static::request_limit" 67 | set x_rate_limit_remaining "0" 68 | set x_rate_limit_reset "$time" 69 | 70 | HTTP::respond 429 content "{\"x-rate-limit-limit\": $x_rate_limit_limit,\"x-rate-limit-remaining\": $x_rate_limit_remaining,\"x-rate-limit-reset\": $x_rate_limit_reset}" noserver Content-Type "application/json" Connection Close 71 | event disable all 72 | } else { 73 | if { $count == 0 } { 74 | table add $key 1 $static::window_size $static::window_size 75 | } else { 76 | table incr $key 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /irules/Registration service for OAuth 2.0 client Apps.tcl: -------------------------------------------------------------------------------- 1 | when RULE_INIT { 2 | 3 | ### 4 | # credentials required to access iControl REST API 5 | ### 6 | 7 | set static::adm_user "admin" 8 | set static::adm_pwd "admin" 9 | 10 | ### 11 | # settings required to authenticate the user trying to register an application 12 | ### 13 | 14 | set static::timeout 300 15 | set static::lifetime 300 16 | set static::access_profile "/Common/ap-ldap-auth" 17 | 18 | ### 19 | # settings required to update the APM configuration with the newly created ClientApp configuraiton 20 | ### 21 | 22 | set static::adm_partition "Common" 23 | set static::oauth_profile "my-oauth-profile" 24 | set static::scopes "myscope" 25 | 26 | ### 27 | # settings required to sync the OAuth 2.0 Authorization Server access profile 28 | ### 29 | 30 | set static::oauth_access_policy "ap-oauth-auth-server" 31 | 32 | ### 33 | # settings required to publish the client registration service 34 | ### 35 | 36 | set static::client_register_uri "/f5-oauth2/v1/client-register" 37 | set static::host "oauthas.example.com" 38 | 39 | } 40 | 41 | when CLIENT_ACCEPTED { 42 | ### 43 | # When we accept a connection, create an Access session and save the session ID. 44 | ### 45 | 46 | set flow_sid [ACCESS::session create -timeout $static::timeout -lifetime $static::lifetime] 47 | } 48 | 49 | when HTTP_REQUEST { 50 | 51 | ### 52 | # initialize vars 53 | ### 54 | 55 | set username "" 56 | set password "" 57 | set name "" 58 | set client_app "" 59 | set scopes "" 60 | set client_id "" 61 | set client_secret "" 62 | set agent "" 63 | 64 | set timestamp [clock seconds] 65 | 66 | switch -glob [string tolower [HTTP::header "User-Agent"]] { 67 | "*android*" { set agent "android" } 68 | "*ios*" { set agent "ios" } 69 | default { set agent "default" } 70 | } 71 | 72 | ### 73 | # identify client registration request. The client applicaiton needs to do a POST request on client registration URI and provides username and password 74 | ### 75 | 76 | if { [HTTP::path] eq $static::client_register_uri and [HTTP::host] eq $static::host and [HTTP::method] eq "POST" } { 77 | 78 | set username [URI::query "/?[HTTP::payload]" username] 79 | set password [URI::query "/?[HTTP::payload]" password] 80 | 81 | ### 82 | # play inline ACCESS policy to validate user credentials 83 | ### 84 | 85 | set username [string map -nocase { "%40" "@" } $username] 86 | 87 | ACCESS::policy evaluate -sid $flow_sid -profile $static::access_profile session.logon.last.username $username session.logon.last.password $password session.server.landinguri [string tolower [HTTP::path]] 88 | 89 | if { [ACCESS::policy result -sid $flow_sid] eq "deny" or [ACCESS::policy result -sid $flow_sid] eq "not_started" } { 90 | HTTP::respond 403 content "{\"error\": \"Invalid user credentials\",\"error-message\": \"Access denied by Acces policy\"}" noserver Content-Type "application/json" Connection Close 91 | ACCESS::session remove -sid $flow_sid 92 | event disable all 93 | } 94 | 95 | ACCESS::session remove -sid $flow_sid 96 | 97 | ### 98 | # start transaction (transId, state) state = STARTED 99 | ### 100 | 101 | # POST /mgmt/tm/transaction 102 | 103 | set json_body "{}" 104 | set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/transaction" -method POST -body $json_body -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd] 105 | set json_result [call /Common/sys-exec::json2dict $rbody] 106 | 107 | if { $status contains "200" } { 108 | set state [lindex $json_result [expr {[lsearch $json_result "state"]+1}]] 109 | set trans_id [lindex $json_result [expr {[lsearch $json_result "transId"]+1}]] 110 | 111 | if { $state contains "STARTED" } { 112 | HTTP::respond 403 content "{\"error\": \"Transaction failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close 113 | event disable all 114 | } 115 | } else { 116 | HTTP::respond 403 content "{\"error\": \"Transaction failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close 117 | event disable all 118 | } 119 | 120 | ### 121 | # generate client name and client application name 122 | ### 123 | 124 | set username [string map -nocase { "@" "." } $username] 125 | 126 | set name "$username-$agent-$timestamp" 127 | set client_app $name 128 | set scopes $static::scopes 129 | 130 | ### 131 | # prepare and execute API REST call to create a new client application. Endpoint: /mgmt/tm/apm/oauth/oauth-client-app 132 | ### 133 | 134 | set json_body "{\"name\": \"$name\",\"appName\": \"$client_app\",\"authType\": \"secret\",\"grantPassword\": \"enabled\",\"scopes\": \"$scopes\"}" 135 | set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/apm/oauth/oauth-client-app" -method POST -body $json_body -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd -headers { X-F5-REST-Coordination-Id $trans_id } ] 136 | set json_result [call /Common/sys-exec::json2dict $rbody] 137 | 138 | if { $status contains "200" } { 139 | 140 | ### 141 | # extract client_id and client_secret from JSON body 142 | ### 143 | 144 | set client_id [lindex $json_result [expr {[lsearch $json_result "clientId"]+1}]] 145 | set client_secret [lindex $json_result [expr {[lsearch $json_result "clientSecret"]+1}]] 146 | 147 | ### 148 | # prepare and execute API REST call to bind the client application to the OAuth profile. Endpoint: /mgmt/tm/apm/profile/oauth/~$static::adm_parition~$static::oauth_profile/client-apps 149 | ### 150 | 151 | set json_body "{\"name\": \"$name\"}" 152 | set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/apm/profile/oauth/~$static::adm_partition~$static::oauth_profile/client-apps" -method POST -body $json_body -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd -headers { X-F5-REST-Coordination-Id $trans_id } ] 153 | set json_result [call /Common/sys-exec::json2dict $rbody] 154 | 155 | ### 156 | # if binding is successful, respond to the client with client_id and client_secret 157 | ### 158 | 159 | if { $status contains "200" } { 160 | 161 | ### 162 | # Prepare and execute API REST call to apply Access Profile after Client Application has been assigned to OAuth profile 163 | ### 164 | 165 | set json_body "{\"generationAction\": \"increment\"}" 166 | set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/apm/profile/access/~$static::adm_partition~$static::oauth_access_policy" -method PATCH -body $json_body -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd -headers { X-F5-REST-Coordination-Id $trans_id } ] 167 | set json_result [call /Common/sys-exec::json2dict $rbody] 168 | 169 | if { $status contains "200" } { 170 | ### 171 | # Commit transaction 172 | ### 173 | 174 | set json_body "{\"state\": \"VALIDATING\"}" 175 | set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/transaction/$trans_id" -method PATCH -body $json_body -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd] 176 | set json_result [call /Common/sys-exec::json2dict $rbody] 177 | 178 | HTTP::respond 200 content "{\"client_id\": \"$client_id\",\"client_secret\": \"$client_secret\"}" noserver Content-Type "application/json" Connection Close 179 | event disable all 180 | } else { 181 | HTTP::respond 403 content "{\"error\": \"Synchronization failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close 182 | event disable all 183 | } 184 | } else { 185 | HTTP::respond 403 content "{\"error\": \"ClientApp binding failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close 186 | event disable all 187 | } 188 | } else { 189 | HTTP::respond 403 content "{\"error\": \"ClientApp creation failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close 190 | event disable all 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /irules/Sanitize special characters in AD groups names.tcl: -------------------------------------------------------------------------------- 1 | when ACCESS_POLICY_AGENT_EVENT { 2 | if { [ACCESS::policy agent_id] eq "clean_group_names" } { 3 | set newMemberOf " | " 4 | set memberOf [ACCESS::session data get "session.ad.last.attr.memberOf"] 5 | set splited [split $memberOf "|"] 6 | # Loop through all groups 7 | foreach field $splited { 8 | # If the group starts with 0x, it is hexa, needs to be decoded 9 | if { $field starts_with " 0x" } { 10 | # remove spaces 11 | set trimed [string trim $field " "] 12 | # skip the 0x at the beginning 13 | set hex_data [string tolower [substr $trimed 2]] 14 | # Loop through all items in datagroup 15 | foreach item [class names dg_special_chars] { 16 | set new_char [class lookup $item dg_special_chars] 17 | # Replace the special char with a "normal" char 18 | regsub -all $item $hex_data $new_char hex_data 19 | } 20 | # Decode the hexa without special chars to string 21 | set groupStr [binary format H* $hex_data] 22 | # Concat the sanitize group name to the list 23 | set newMemberOf [concat $newMemberOf $groupStr " | "] 24 | # The group is not hexa, just concat the value as it is 25 | } elseif { $field ne "" } { 26 | set newMemberOf [concat $newMemberOf $field " | "] 27 | } 28 | } 29 | # Store the sanitize memberOf into a new session var 30 | ACCESS::session data set "session.custom.ad.memberOf" $newMemberOf 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /irules/Simple SAML IDP Selector.tcl: -------------------------------------------------------------------------------- 1 | when HTTP_REQUEST { 2 | # Detect the origin SP from the initial SAML Auth Req 3 | if {[HTTP::path] eq "/saml/idp/profile/redirectorpost/sso" && [HTTP::method] eq "POST"} { 4 | set spName "NULL" 5 | set referer [HTTP::header Referer] 6 | if { !([URI::host $referer] eq "ssotest.example.net") } { 7 | set spName [URI::host $referer] 8 | # If the session is already created, set the session variable (does not work for the very first auth) 9 | ACCESS::session data set session.custom.saml.spName $spName 10 | } 11 | } 12 | } 13 | 14 | # For the first authentication SAML Auth req, the session does not exist yet. 15 | # So the session variable with the SP name must be created here 16 | # The var "spName" is set previously while the HTTP_REQUEST event 17 | 18 | when ACCESS_SESSION_STARTED { 19 | if { [info exists spName] } { 20 | ACCESS::session data set session.custom.saml.spName $spName 21 | } 22 | } 23 | 24 | when ACCESS_ACL_ALLOWED { 25 | # Just in case we are not in a standard SAML Auth (for example the user tried to login directly to the IDP) 26 | # Display a static page saying login is OK 27 | if { [HTTP::uri] != "/saml/idp/profile/redirectorpost/sso" } { 28 | ACCESS::respond 200 content "You are now connected" Connection Close 29 | } else { 30 | # Here the SP should be known, select the proper IDP config 31 | set savedSpName [ACCESS::session data get session.custom.saml.spName] 32 | set idp [class match -value [string tolower $savedSpName] equals dg-test-saml-idp-selector] 33 | # Test if we have a configuration IDP for the SP 34 | # If we dont have any, the default SSO profile of the AP will be used 35 | if { [info exists idp] && $idp != ""} { 36 | set idp_config /Common/$idp 37 | # log local0. "IDP $idp_config select for [HTTP::uri]" 38 | WEBSSO::select $idp_config 39 | unset idp_config 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /irules/Sliding session for FedAuth Persistent cookie delivered by ADFS.tcl: -------------------------------------------------------------------------------- 1 | when RULE_INIT { 2 | set static::timeout 900 3 | set static::httponly 1 4 | set static::debug 0 5 | set static::irule_name "irule-test-sliding-session" 6 | } 7 | 8 | when HTTP_REQUEST { 9 | if { $static::debug } { set event "HTTP_REQUEST" } 10 | set hostname [string tolower [HTTP::host]] 11 | switch -glob $hostname { 12 | "sharepoint1" - 13 | "sharepoint2" { 14 | set key "" 15 | set valid 1 16 | if { [HTTP::cookie exists FedAuth] } { 17 | set key [sha1 "$hostname:[HTTP::cookie FedAuth]"] 18 | if { [table lookup $key] == "" } { 19 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: no valid sliding session key found for [IP::client_addr] with session FedAuth:[string range [HTTP::cookie FedAuth] 0 7] on $hostname - Action: redirect user to logout uri" } 20 | 21 | HTTP::redirect "https://[HTTP::host]/_trust/default.aspx?wa=wsignoutcleanup1.0" 22 | } else { 23 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: a valid key has been found for [IP::client_addr] with session FedAuth:[string range [HTTP::cookie FedAuth] 0 7] on $hostname" } 24 | } 25 | } 26 | } 27 | default { set valid 0 } 28 | } 29 | } 30 | 31 | when HTTP_RESPONSE { 32 | if { $static::debug } { set event "HTTP_RESPONSE" } 33 | if {[HTTP::cookie exists FedAuth] and $valid } { 34 | 35 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: set-cookie header found with FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) for [IP::client_addr]" } 36 | 37 | set key [sha1 "$hostname:[HTTP::cookie FedAuth]"] 38 | 39 | if { [table lookup $key] != "" } { 40 | if { [table lifetime -remaining $key] >= $static::timeout } { 41 | 42 | table timeout $key $static::timeout 43 | HTTP::cookie expires FedAuth $static::timeout relative 44 | 45 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : cookie expiration set to 300 seconds" } 46 | } else { 47 | HTTP::cookie expires FedAuth [table lifetime -remaining $key] relative 48 | 49 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : cookie expiration set to remaining lifetime" } 50 | } 51 | } else { 52 | table add $key [HTTP::cookie FedAuth] $static::timeout [HTTP::cookie expires FedAuth] 53 | 54 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : Add cookie to the sliding session table for [HTTP::cookie expires FedAuth] seconds" } 55 | 56 | #HTTP::cookie expires FedAuth $static::timeout relative 57 | 58 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : set cookie to expires within 300 seconds" } 59 | } 60 | } elseif { $key != "" } { 61 | if { [table lookup $key] != "" } { 62 | if { [table lifetime -remaining $key] >= $static::timeout } { 63 | 64 | table timeout $key $static::timeout 65 | 66 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : insert FedAuth session cookie with 300 seconds expiration time" } 67 | 68 | HTTP::cookie insert name FedAuth value [table lookup $key] path / 69 | HTTP::cookie expires FedAuth $static::timeout relative 70 | HTTP::cookie secure FedAuth enable 71 | 72 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : insert FedAuth session cookie with 300 seconds expiration time" } 73 | 74 | } else { 75 | HTTP::cookie insert name FedAuth value [table lookup $key] path / 76 | HTTP::cookie expires FedAuth [table lifetime -remaining $key] relative 77 | HTTP::cookie secure FedAuth enable 78 | 79 | if { $static::debug } { log local0. "$static::irule_name - [string map -nocase {"/common/" ""} [virtual name]]: FedAuth cookie ([string range [HTTP::cookie FedAuth] 0 7]) valid for [table lifetime -remaining $key] seconds - Action : insert FedAuth session cookie" } 80 | } 81 | 82 | # 83 | # insert httponly flag to FedAuth Cookie 84 | # 85 | 86 | if { $static::httponly } { 87 | set value [HTTP::cookie value FedAuth] 88 | set testvalue [string tolower $value] 89 | set valuelen [string length $value] 90 | switch -glob $testvalue { 91 | "*;httponly*" - 92 | "*; httponly*" { } 93 | default { set value "$value; HttpOnly"; } 94 | } 95 | if { [string length $value] > $valuelen} { 96 | HTTP::cookie value FedAuth "${value}" 97 | } 98 | } 99 | } 100 | } 101 | } --------------------------------------------------------------------------------