content manipulation
44 TopicsEncrypting Cookies
Problem this snippet solves: This example shows how to encrypt and decrypt a HTTP cookie from within an iRule. Using HTTP::cookie encrypt / decrypt Here is a cookie encryption iRule that works in a high-availability setup and is CMP-compatible. If you are running a new enough version of TMOS, yet you don't want to use the built-in cookie encryption provided in the HTTP profile, here is the way to go. Here is an example which allows you to select the cookie(s) to encrypt by wildcard: Encrypt HTTP cookies with dynamic names How to use this snippet: when CLIENT_ACCEPTED { set cookiename "MyCookie" set encryption_passphrase "abcd1234" } when HTTP_RESPONSE { if { [HTTP::cookie exists $cookiename] } { HTTP::cookie encrypt $cookiename $encryption_passphrase } } when HTTP_REQUEST { if { [HTTP::cookie exists $cookiename] } { set decrypted [HTTP::cookie decrypt $cookiename $encryption_passphrase] if { ($decrypted eq "") } { # Cookie wasn't encrypted, delete it HTTP::cookie remove $cookiename } } } Using AES::encrypt / AES::decrypt The HTTP::cookie encrypt / decrypt functions were added in the LTM 9.0 branch, but in early versions, those commands caused system instability. Here is an iRule designed to work around that limitation by instead using the AES::encrypt and AES::decrypt commands along with the HTTP::cookie command to accomplish much the same goal. (It has been modified from its original incarnation to eliminate global variables, thus making it CMP-friendly.) First we'll use the RULE_INIT event to define a static encryption key (the same for each request, and the same on both units of a redundant pair.), and to define the name of the cookie to encrypt in responses and decrypt in requests. (Note that the key is stored in plaintext in the configuration file.) In the HTTP_RESPONSE event from the server, check to see if the cookie exists and has a value. Encrypt the original cookie value, URI encode it, and set the cookie to the new value. On subsequent client requests in the HTTP_REQUEST event you check to see if the cookie is present, with a value. If so, try to URI decode the value. Use the catch command to prevent a TCL error if the decoding fails. Then try to decrypt the URI decoded value. Use catch again to prevent a TCL error if the decryption fails. You can add additional logic to handle the scenarios if the cookie not being present, not decode-able or not-decryptable. Pretty simple huh? Check out the original thread here. Code : when CLIENT_ACCEPTED { # Define an AES encryption key. Valid key lengths are 128, 192, or 256 bits. # You can use a key generator, or create your own using only HEX characters. set aes_key "AES 128 63544a5e7178677b45366b41405f2dab" # Name of the cookie to encrypt/decrypt set cookie"myCookie" # Log debug messages to /var/log/ltm? 1=yes, 0=no. set cookie_encryption_debug 0 } when HTTP_RESPONSE { # Check if response contains an error cookie with a value if {[string length [HTTP::cookie value $cookie]] > 0}{ # Log the original error cookie value from the app if {$cookie_encryption_debug}{log local0. \ "Response from app contained our cookie: [HTTP::cookie value $cookie]"} # Encrypt the cookie value so the client can't change the value HTTP::cookie value $cookie [URI::encode [AES::encrypt $aes_key [HTTP::cookie value $cookie]]] # Log the encoded and encrypted error cookie value if {$cookie_encryption_debug}{log local0. \ "Encrypted error cookie to: [URI::encode [AES::encrypt $aes_key [HTTP::cookie value $cookie]]]"} } } when HTTP_REQUEST { # If the error cookie exists with any value, for any requested object, try to decrypt it if {[string length [HTTP::cookie value $cookie]]}{ if {$cookie_encryption_debug}{log local0. \ "Original error cookie value: [HTTP::cookie value $cookie]"} # URI decode the value (catching any errors that occur when trying to # decode the cookie value and save the output to cookie_uri_decoded) if {not ([catch {URI::decode [HTTP::cookie value $cookie]} cookie_uri_decoded])}{ # Log that the cookie was URI decoded if {$cookie_encryption_debug}{log local0. "\$cookie_uri_decoded was set successfully"} # Decrypt the value if {not ([catch {AES::decrypt $aes_key $cookie_uri_decoded} cookie_decrypted])}{ # Log the decrypted cookie value if {$cookie_encryption_debug}{log local0. "\$cookie_decrypted: $cookie_decrypted"} } else { # URI decoded value couldn't be decrypted. } } else { # Cookie value couldn't be URI decoded } } else { # Cookie wasn't present in the request } }4.3KViews1like0CommentsX Forwarded For Single Header Insert
Problem this snippet solves: Many servers and applications expect only a single X-Forwarded-For header per request. However, the BIG-IP HTTP profile option appends a new X-Forwarded-For header to the existing set of HTTP headers, even if there is an existing X-Forwarded-For header in the request. Both approaches are valid according to Section 4.2 of RFC2616. F5 Networks Product Development is tracking a Request for Enhancement as CR107639 for BIG-IP to instead append the value to the last existing X-Forwarded-For header. For applications expecting a single X-Forwarded-For header, it is possible to use an iRule like this one instead of the HTTP profile option to append the client IP value to the end of any existing X-Forwarded-For: header. Code : when HTTP_REQUEST { if {[HTTP::header exists X-Forwarded-For]}{ HTTP::header replace X-Forwarded-For "[HTTP::header X-Forwarded-For], [IP::client_addr]" } else { HTTP::header insert X-Forwarded-For [IP::client_addr] } }4KViews0likes2CommentsRewrite Host Header to Server Name
Problem this snippet solves: On each HTTP request, the selected pool member IP address is looked up against a datagroup and the corresponding hostname is inserted in the HTTP host header. The iRule expects one string type datagroup to be defined: ip_to_host_dg If you're using a version lower than 10.0, see the second example. For versions lower than 9.4.4, change the class reference in the second iRule from ip_to_host_dg to $::ip_to_host_dg. Code : # Name : host_header_rewrite_rule # # HTTP host header rewrite rule # # BIG-IP LTM # Tested on version 11.1.0 (but should work on 10.x or 11.x) # # Version: 1.1 - Initial version tested # # Description: # # On each HTTP request, the selected pool member IP address is looked up against a datagroup and the corresponding hostname # is replaced in the HTTP host header. # # Configuration requirements: # # 1. This iRule expects a string datagroup named ip_to_host_dg to be defined with the following format: # #ltm data-group internal ip_to_host_dg { # records { # 1.1.1.1 { # data host1.example.com # } # 1.1.1.2 { # data host2.example.com # } # 1.1.1.5 { # data host3.example.com # } # } # type string #} # # 2. Each pool member must be configured and any time the pool membership changes, this data group must be updated. # when HTTP_REQUEST_SEND { # Log debug messages to /var/log/ltm? 1=yes, 0=no. set host_debug 1 # Need to force the host header replacement and HTTP:: commands into the clientside context # as the HTTP_REQUEST_SEND event is in the serverside context clientside { if {$host_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: [HTTP::method] request to [HTTP::host][HTTP::uri]"} # Look up the selected server IP in the datagroup to get the host header value set host_header_value [class match -value [IP::server_addr] equals ip_to_host_dg] if {$host_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Looked up [IP::server_addr], found: $host_header_value."} # Check if the lookup returned a value if {$host_header_value ne ""}{ # Replace the host header value HTTP::header replace Host $host_header_value if {$host_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Replaced Host header with $host_header_value."} } } } # v9.x # Name : host_header_rewrite_rule (v9.x) # # HTTP host header rewrite rule # # BIG-IP LTM # Tested on version 9.4.8 # # Version: 1.0 - Initial version tested # # Description: # # On each HTTP request, the selected pool member IP address is looked up against a datagroup and the corresponding hostname # is replaced in the HTTP host header. # # Configuration requirements: # # 1. This iRule expects a string datagroup named ip_to_host_dg to be defined with the following format: # # pool member IP | host header to insert # ---------------|------------------- # 1.1.1.1 | host1.example.com # 1.1.1.2 | host2.example.com # 1.1.1.5 | host3.example.com # # 2. Each pool member must be configured and any time the pool membership changes, this datagroup must be updated. # when HTTP_REQUEST_SEND { # Log debug messages to /var/log/ltm? 1=yes, 0=no. set host_debug 1 # Need to force the host header replacement and HTTP:: commands into the clientside context # as the HTTP_REQUEST_SEND event is in the serverside context clientside { if {$host_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: New [HTTP::method] request to [HTTP::host][HTTP::uri]"} # Look up the selected server IP in the datagroup to get the host header value set host_header_value [findclass [LB::server addr] ip_to_host_dg " "] if {$host_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Looked up [LB::server addr], found: $host_header_value."} # Check if the lookup returned a value if {$host_header_value ne ""}{ # Replace the host header value HTTP::header replace Host $host_header_value if {$host_debug}{log local0. "[IP::client_addr]:[TCP::client_port]: Replaced Host header with $host_header_value."} } } }3.7KViews1like3CommentsHTTP Request Throttle
Problem this snippet solves: iRule to limit the number of requests clients can make within a certain amount of time. Units used are requests/minute but this could be changed to requests/sec pretty easily. Once users hit a predefined limit of requests per minute they are throttled. Clients can also be blacklisted (automatically restricted to a very low rate) or whitelisted. This rule as it is shown here has a very low limit as it only applies to certain URLs. If you remove those 'if' statements then you will need to allow many more requests as most web pages have a number of objects per page. This has not been tested against production traffic so the impact on the BIG-IP is not known. Code : # This is a complete rewrite that is CMP-friendly, see older TMOS v9 code below. ## HTTP Request Throttling ## ## This I-Rule allows only "maxReqs" HTTP requests within "timeout" interval. ## This version throttles by URI and allows IP address whitelists. IP address can be in ## the IP header or the X-Forwarded-For HTTP header. ## ## CMP compatible: Yes ## ## This rule requires: ## A default pool so that the session table can be used and to forward requests. ## ## 09/14/2014, Irule revised to use CMP compatible commands. ## - "static" is added to global variable names. ## - arrays replaced with subtables. ## ## This rule tested on: ## TMOS v11.6.0, should work on an 11.x version ## LTM ## when RULE_INIT { # This defines the maximum requests to be served within the timing interval defined by the static::timeout variable below. set static::maxReqs 4; # Timer Interval in seconds within which only static::maxReqs Requests are allowed. # (i.e: 10 req per 2 sec == 5 req per sec) # If this timer expires, it means that the limit was not reached for this interval and the request # counting starts over. # Making this timeout large increases memory usage. Making it too small negatively affects performance. set static::timeout 2; } when HTTP_REQUEST { # The iRule allows throttling for only sepecific URIs. You list the URIs_to_throttle # in a datagroup. URIs_to_throttle or Methods_to_throttle. # if you need to throttle by Method use an statement like this: # if { [class match [HTTP::uri] equals URIs_to_throttle] } # Note: a URI is everything after the hostname: e.g. /path1/login.aspx?name=user1 # if { [class match [HTTP::uri] equals URIs_to_throttle] } { # The following expects the IP addresses in multiple X-forwarded-for headers. It picks the first one. if { [HTTP::header exists X-forwarded-for] } { set client_IP_addr [getfield [lindex [HTTP::header values X-Forwarded-For] 0] "," 1] } else { set client_IP_addr [IP::client_addr] } # The matching below expects a datagroup named: Throttling_Whitelist_IPs # The Condition of the if statement is true if the IP address is NOT in the whitelist. if { not ([class match $client_IP_addr equals Throttling_Whitelist_IPs ] )} { set getcount [table lookup -notouch $client_IP_addr] if { $getcount equals "" } { table set $client_IP_addr "1" $static::timeout $static::timeout # record of this session does not exist, starting new record, request is allowed. } else { if { $getcount < $static::maxReqs } { # log local0. "Request Count for $client_IP_addr is $getcount" table incr -notouch $client_IP_addr # record of this session exists but request is allowed. } else { HTTP::respond 403 content { HTTP Request denied Your HTTP requests are being throttled. } } } } } }3.2KViews0likes7CommentsHTTP POST redirect preserving POST data
Problem this snippet solves: Use Javascript in an iRule to redirect HTTP POST requests to HTTPS. When an HTTP 30x redirect it sent to a client that has sent a POST request, the user-agent transparently issues a new GET request. As a result, the original POST request payload is lost. The idea behind this iRule is when client sends a POST to an HTTP virtual server, LTM replies with an HTML page which contains a form. The form contains post-data that the client just sent. The form will be auto-submitted by Javascript via HTTPS. Here is how this iRule works: if method = post, it issues HTTP::collect, which will invoke HTTP_REQUEST_DATA then it scans POST-data and prepares new content to respond to client. the new content will include form with all INPUT field names retrieved from POST-data the "action" parameter in the form points to external server all input fields are set as hidden (so client won't see the data during the process) Javascript that submits the form automatically then iRule replies to client with HTTP::respond command with content prepared in step 2 Code : when RULE_INIT { set static::ext_url "https://10.10.71.3/test.post" } when HTTP_REQUEST { # Check if request was a POST if { [string tolower [HTTP::method]] eq "post" } { # Check if there is a Content-Length header if { [HTTP::header exists "Content-Length"] } { if { [HTTP::header "Content-Length"] > 1048000 }{ # Content-Length over 1Mb so collect 1Mb set content_length 1048000 } else { # Content-Length under 1Mb so collect actual length set content_length [HTTP::header "Content-Length"] } } else { # Response did not have Content-Length header, so use default of 1Mb set content_length 1048000 } # Don't collect content if Content-Length header value was 0 if { $content_length > 0 } { HTTP::collect $content_length } } } when HTTP_REQUEST_DATA { set content "< script type=text/javascript language=javascript> \ function s(){ document.f.submit(); } \ " foreach p [split [HTTP::payload] &] { set name [URI::decode [getfield $p = 1]] set value [URI::decode [getfield $p = 2]] set content "${content} " } set content "${content} " HTTP::respond 200 content $content }2.8KViews0likes4CommentsRemove X- Headers From Web Server Response
Problem this snippet solves: Remove any X- header from web server HTTP responses Here is a simple iRule which removes any response header from the pool which starts with X-. The goal is to prevent users of the application from learning details of the application architecture from these user-defined comment headers. Code : when HTTP_RESPONSE { # Remove all instances of the Server header HTTP::header remove Server # Remove all headers starting with x- foreach header_name [HTTP::header names] { if {[string match -nocase x-* $header_name]}{ HTTP::header remove $header_name } } }2.6KViews0likes2CommentsClient Cert Request by URI with OCSP Checking
Problem this snippet solves: This iRule requests a client cert for specific URIs and then validates the client cert against the client SSL profile's trusted CA cert bundle. If that succeeds, then the client cert is validated against an OCSP server (or pool of servers). Invalid certs are redirected to a URL with the Openssl verify code appended. Note: See below for a sample bigip.conf showing the profile and virtual server definitions. Warning: This is an anonymized version of an OCSP iRule that was tested in a customer implementation. I have not done any testing of the iRule since anonymizing it. So test, test, test! Code : # hooleya_auth_ssl_cc_ocsp_rule # https://devcentral.f5.com/s/Wiki/default.aspx/iRules/client_cert_request_by_uri_with_ocsp_checking # # Requires 9.4.8 and hotfix 3 for: # CR125264 - HTTP::respond should be allowed in CLIENTSSL_HANDSHAKE # CR126501 - OCSP AUTH iRules need to detect server down vs. bad cert # CR111646: Connections are no longer rejected when clients fail to send a # certificate to a virtual server with a clientssl profile configured to "request" one. # v0.9.9 - 2010-02-22 # Description: # # This iRule requests a client cert for specific URIs and then validates the client cert against the client SSL profile's # trusted CA cert bundle. If that succeeds, then the client cert is validated against an OCSP server (or pool of servers). # Invalid certs are redirected to a URL with the Openssl verify code appended. # # Configuration requirements: # # 0. This iRule will only work for 9.4.8 with hotfix 3. It cannot work for any lower LTM version. # It could be updated for 10.0.x. In 10.1, you don't need to use the session table to store the cert details, # so this iRule is probably not worth updating for 10.1. # 1. Configure the URIs to request a client cert for in a datagroup named ocsp_pages_to_require_cert_class. # 2. You might also need to customize the cert parsing based on your requirements for which headers to insert. # 3. Add this iRule to an OCSP auth profile. # 4. Ideally, configure an OCSP server pool and VIP to use in the OCSP responder field # and consider uncommenting the code in this iRule to check the state of the OCSP server pool # before attempting the OCSP validation of a client cert. # 5. Test, test test! This is an anonymized version of an OCSP iRule that was tested in a customer implementation. # I have not done any testing of the iRule since anonymizing it. when RULE_INIT { # URL to redirect clients to for failed authentication # The error code is appended to the URL in the iRule set ::auth_failure_url "https://example.com/error.asp?errCode=" # Session timeout. Length of time (in seconds) to store the client cert in the session table. set ::ocsp_session_timeout 1800 # Log debug messages? (0=none, 1=minimal, 2=verbose, 3=everything) set ::ocsp_debug 3 # Enable audit logging? (0=none, 1=unvalidated requests only, 2=all requests) set ::ocsp_audit_log_level 2 # Pages to require a client cert for (replace with datagroup post-testing) # This is now configured in the ocsp_pages_to_require_cert_class datagroup # Prefix to use when inserting the certificate details in the HTTP headers set ::header_prefix "CRT_" # SSL::sessionid returns 64 0's if the session ID doesn't exist, so set a variable to check for this set ::ocsp_null_sessionid [string repeat 0 64] } when CLIENT_ACCEPTED { # Initialise the TMM session id and variables tracking the auth status on each new connection set tmm_auth_ssl_ocsp_sid 0 set invalidate_session 0 set need_cert 0 set inserted_headers 0 # Save the client IP:port and VIP name to shorten the log lines set log_prefix "client IP:port=[IP::client_addr]:[TCP::client_port]; VIP=[virtual name]" if {$::ocsp_debug > 0}{log local0. "$log_prefix: New TCP connection to [IP::local_addr]:[TCP::local_port]"} } when CLIENTSSL_CLIENTCERT { # This event is triggered when LTM requests/requires a cert, even if the client doesn't present a cert. if {$::ocsp_debug > 0}{log local0. "$log_prefix: Cert count: [SSL::cert count], SSL sessionid: [SSL::sessionid]"} # Exit this event if we didn't request a cert if {$need_cert == 0}{ if {$::ocsp_debug > 2}{log local0. "$log_prefix: Exiting event as \$need_cert is 0"} return } # Check if client presented a cert after it was requested if {[SSL::cert count] == 0}{ # No client cert received. Use -1 to track this (0 will be used to indicate no error by SSL::verify_result) set ssl_status_code "-1" # $ssl_status_desc is only used in this rule for debug logging. set ssl_status_desc "Required client certificate not present for resource." if {$::ocsp_debug > 0}{log local0. "$log_prefix: No cert for protected resource. Invalidating session."} set invalidate_session 1 # Audit logging if {$::ocsp_audit_log_level > 0}{catch {log -noname local0. "cc_audit: $log_prefix; status_text=No cert for secured URI; URI=$requested_uri;"}} } else { # Client presented at least one cert. The actual client cert should always be first. if {$::ocsp_debug > 1}{ # Loop through each cert and log the cert subject for {set i 0} {$i < [SSL::cert count]} {incr i}{ log local0. "$log_prefix: cert $i, subject: [X509::subject [SSL::cert $i]],\ issuer: [X509::issuer [SSL::cert $i]], cert_serial=[X509::serial_number [SSL::cert $i]]" } } if {$::ocsp_debug > 2}{log local0. "$log_prefix: Received cert with SSL session ID: [SSL::sessionid]. Base64 encoded cert: [b64encode [SSL::cert 0]]"} # Save the SSL status code (defined here: http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS) set ssl_status_code [SSL::verify_result] set ssl_status_desc [X509::verify_cert_error_string [SSL::verify_result]] # Check if there was no error in validating the client cert against LTM's server cert if { $ssl_status_code == 0 }{ if {$::ocsp_debug > 0}{log local0. "$log_prefix: Certificate validation against root cert OK. status: $ssl_status_desc. Checking against OCSP."} ###################################################################################################### ##### TODO: ##### If the OCSP responder is an LTM VIP (used for load balancing multiple OCSP servers) ##### you could add a check here of the OCSP server pool before attempting the OCSP validation. ##### Just change my_ocsp_http_pool to the actual OCSP server pool name. ## Check if the OCSP server pool does not have any #if {[active_members my_ocsp_http_pool] == 0}{ # # OCSP servers are not available!! # log local0.emerg "$log_prefix: OCSP auth pool is down! Resuming SSL handshake and blocking HTTP request." # # Audit logging # if {$::ocsp_audit_log_level > 0}{ # catch {log -noname local0. "cc_audit: $log_prefix; status_text=OCSP server pool is unavailable. Blocking request."} # } # # We could send an HTTP response from this event, but it doesn't actually get sent until # # the CLIENTSSL_HANDSHAKE event anyhow. So track that this is an invalid request and set the app auth status code # # to indicate OCSP validation of the cert failed. # set invalidate_session 1 # SSL::handshake resume # return #} ##### TODO END: ###################################################################################################### # Check if there isn't already a TMM authentication OCSP session ID if {$tmm_auth_ssl_ocsp_sid == 0} { # [AUTH::start pam default_ssl_ocsp] returns an authentication session ID set tmm_auth_ssl_ocsp_sid [AUTH::start pam default_ssl_ocsp] if {$::ocsp_debug > 2}{log local0. "$log_prefix: \$tmm_auth_ssl_ocsp_sid was 0, \$tmm_auth_ssl_ocsp_sid: $tmm_auth_ssl_ocsp_sid"} if {[info exists tmm_auth_subscription]} { if {$::ocsp_debug > 1}{log local0. "$log_prefix: Subscribing to \$tmm_auth_ssl_ocsp_sid: $tmm_auth_ssl_ocsp_sid"} AUTH::subscribe $tmm_auth_ssl_ocsp_sid } } AUTH::cert_credential $tmm_auth_ssl_ocsp_sid [SSL::cert 0] AUTH::cert_issuer_credential $tmm_auth_ssl_ocsp_sid [SSL::cert issuer 0] AUTH::authenticate $tmm_auth_ssl_ocsp_sid # Hold the SSL handshake until the auth result is returned from OCSP # The AUTH::authenticate command triggers an OCSP lookup and then the AUTH_RESULT event. # In AUTH_RESULT, SSL::handshake resume triggers CLIENTSSL_HANDSHAKE. if {$::ocsp_debug > 0}{log local0. "$log_prefix: Holding SSL handshake for OCSP check"} SSL::handshake hold } else { # Client cert validation against the CA's root server cert failed. if {$::ocsp_debug > 0}{log local0. "$log_prefix: Certificate validation not ok. Status: $ssl_status_code, $ssl_status_desc"} # Audit logging if {$::ocsp_audit_log_level > 0}{catch {log -noname local0. "cc_audit: $log_prefix; \ status_text=Invalid cert for secured URI; openssl_code=$ssl_status_code; openssl_desc=$ssl_status_desc;\ cert_subject=[X509::subject [SSL::cert 0]]; cert_issuer=[X509::issuer [SSL::cert 0]];\ cert_serial=[X509::serial_number [SSL::cert 0]]; URI=$requested_uri"}} # Delete the SSL session from the session table if {$::ocsp_debug > 1}{log local0. "$log_prefix: Invalidating SSL session [SSL::sessionid]"} session delete ssl [SSL::sessionid] SSL::session invalidate set invalidate_session 1 # Release the request flow as we want to send an HTTP response to clients who don't send a valid cert if {$::ocsp_debug > 0}{log local0. "$log_prefix: Invalid cert. Releasing HTTP."} } } } when AUTH_RESULT { # AUTH::status values: # https://devcentral.f5.com/s/wiki/default.aspx/iRules/AUTH__status.html # 0 = success # 1 = failure # -1 = error # 2 = not-authed if {$::ocsp_debug > 0}{log local0. "$log_prefix: \[AUTH::status\]: [AUTH::status]; (0=success, 1=failure, -1=error, 2=not-authed)"} # Check if there is an existing TMM SSL OCSP session ID if {[info exists tmm_auth_ssl_ocsp_sid] and ($tmm_auth_ssl_ocsp_sid == [AUTH::last_event_session_id])} { # Save the auth status set tmm_auth_status [AUTH::status] # TESTING ONLY: If you want to take any response from the OCSP server as valid, # uncomment this line and set tmm_auth_status to 0. #set tmm_auth_status 0 # Check if auth was successful if {$tmm_auth_status == 0 } { # OCSP auth was successful, so resume the SSL handshake. This will trigger the CLIENTSSL_HANDSHAKE event next. if {$::ocsp_debug > 0}{log local0. "$log_prefix: Valid cert per OCSP. Resuming SSL handshake"} SSL::handshake resume } else { # OCSP auth failed # Audit logging if {$::ocsp_audit_log_level > 0}{ catch {log -noname local0. "cc_audit: status=403.13. $log_prefix; status_text=Invalid cert per OCSP for secured URI; URI=$requested_uri"} } # We could send an HTTP response from this event, but it doesn't actually get sent until # the CLIENTSSL_HANDSHAKE event anyhow. So track that this is an invalid request and set the app auth status code # to indicate OCSP validation of the cert failed. set invalidate_session 1 if {$::ocsp_debug > 0}{log local0. "$log_prefix: Invalid cert per OCSP. \[AUTH::response_data\]: [AUTH::response_data]. Resuming SSL handshake."} SSL::handshake resume } } } when CLIENTSSL_HANDSHAKE { # This event is triggered when the SSL handshake with the client completes # Log SSL cipher details if {$::ocsp_debug > 2}{log local0. "$log_prefix: Cipher name, version, bits: [SSL::cipher name], [SSL::cipher version], [SSL::cipher bits]"} # Exit this event if cert isn't required if {$need_cert == 0}{ if {$::ocsp_debug > 2}{log local0. "$log_prefix: \$need_cert is 0, exiting event."} return } # Check if OCSP auth was already successful if {[info exists tmm_auth_status] and $tmm_auth_status == 0}{ if {$::ocsp_debug > 1}{log local0. "$log_prefix: Auth succeeded, parsing cert fields and adding session table entry."} # The parsing of the cert can be customized based on the application's requirements # For this particular implementation, the customer wanted the following fields inserted into the request HTTP headers: # # Issuer # Serial number # Valid from date # Valid to date # Subject # Add the client cert fields as a list to the session table if {$::ocsp_debug > 0}{log local0. "$log_prefix: Saving client cert details in session using SSL sessionid [SSL::sessionid]."} session add ssl [SSL::sessionid] [list \ ${::header_prefix}Issuer [X509::issuer [SSL::cert 0]] \ ${::header_prefix}SerialNumber [X509::serial_number [SSL::cert 0]] \ ${::header_prefix}ValidFrom [X509::not_valid_before [SSL::cert 0]] \ ${::header_prefix}ValidUntil [X509::not_valid_after [SSL::cert 0]] \ ${::header_prefix}Subject [X509::subject [SSL::cert 0]] ] $::ocsp_session_timeout # Audit logging if {$::ocsp_audit_log_level > 1}{ catch {log -noname local0. "cc_audit: status=okay; $log_prefix; status_text=Valid cert per OCSP for secured URI (new SSL session);\ cert_subject=[X509::subject [SSL::cert 0]]; cert_issuer=[X509::issuer [SSL::cert 0]]; cert_serial=[X509::serial_number [SSL::cert 0]]; \ URI=$requested_uri"} } if {$::ocsp_debug > 1}{log local0. "$log_prefix: Auth was successful, releasing HTTP"} HTTP::release } elseif {$invalidate_session}{ if {$::ocsp_debug > 0}{log local0. "$log_prefix: No/invalid cert received, sending block response."} # Send response to client for invalid request HTTP::respond 302 Location "${::auth_failure_url}${ssl_status_code}" Connection Close Cache-Control No-Cache Pragma No-Cache HTTP::release session delete ssl [SSL::sessionid] SSL::session invalidate TCP::close } else { if {$::ocsp_debug > 2}{log local0. "$log_prefix: default case."} } } when HTTP_REQUEST { if {$::ocsp_debug > 1}{log local0. "$log_prefix: URI: [HTTP::uri], SSL session ID: [SSL::sessionid],\ session lookup llength: [llength [session lookup ssl [SSL::sessionid]]],\ string len: [string length [session lookup ssl [SSL::sessionid]]]\ User-Agent: [HTTP::header User-Agent]"} # Double check that the session is valid if {[info exists invalidate_session] and $invalidate_session == 1}{ if {$::ocsp_debug > 0}{log local0. "$log_prefix: Invalidating SSL session ID: [SSL::sessionid]"} session delete ssl [SSL::sessionid] SSL::session invalidate } # Check if request is to a page which requires a client SSL certificate if {[matchclass [string tolower [HTTP::path]] starts_with $::ocsp_pages_to_require_cert_class]}{ # Save the requested URI for logging in subsequent events set requested_uri [HTTP::uri] # Track that this is a request for a restricted URI set need_cert 1 if {$::ocsp_debug > 0}{log local0. "$log_prefix: Request to restricted path: [HTTP::path]. \$need_cert: $need_cert"} # Check if there is an existing SSL session ID and if the cert is in the session table # This condition should only be true on resumed SSL sessions. if {[SSL::sessionid] ne $::ocsp_null_sessionid and [session lookup ssl [SSL::sessionid]] ne ""}{ if {$::ocsp_debug > 0}{ log local0. "$log_prefix: Allowed request to [HTTP::host][HTTP::uri]. Inserting SSL cert details in HTTP headers." # Debug logging of each session table list item if {$::ocsp_debug > 2}{ foreach session_element [session lookup ssl [SSL::sessionid]] { log local0. "$log_prefix: $session_element" } } } # Remove any HTTP header which starts with "crt_" foreach a_header [HTTP::header names] { # Check if this header name starts with "crt_" if {[string match -nocase ${::header_prefix}* $a_header]}{ HTTP::header remove $a_header # If there is a header which starts with crt_, it is probably someone attacking the application! log local0.emerg "$log_prefix: Client with possible spoofed client cert header [HTTP::request]" } } # Insert SSL cert details in the HTTP headers HTTP::header insert [session lookup ssl [SSL::sessionid]] # Track that we've inserted the HTTP headers, so we don't do it again in HTTP_REQUEST_SEND set inserted_headers 1 if {$::ocsp_debug > 0}{log local0. "$log_prefix: Valid request"} # Audit logging if {$::ocsp_audit_log_level > 1}{ # Get the cert details from the session table for the audit logging set session_list [session lookup ssl [SSL::sessionid]] catch {log -noname local0. "cc_audit: status=okay; $log_prefix; status_text=Valid cert per OCSP for secured URI (resumed SSL session);\ cert_subject=[lindex $session_list 9]; cert_issuer=[lindex $session_list 1]; cert_serial=[lindex $session_list 3]; URI=$requested_uri"} } } else { # Hold the HTTP request until the SSL re-negotiation is complete HTTP::collect # Force renegotiation of the SSL connection with a cert requested SSL::session invalidate SSL::authenticate always SSL::authenticate depth 9 SSL::cert mode request SSL::renegotiate if {$::ocsp_debug > 0}{log local0. "$log_prefix: Restricted path, [HTTP::uri], with no client cert. Collecting HTTP and renegotiating SSL"} } } else { if {$::ocsp_debug > 1}{log local0. "$log_prefix: Request to unrestricted path: [HTTP::path]"} set need_cert 0 } } when HTTP_REQUEST_SEND { # This event is relevant only on the initial request of a secured URI (non-resumed SSL sessions). # The insertion of cert details for resumed SSL sessions is handled in HTTP_REQUEST. # Force evaluation in clientside context as HTTP_REQUEST_SEND is a serverside event clientside { if {$::ocsp_debug > 0}{log local0. "$log_prefix: \$invalidate_session: $invalidate_session,\ \$need_cert: $need_cert, \[SSL::sessionid\]: [SSL::sessionid], \[session lookup ssl \[SSL::sessionid\]\]: [session lookup ssl [SSL::sessionid]],\ URI: [clientside {HTTP::uri}]"} # Check if request was to a restricted URI and the headers weren't inserted already in HTTP_REQUEST if {$need_cert==1 and $inserted_headers==0}{ # Check if the session is still valid, there is an existing SSL session ID and that the cert is in the session table if {$invalidate_session == 0 and [SSL::sessionid] ne $::ocsp_null_sessionid and [session lookup ssl [SSL::sessionid]] ne ""}{ # Remove any HTTP header which starts with "crt_" foreach a_header [HTTP::header names] { # Check if this header name starts with "crt_" if {[string match -nocase ${::header_prefix}* $a_header]}{ HTTP::header remove $a_header # If there is a header which starts with crt_, it is probably someone attacking the application! log local0.emerg "$log_prefix: Client with possible spoofed client cert header [HTTP::host][HTTP::uri], [HTTP::header User-Agent"]" } } if {$::ocsp_debug > 0}{log local0. "$log_prefix: Inserting SSL cert details in HTTP headers."} # Insert SSL cert details from the session table in the HTTP headers HTTP::header insert [session lookup ssl [SSL::sessionid]] } else { # Client request for secured URI wasn't valid log local0. "$log_prefix: Rejecting connection for invalid request to [HTTP::host][HTTP::uri] ([IP::local_addr]:[TCP::local_port])\ with session ID: [SSL::sessionid]" # Reject the connection as we should never get here reject } } } }1.9KViews0likes3CommentsHTTP Payload Collection
Problem this snippet solves: iRule demonstrating the basic approach for collection and manipulation of HTTP payload data. Note: Data collection and manipulation can require a large amount of memory and CPU processing for each connection. LTM is not the optimal place to perform HTTP payload manipulation unless you can take advantage of the streaming replacement functionality. For general payload examination, payload collection is limited by memory allocation constraints to a 32MB maximum (4Mb in pre-v11.x). Character set conversions and regex operations can quadruple the payload size in memory, so the functional maximum collection limit for payload manipulation is 8MB (1Mb in pre-v11.x). (See AskF5 SOL6578 for details). Even though it is possible to collect up to store up to 32Mb of data in a variable, it is not recommended to do this without careful consideration. If each connection can use 32Mb of memory, TMM can quickly run out of memory. Forcing the server to HTTP/v1.0 prevents chunking & ensures a Content-Length header is sent. The first example uses that value to set the collection size to the smaller of Content-Length or 1MB/4MB limit. For payloads larger than 1MB, the entire payload may be processed by iteratively collecting and releasing max sized payload sections and decrementing a counter on each pass by # of bytes processed until the entire content length has been processed. Note that pattern matching may be compromised for patterns that straddle collection boundaries. The second example demonstrates this approach. How to use this snippet: This iRule requires LTM v10. or higher. Code : # The following example gives the framework to examine and manipulate the first 1MB of each response: when HTTP_REQUEST { # Don't allow data to be chunked if { [HTTP::version] eq "1.1" } { if { [HTTP::header is_keepalive] } { HTTP::header replace "Connection" "Keep-Alive" } HTTP::version "1.0" } } # The following example includes collection counter / recollect logic to process payloads > 1MB in size. (Untested, may not be optimal -- please update with improvements.) when HTTP_REQUEST { # Don't allow data to be chunked if { [HTTP::version] eq "1.1" } { if { [HTTP::header is_keepalive] } { HTTP::header replace "Connection" "Keep-Alive" } HTTP::version "1.0" } set collected 0 } when HTTP_RESPONSE { # Get the content length so we can request the data to be # processed in the HTTP_RESPONSE_DATA event. if { [HTTP::header exists "Content-Length"] } { set content_length [HTTP::header "Content-Length"] } else { set content_length 0 } # content_length of 0 indicates chunked data (of unknown size) if { $content_length > 0 && $content_length < 1048577 } { set collect_length $content_length } else { set collect_length 1048576 } log local0.info "Content Length: $content_length Collect length: $collect_length" if { $collect_length > 0 } { HTTP::collect $collect_length } } when HTTP_RESPONSE_DATA { HTTP::release if { $content_length > 0 } { # for unchunked data, calculate remaining length & re-collect if necessary # The HTTP_RESPONSE_DATA event will be triggered again when each collection is complete set collected [expr {$collected + $collect_length}] set remaining [expr {$content_length - $collected}] if { $remaining > 0 } { if { $remaining < $collect_length } { set collect_length $remaining } HTTP::collect $collect_length } } else { # chunked responses data, continue collecting in 1MB chunks. Watch for hanging responses here. # The HTTP_RESPONSE_DATA event will be triggered again when each collection is complete HTTP::collect $collect_length } } when HTTP_RESPONSE { # Get the content length so we can request the data to be # processed in the HTTP_RESPONSE_DATA event. if { [HTTP::header exists "Content-Length"] && [HTTP::header "Content-Length"] < 1048577 } { set content_length [HTTP::header "Content-Length"] } else { set content_length 1048576 } log local0.info "Content Length: $content_length" if { $content_length > 0 } { HTTP::collect $content_length } } when HTTP_RESPONSE_DATA { HTTP::release } Tested this on version: 10.01.6KViews0likes0CommentsInsert Client Certificate In Serverside HTTP Headers
Problem this snippet solves: An example iRule that pulls certain information from a client cert and passes it along to backend server in HTTP headers. Here's one that illustrates how to use the "session" command in conjunction with SSL certificate information to allow passing of information to backend webservers. As above, I want to deliver ssl cert serial number to http server behind BIG-IP, and redirect the users who has no cert to an error page at same time. Code : # client_cert_header_insert_rule when CLIENTSSL_CLIENTCERT { # Check if client presented at least one cert if {[SSL::cert count] > 0}{ # Insert the following fields in the session table with a timeout of 7200 seconds: # Do the processing now as opposed to in HTTP_REQUEST as there # can be many HTTP requests using the same SSL session ID # # Index - item # 0 - base64 encoding of the client SSL cert # 1 - serial number of the cert # 2 - the verification status text for the client cert against the client SSL profile's root CA cert session add ssl [SSL::sessionid] [list \ [SSL::verify_result] \ [b64encode [SSL::cert 0]] \ [X509::serial_number [SSL::cert 0]] \ ] 7200 log local0. "[IP::client_addr]:[TCP::client_port]: Added session data for cert. Status:\ [X509::verify_cert_error_string [lindex [session lookup ssl [SSL::sessionid]] 0]] with key [SSL::sessionid]" } } when HTTP_REQUEST { # Check if SSL session ID is in the cache (SSL::sessionid returns 64 zeroes if it's not in v9 and a null string in v10) if {[SSL::sessionid] ne "0000000000000000000000000000000000000000000000000000000000000000" && [SSL::sessionid] ne ""}{ # Get the session table entry (a TCL list) for this session ID set session_data [session lookup ssl [SSL::sessionid]] # Check if the first element of the session table entry for this session ID is 0 (status for successful cert validation) if {[lindex $session_data 0] == 0}{ log local0. "[IP::client_addr]:[TCP::client_port]: Valid cert per session table entry. Inserting cert details in HTTP headers." # Insert cert details in the HTTP headers HTTP::header insert SSLClientCertStatus "ok" HTTP::header insert SSLClientCertb64 [lindex $session_data 1] HTTP::header insert SSLClientCertSN [lindex $session_data 2] # Exit this event in this rule return } } # If we're still in this rule, cert wasn't valid # so send HTTP 302 redirect to an error page HTTP::respond 302 Location "http://[HTTP::host]/cert_error.html" log local0. "[IP::client_addr]:[TCP::client_port]: No or invalid cert from client." }1.5KViews0likes2CommentsHTTPS offload rewriting
Problem this snippet solves: This iRule shows how to rewrite an HTTP web application's self references from http:// to https:// to avoid insecure content warnings. Note if the response content size is changed like it would be with this iRule you must use a custom HTTP profile with response chunking set to rechunk. Code : when HTTP_REQUEST { # Save the requested host value set host [string tolower [HTTP::host]] # If the HTTP host header is blank, use the VS IP address # If the VS IP is not routable for clients, hard code a routable IP # to replace [IP::local_addr] if {$host eq ""}{set host [IP::local_addr]} # Disable the stream filter by default STREAM::disable } when HTTP_RESPONSE { # Check if response type is text and host isn't null if {[HTTP::header value Content-Type] contains "text" and $host ne ""}{ # Replace http://$host with https://$host STREAM::expression "@http://$host@https://$host@" # Enable the stream filter for this response only STREAM::enable } # Rewrite the Location header in redirects to https:// if { [HTTP::is_redirect] && [string tolower [HTTP::header Location]] starts_with "http://$host"} { HTTP::header replace Location [string map -nocase "http://$host https://$host" [HTTP::header Location]] } }1.1KViews0likes7Comments