An age old question that we’ve seen time and time again in the iRules forums here on DevCentral is “How can I use iRules to manage multiple SSL certs on one VIP"?”. The answer has always historically been “I’m sorry, you can’t.”. The reasoning is sound. One VIP, one cert, that’s how it’s always been. You can’t do anything with the connection until the handshake is established and decryption is done on the LTM. We’d like to help, but we just really can’t. That is…until now.

The TLS protocol has somewhat recently provided the ability to pass a “desired servername” as a value in the originating SSL handshake. Finally we have what we’ve been looking for, a way to add contextual server info during the handshake, thereby allowing us to say “cert x is for domain x” and “cert y is for domain y”. Known to us mortals as "Server Name Indication" or SNI (hence the title), this functionality is paramount for a device like the LTM that can regularly benefit from hosting multiple certs on a single IP. We should be able to pull out this information and choose an appropriate SSL profile now, with a cert that corresponds to the servername value that was sent. Now all we need is some logic to make this happen.

Lucky for us, one of the many bright minds in the DevCentral community has whipped up an iRule to show how you can finally tackle this challenge head on. Because Joel Moses, the shrewd mind and DevCentral MVP behind this example has already done a solid write up I’ll quote liberally from his fine work and add some additional context where fitting. Now on to the geekery:

First things first, you’ll need to create a mapping of which servernames correlate to which certs (client SSL profiles in LTM’s case). This could be done in any manner, really, but the most efficient both from a resource and management perspective is to use a class. Classes, also known as DataGroups, are name->value pairs that will allow you to easily retrieve the data later in the iRule. Quoting Joel:

Create a string-type datagroup to be called "tls_servername". Each hostname that needs to be supported on the VIP must be input along with its matching clientssl profile. For example, for the site "" with a ClientSSL profile named "clientssl_testsite", you should add the following values to the datagroup.

        Value: clientssl_testsite

Once you’ve finished inputting the different server->profile pairs, you’re ready to move on to pools. It’s very likely that since you’re now managing multiple domains on this VIP you'll also want to be able to handle multiple pools to match those domains. To do that you'll need a second mapping that ties each servername to the desired pool. This could again be done in any format you like, but since it's the most efficient option and we're already using it, classes make the most sense here. Quoting from Joel:

If you wish to switch pool context at the time the servername is detected in TLS, then you need to create a string-type datagroup called "tls_servername_pool". You will input each hostname to be supported by the VIP and the pool to direct the traffic towards. For the site "" to be directed to the pool "testsite_pool_80", add the following to the datagroup:

        Value: testsite_pool_80

If you don't, that's fine, but realize all traffic from each of these hosts will be routed to the default pool, which is very likely not what you want. Now then, we have two classes set up to manage the mappings of servername->SSLprofile and servername->pool, all we need is some app logic in line to do the management and provide each inbound request with the appropriate profile & cert. This is done, of course, via iRules. Joel has written up one heck of an iRule which is available in the codeshare (here) in it's entirety along with his solid write-up, but I'll also include it here in-line, as is my habit.

Effectively what's happening is the iRule is parsing through the data sent throughout the SSL handshake process and searching for the specific TLS servername extension, which are the bits that will allow us to do the profile switching magic. He's written it up to fall back to the default client SSL profile and pool, so it's very important that both of these things exist on your VIP, or you may likely find yourself with unhappy users.

One last caveat before the code: Not all browsers support Server Name Indication, so be careful not to implement this unless you are very confident that most, if not all, users connecting to this VIP will support SNI. For more info on testing for SNI compatibility and a list of browsers that do and don't support it, click through to Joel's awesome CodeShare entry, I've already plagiarized enough.

