STARTTLS Server SMTP with cleartext and STARTTLS client support

Problem this snippet solves:

We were looking at our O365 security score and our SMTP scores were pretty sad, so I looked at how I could create a STARTTLS connection up to O365 regardless of the client support. Originally I just supported cleartext clients, but I was able to get it to support clients running STARTTLS too. Links in the iRule to some helpful threads I used while developing this.

How to use this snippet:

Attach this iRule to a SMTP VS with a clientssl, serverssl and a SMTP profile.

Code :

# This iRule allows dynamic cleartext and STARTTLS client connection with a STARTTLS connection to the upstream server
# unfortunately, cleartext to server connections do not work with the iRule as-is
when CLIENT_ACCEPTED {
    # No SSL client side, also check no SSL running already on server side
    # Debug mode
set DEBUG 1
set SERVER_SSL 0
set CLIENT_SSL 0
set EHLO_Name "smtp.domain.com"
    if { $DEBUG } { log local0. "CLIENT_ACCEPTED" }
    SSL::disable serverside 
    # Disable All TLS so we can dynamically enable it
    SSL::disable 
}
when SERVER_CONNECTED {
    if { $DEBUG } { log local0. "SERVER_CONNECTED" }
    # Start collecting from the server because in SMTP the server responds first
    TCP::collect
}

when CLIENT_DATA {
    set lcpayload [string tolower [TCP::payload]]
    if { $DEBUG } { log local0.debug "CLIENT_DATA - PAYLOAD - $lcpayload" }
    if { $lcpayload starts_with "ehlo" } {
        if { $DEBUG } { log local0.debug "CLIENT_DATA - ehlo" }
        # https://devcentral.f5.com/s/articles/offload-smtp-encryption-via-irules
        # Spoof back STARTTLS headers to the client so they'll tell us if the support it or not
        TCP::respond "250-STARTTLS\r\n250 OK\r\n"
        TCP::payload replace 0 [TCP::payload length] ""
        TCP::release
        TCP::collect
    } elseif { $lcpayload starts_with "starttls" } {
        if { $DEBUG } { log local0.debug "CLIENT_DATA - Starttls" }
        # https://devcentral.f5.com/s/articles/smtp-start-tls
        # Spoof back the 'Ready to start TLS' header to the client so we can do client<->F5 SSL
        TCP::respond "220 Ready to start TLS\r\n"
        TCP::payload replace 0 [TCP::payload length] ""
        TCP::release
        clientside {SSL::enable}
    } elseif { $lcpayload starts_with "rset" and $SERVER_SSL == 1 } {
        if { $DEBUG } { log local0.debug "CLIENT_DATA - Client RSET" }
        # In situations where the client is not encrypting, but the server is, it appears nessecary to reset and re-EHLO to the SMTP server.
        # In this case, these are the advertisements from the O365 relay so you may have to adjust them according to your SMTP server responses.
        # This can be attained by setting DEBUG variable to '1' and watching /var/log/ltm when you send a message for the response returned from 
        # the SMTP server for 'Hello'.
        # Example: Rule /Common/SMTP_STARTTLS : server SSL payload: 250-blah.mail.protection.outlook.com Hello [removed-IP]  250-SIZE 157286400  250-PIPELINING  250-DSN  250-ENHANCEDSTATUSCODES  250-8BITMIME  250-BINARYMIME  250-CHUNKING  250 SMTPUTF8 
        TCP::respond "250-STARTTLS 250-SIZE 157286400  250-PIPELINING  250-DSN  250-ENHANCEDSTATUSCODES  250-8BITMIME  250-BINARYMIME  250-CHUNKING  250 SMTPUTF8\r\n"
        TCP::payload replace 0 [TCP::payload length] ""
        TCP::release
        TCP::collect
        # /var/log/ltm complains about this, but it appears to work
        serverside { SSL::respond "EHLO $EHLO_Name\r\n" }
    } else {
        if { $DEBUG } { log local0.debug "CLIENT_DATA - Default release" }
        TCP::release
    }
}
when SERVER_DATA {
    # Most of thise was helpfully taken from https://devcentral.f5.com/s/questions/need-an-irule-for-starttls-for-smtps-server-side-only-not-client-side
    # Read in responses from remote server into a variable and log to /var/log/ltm
    set payload [string tolower [TCP::payload]]
    if { $DEBUG } { log local0. "SERVER_DATA - PAYLOAD - $payload" }
    if {$payload starts_with "220" and $payload contains "esmtp"} {    
        # Listen for remote servers opening 220 and esmtp message 
        # NOTE the ‘if’ statement above may need to be tweaked to except what message the other 
        # side is actually sending in reply. Logs should show this.
        # Respond with a EHLO to server, most servers require a name after the EHLO as well.

        TCP::respond "EHLO $EHLO_Name\r\n" 
        TCP::payload replace 0 [TCP::payload length] ""
        TCP::release
        if { $DEBUG } { log local0. "SERVER_DATA - Responded to server with EHLO" }
        serverside {TCP::collect}
    } elseif {$payload contains "250-starttls" } {    
        # Check server responds with "250-starttls", if so, respond with a STARTTLS 
        TCP::respond "STARTTLS\r\n" 
        TCP::payload replace 0 [TCP::payload length] ""
        TCP::release
        if { $DEBUG } { log local0. "SERVER_DATA - Sent the server a STARTTLS" }
        serverside {TCP::collect}
    } elseif {$payload contains "220 ready for tls" or $payload contains "220 2.0.0 continue" or $payload contains "220 2.0.0 smtp server ready" } {    
        # if server gives a 220 response, then start server side ssl profile
        # NOTE the ‘if’ statement above may need to be tweaked to except what message the other 
        # side is actually sending in reply. Logs should show this.
        ######
        # O365 Edit - O365 returns 220 2.0.0 smtp server ready after enabling TLS - Adjust as needed
        ######
        if { $DEBUG } { log local0. "SERVER_DATA - server said he is ready for TLS, enable the SSL profile" }
        TCP::payload replace 0 [TCP::payload length] ""
        TCP::release
        serverside {SSL::enable}
        # TLS hanshake should now start, which is best seen in wireshark packet captures.
    } else {
        if { $DEBUG } { log local0.debug "SERVER_DATA - Default release" }
        TCP::release
        clientside { TCP::collect }
    }
}

