├── 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 | 
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 | }
--------------------------------------------------------------------------------