So finally, the code. Again, my hat is off to Joel Moses for this outstanding example of the power of iRules. Keep at it Joel, and thanks for sharing!

   1: when CLIENT_ACCEPTED {
   2:     if { [PROFILE::exists clientssl] } {
   4:                 # We have a clientssl profile attached to this VIP but we need
   5:                 # to find an SNI record in the client handshake. To do so, we'll
   6:                 # disable SSL processing and collect the initial TCP payload.
   8:                 set default_tls_pool [LB::server pool]
   9:                 set detect_handshake 1
  10:                 SSL::disable
  11:                 TCP::collect
  13:         } else {
  15:                 # No clientssl profile means we're not going to work.
  17:                 log local0. "This iRule is applied to a VS that has no clientssl profile."
  18:                 set detect_handshake 0
  20:         }
  22: }
  24: when CLIENT_DATA {
  26:         if { ($detect_handshake) } {
  28:                 # If we're in a handshake detection, look for an SSL/TLS header.
  30:                 binary scan [TCP::payload] cSS tls_xacttype tls_version tls_recordlen
  32:                 # TLS is the only thing we want to process because it's the only
  33:                 # version that allows the servername extension to be present. When we
  34:                 # find a supported TLS version, we'll check to make sure we're getting
  35:                 # only a Client Hello transaction -- those are the only ones we can pull
  36:                 # the servername from prior to connection establishment.
  38:                 switch $tls_version {
  39:                         "769" -
  40:                         "770" -
  41:                         "771" {
  42:                                 if { ($tls_xacttype == 22) } {
  43:                                         binary scan [TCP::payload] @5c tls_action
  44:                                         if { not (($tls_action == 1) && ([TCP::payload length] > $tls_recordlen)) } {
  45:                                                 set detect_handshake 0
  46:                                         }
  47:                                 }
  48:                         }
  49:                         default {
  50:                                 set detect_handshake 0
  51:                         }
  52:                 }
  54:                 if { ($detect_handshake) } {
  56:                 # If we made it this far, we're still processing a TLS client hello.
  57:                 #
  58:                 # Skip the TLS header (43 bytes in) and process the record body. For TLS/1.0 we
  59:                 # expect this to contain only the session ID, cipher list, and compression
  60:                 # list. All but the cipher list will be null since we're handling a new transaction
  61:                 # (client hello) here. We have to determine how far out to parse the initial record
  62:                 # so we can find the TLS extensions if they exist.
  64:                         set record_offset 43
  65:                         binary scan [TCP::payload] @${record_offset}c tls_sessidlen
  66:                         set record_offset [expr {$record_offset + 1 + $tls_sessidlen}]
  67:                         binary scan [TCP::payload] @${record_offset}S tls_ciphlen
  68:                         set record_offset [expr {$record_offset + 2 + $tls_ciphlen}]
  69:                         binary scan [TCP::payload] @${record_offset}c tls_complen
  70:                         set record_offset [expr {$record_offset + 1 + $tls_complen}]
  72:                 # If we're in TLS and we've not parsed all the payload in the record
  73:                 # at this point, then we have TLS extensions to process. We will detect
  74:                 # the TLS extension package and parse each record individually.
  76:                         if { ([TCP::payload length] >= $record_offset) } {
  77:                                 binary scan [TCP::payload] @${record_offset}S tls_extenlen
  78:                                 set record_offset [expr {$record_offset + 2}]
  79:                                 binary scan [TCP::payload] @${record_offset}a* tls_extensions
  81:                 # Loop through the TLS extension data looking for a type 00 extension
  82:                 # record. This is the IANA code for server_name in the TLS transaction.
  84:                                 for { set x 0 } { $x < $tls_extenlen } { incr x 4 } {
  85:                                         set start [expr {$x}]
  86:                                         binary scan $tls_extensions @${start}SS etype elen
  87:                                         if { ($etype == "00") } {
  89:                 # A servername record is present. Pull this value out of the packet data
  90:                 # and save it for later use. We start 9 bytes into the record to bypass
  91:                 # type, length, and SNI encoding header (which is itself 5 bytes long), and
  92:                 # capture the servername text (minus the header).
  94:                                                 set grabstart [expr {$start + 9}]
  95:                                                 set grabend [expr {$elen - 5}]
  96:                                                 binary scan $tls_extensions @${grabstart}A${grabend} tls_servername
  97:                                                 set start [expr {$start + $elen}]
  98:                                         } else {
 100:                 # Bypass all other TLS extensions.
 102:                                                 set start [expr {$start + $elen}]
 103:                                         }
 104:                                         set x $start
 105:                                 }
 107:                 # Check to see whether we got a servername indication from TLS. If so,
 108:                 # make the appropriate changes.
 110:                                 if { ([info exists tls_servername] ) } {
 112:                 # Look for a matching servername in the Data Group and pool.
 114:                                         set ssl_profile [class match -value [string tolower $tls_servername] equals tls_servername]
 115:                                         set tls_pool [class match -value [string tolower $tls_servername] equals tls_servername_pool]
 117:                                         if { $ssl_profile == "" } {
 119:                 # No match, so we allow this to fall through to the "default"
 120:                 # clientssl profile.
 122:                                                 SSL::enable                                                
 123:                                         } else {
 125:                 # A match was found in the Data Group, so we will change the SSL
 126:                 # profile to the one we found. Hide this activity from the iRules
 127:                 # parser.
 129:                                                 set ssl_profile_enable "SSL::profile $ssl_profile"
 130:                                                 catch { eval $ssl_profile_enable }
 131:                                                 if { not ($tls_pool == "") } {
 132:                                                         pool $tls_pool
 133:                                                 } else {
 134:                                                         pool $default_tls_pool
 135:                                                  }
 136:                                                 SSL::enable
 137:                                         }
 138:                                 } else {
 140:                 # No match because no SNI field was present. Fall through to the
 141:                 # "default" SSL profile.
 143:                                         SSL::enable
 144:                                 }
 146:                         } else {
 148:                 # We're not in a handshake. Keep on using the currently set SSL profile
 149:                 # for this transaction.
 151:                                 SSL::enable
 152:                         }
 154:                 # Hold down any further processing and release the TCP session further
 155:                 # down the event loop.
 157:                         set detect_handshake 0
 158:                         TCP::release
 159:                 } else {
 161:                 # We've not been able to match an SNI field to an SSL profile. We will
 162:                 # fall back to the "default" SSL profile selected (this might lead to
 163:                 # certificate validation errors on non SNI-capable browsers.
 165:                 set detect_handshake 0
 166:                 SSL::enable
 167:                 TCP::release
 169:                 }
 170:         }
 171: }