when SERVERSSL_HANDSHAKE {
    # This will only trigger if that is completed successfully. 
        # ServerSSL profile will need a certificate to match the outbound IP and DNS name, 
        # and you may want to set the "Server certificate" setting to "require", 
        # and the "Trusted Certificate Authorities" set to "ca-bundle".
    if { $DEBUG } { log local0. "SERVERSSL_HANDSHAKE - SSL handshake completed." }
    set SERVER_SSL 1
    if { $CLIENT_SSL == 1 } {
        if { $DEBUG } { log local0.debug "SERVERSSL_HANDSHAKE - Client respond SSL" }
        clientside { SSL::respond "220 SMTP ESMTP Relay F5\r\n" }
    } else {
        if { $DEBUG } { log local0.debug "SERVERSSL_HANDSHAKE - Client respond TCP" }
        clientside { TCP::respond "220 SMTP ESMTP Relay F5\r\n" }
        # Give the client side a chance to STARTTLS
        clientside { TCP::collect }
    }
    if { $DEBUG } { log local0.debug "SERVERSSL_HANDSHAKE - SSL collect" }
    SSL::collect
}

when CLIENTSSL_HANDSHAKE {
    # This will only trigger if that is completed successfully. 
        # ServerSSL profile will need a certificate to match the outbound IP and DNS name, 
        # and you may want to set the "Server certificate" setting to "require", 
        # and the "Trusted Certificate Authorities" set to "ca-bundle".
    if { $DEBUG } { log local0. "SSL handshake completed." }
    set CLIENT_SSL 1
    SSL::collect
}

when SERVERSSL_DATA {
    # Log the SMTP responses to see any errors.
    if { $DEBUG } { log local0. "SERVERSSL_DATA - PAYLOAD - [SSL::payload]" }
    SSL::release
    if { $CLIENT_SSL == 0 } {
        if { $DEBUG } { log local0.debug "SERVERSSL_DATA - Client TCP Collect"}
        clientside { TCP::collect }
    }
    SSL::collect 
}

when CLIENTSSL_DATA {
    # Log the SMTP responses to see any errors.
    if { $DEBUG } { log local0. "CLIENTSSL_DATA - PAYLOAD - [SSL::payload]" }
    #log local0.debug "Clientssl_data - release"
    SSL::release
    SSL::collect
}
Published May 02, 2019
Version 1.0
  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    Hi Sam,

     

    Thanks for the info, very useful to know. I'm waiting for someone to do some testing so I'll keep an eye out for those errors.

     

    Which version of TMOS are you running?

  •  

     

    The issue I run into with SMTPS is that the LTM logs complain about not having a forward proxy license, and when I attempt to send messages to it I get an '01260009:4: Connection error: hud_ssl_handler:1170: invalid profile (40)'. Additionally, when I attempt to configure the iApp, I also get an error message regarding ssl forward proxy not being enabled. I believe this is part of SSL Orchestrator, but I do not have a license for that (which is funny because I have 1Gb VE Best).

     

    And I guess an added benefit of doing this in an iRule is that I can get some logging about what messages are sent, which I send over to Elasticsearch. That's already proven to be very handy as we transition to LTM with my iRule for compatibility with Office 365 relaying.

  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    I'll add that the deployment guide for the iApp mentions that an iRule was needed in version 11.4. From 11.5 the profile enables the functionality natively.

  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    Hi Sam,

     

    I'm about to try implementing the SMTP iApp to achieve what your iRule is doing. Having now stumbled upon your article, my question is, does the iApp not provide this functionality? Did you try the iApp and failed to get your scenario working?

     

    According to the documentation it appears the iApp should achieve this. I just took a look at the magic piece of the iApp (the SMTPS profile) and it seems to provide the configuration options for the opportunistic STARTTLS (i.e. STARTTLS Activation mode = None | Allow | Require)

     

    Thanks,

    Andrew

  • For those interested, here is the iRule I use for client (cleartext) -> Big-IP -> O365 (Starttls)

     

     

    when CLIENT_ACCEPTED {
        # No SSL client side, also check no SSL running already on server side
        # Serverside debug mode
        set DEBUG_SERVER 1
        # Clientside debug mode
        set DEBUG_CLIENT 1
        # Clientside body capturing
        set DEBUG_BODY 1
        # Variable to track if we've reached the message body
        set BODY_CHECK 0
        if { $DEBUG_CLIENT } { log local0. "CLIENT_ACCEPTED" }
        SSL::disable serverside 
        SSL::disable 
        #TCP::collect
    }
    when SERVER_CONNECTED {
        if { $DEBUG_SERVER } { log local0. "SERVER_CONNECTED" }
        TCP::collect 
    }
     
    when CLIENT_DATA {
        set lcpayload [string tolower [TCP::payload]]
        if { $DEBUG_BODY and $BODY_CHECK } 
        { 
            log local0.debug "CLIENT_DATA - [IP::client_addr] - BODY_PAYLOAD - $lcpayload" 
            set BODY_CHECK 0
        }
        if { $lcpayload starts_with "data" } { set BODY_CHECK 1 }
        #if { [TCP::payload] starts_with "MAIL FROM:" } { set $CFROM [TCP::payload] }
        if { $DEBUG_CLIENT and !$BODY_CHECK } { log local0.debug "CLIENT_DATA - [IP::client_addr] - PAYLOAD - $lcpayload" }
        TCP::release
    }
    when SERVER_DATA {
        # Read in responses from remote server into a variable and log to /var/log/ltm
        if { $DEBUG_SERVER } { log local0. "server payload: [string tolower [TCP::payload]]" }
        set payload [string tolower [TCP::payload]]
     
    if {$payload starts_with "220" and $payload contains "esmtp"}
        {    
            # Listen for remote servers opening 220 and esmtp message 
            # NOTE the ‘if’ statement above may need to be tweaked to except what message the other 
            # side is actually sending in reply. Logs should show this.
            # Respond with a EHLO to server, most servers require a name after the EHLO as well.
     
            TCP::respond "EHLO F5.yourdomain.com\r\n" 
            TCP::payload replace 0 [TCP::payload length] ""
            TCP::release
            if { $DEBUG_SERVER } { log local0. "responded to server with EHLO" }
            serverside {TCP::collect}
        }
    elseif {$payload contains "250-starttls" }
        {    
            # Check server responds with "250-starttls", if so, respond with a STARTTLS 
            TCP::respond "STARTTLS\r\n" 
            TCP::payload replace 0 [TCP::payload length] ""
            TCP::release
            if { $DEBUG_SERVER } { log local0. "Sent the server a STARTTLS" }
            serverside {TCP::collect}
        }
    elseif {$payload contains "220 ready for tls" or $payload contains "220 2.0.0 continue" or $payload contains "220 2.0.0 smtp server ready" }
        {    
            # if server gives a 220 response, then start server side ssl profile
            # NOTE the ‘if’ statement above may need to be tweaked to except what message the other 
            # side is actually sending in reply. Logs should show this.
            # O365 Edit - O365 returns 220 2.0.0 smtp server ready after enabling TLS
            if { $DEBUG_SERVER } { log local0. "server said he is ready for TLS, enable the SSL profile" }
            TCP::payload replace 0 [TCP::payload length] ""
            TCP::release
            serverside {SSL::enable}
            # TLS hanshake should now start, which is best seen in wireshark packet captures. 
        }
    }
     
    when SERVERSSL_HANDSHAKE {
        # This will only trigger if that is completed successfully. 
            # ServerSSL profile will need a certificate to match the outbound IP and DNS name, 
            # and you may want to set the "Server certificate" setting to "require", 
            # and the "Trusted Certificate Authorities" set to "ca-bundle".
        if { $DEBUG_SERVER } { log local0. "SSL handshake completed." }
        clientside { TCP::respond "220 SMTP ESMTP Relay F5\r\n" }
        SSL::collect
    }
     
    when SERVERSSL_DATA {
        # Log the SMTP responses to see any errors.
        if { $DEBUG_SERVER } { log local0. "server SSL payload: [SSL::payload]" }
        SSL::release
        clientside { TCP::collect }
        SSL::collect
    }

     

     

  • So just FYI, while this did work on a test client (I was using Nessus SMTP testing) other clients may not work because of the way they seem to handle sending commands. For example: Elastalert seems to send EHLO and that immediately after send the mail from: address, but a cleartext client needs to re-EHLO; while O365 does appear to accept this slight mis-ordering, when the client gets the command for data (354) it doesn't return any. So, you milage may vary.