Comments on this Article
Comment made 05-Apr-2011 by hoolio
Nice article and a great Codeshare contribution...

You could eliminate the need for the datagroup mapping hostnames to client SSL profile names if you name the client SSL profile with the hostname it in. In other words, you would assume for a hostname of: -> www.example.com_clientssl -> host1.example.com_clientssl -> mail.example.com_clientssl

Comment made 05-Apr-2011 by Colin Walker 3835
Definitely a possibility as long as you have simple 1:1 mappings. Good tip.

Comment made 21-Jul-2011 by edward 0
When I tried your solution, I saw the following error in ltm.log:

Jul 21 10:44:27 local/tmm4 err tmm4[6785]: 01220001:3: TCL error: acp_https_irules - bad option "-28159": must be -exact, -glob, -regexp, or -- while executing "switch $tls_version { "769" - "770" - "771" {

Is it because the client doesn't support TLS?
Comment made 26-Jul-2011 by Joel Moses 80
What are the specifics of the VS to which you applied the rule? It sounds like you may have applied it to one that is using passthrough SSL or is speaking a protocol other than SSL/TLS over the established connection.

Looking at this, I suspect I should modify the rule to ignore negative text strings or be a little more thorough about the pattern matching to avoid this happening in the future. Thanks for letting me know.
Comment made 04-Jun-2012 by Nicola_DT 116
tried this fantastic irule on V 11.1 HF2 and it does not work.

I have put some points of debug here and there and I see that the irule behaves correctly BUT when it comes to release the correct ssl profile for some reason it does not.

This is the code that has to be changed for V11, but I do not know how :/

129: set ssl_profile_enable "SSL::profile $ssl_profile"
130: catch { eval $ssl_profile_enable }

Can someone have a look into it ?

Thanx a lot,

PS on 10.2.3 it works perfectly :)

Comment made 10-Jul-2012 by Joel Moses 80

Change line 129 to:

set ssl_profile_enable "SSL::profile /Common/$ssl_profile"

and see if that fixes it.

In 11.x, the "folder" structure must be taken into account when activating profiles by name... If it DOES fix it, let me know and I'll update the code above.
Comment made 30-Aug-2012 by Alexey 0
People, how much CPU time does this irule take?
Comment made 30-Aug-2012 by Jason Rahm
that would depend on your platform. You can test by enabling timing in the irule by placing "timing on" at the first line of the iRule and then look at the cycles statistics on the rule in tmsh.
Comment made 30-Aug-2012 by hoolio
Also, in 11.1 and higher, there is native support for TLS SNI so you don't need to use an iRule:

Transport Layer Security Server Name Indication


Comment made 07-Nov-2012 by Zero 0
I was getting an error on line 114 and 115, then Kevin Stewart on the iRule forums noticed the problem. The brackets point the wrong way on your iRule posting:

[class match -value ]string tolower $tls_servername[

should be:

[class match -value [string tolower $tls_servername]
Comment made 23-Jun-2014 by lla0 0
Great article! I am a novice with f5, but I could manage my configuration with this example. Thanks a lot!
Comment made 27-Jan-2015 by jaria 0
This solved my problem, thanks. A small fix to avoid runtime TCL errors when the signed tls_version variable becomes negative: 38: switch $tls_version { to 38: switch -- $tls_version {
Comment made 15-Sep-2015 by vinzclortho 3
We have been advised that when using native SNI on a virtual server, adding or removing a ClientSSL Profile from the virtual will cause all certs on it to be removed and re-added, breaking connections to the VS. Is that the case with this iRule approach, or can that behavior be avoided when using this iRule?
Comment made 04-Sep-2017 by Raphael 1

Can SSL::sni make this iRule simpler?


Comment made 17-Sep-2017 by Stanislas Piron 10677

@Raphael, BIGIP supports SNI from version 11.1. from previous versions, this irule was the only solution to filter Servername

then, SSL::sin is only available from version 12.0, so between 11.1 and 12.0, to get Server name, you had to replace SSL::sni name by [string range [SSL::extensions -type 0] 9 end]

For your information, I tried [SSL::sni name] in version 13.0 HF2 and it did not return any value.

Comment made 17-Sep-2017 by Raphael 1

For [SSL::sni name] returning value, there should be value set in server name indication in the client ssl profile, therefore, this irule may be the only way to capture SNI before client ssl handshake currently.