SMTP & IMAP proxy based on domain (pass-through)

Hello,

My scenario is as follows:
I have a single server with multiple domains. For each domain I’d like to have a separate docker container (won’t go into reasons why I want this, but it does make sense) as an email server (postfix + dovecot). I’ve researched this extensively for months and believe this should be possible using haproxy.
I’d like to achieve this without ssl termination - basically using pass-though or in other words, read the TLS SNI header (domain) and decide based on that which upstream to forward the traffic to.
Something like this is even described on: https://www.haproxy.com/blog/enhanced-ssl-load-balancing-with-server-name-indication-sni-tls-extension/

I’m not sure if I’ve configured something wrong or am I completely missing something here?
If I set a default server, this works, but proxying based on domain (sni) does not.

My configuration is as follows:

defaults
    timeout client 30s
    timeout server 30s
    timeout connect 5s

    option tcplog
    log global


frontend smtp_submission

    mode tcp
    bind *:587

    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    use_backend smtp_submission


frontend imap

    mode tcp
    bind *:993

    tcp-request inspect-delay 5s
    tcp-request content accept if { req_ssl_hello_type 1 }

    use_backend imap


backend smtp_submission

    mode tcp

    acl mail_domain1_match req_ssl_sni -i smtp.domain1.com
    acl mail_domain2_match req_ssl_sni -i smtp.domain2.com

    use-server mail_domain1_smtp_submission if mail_domain1_match
    use-server mail_domain2_smtp_submission if mail_domain2_match

    option ssl-hello-chk

    server mail_domain1_smtp_submission 172.17.0.12:587 weight 0
    server mail_domain2_smtp_submission 172.17.0.11:587 weight 0


backend imap

    mode tcp

    acl mail_domain1_match req_ssl_sni -i imap.domain1.com
    acl mail_domain2_match req_ssl_sni -i imap.domain2.com

    use-server mail_domain1_imap if mail_domain1_match
    use-server mail_domain2_imap if mail_domain2_match

    option ssl-hello-chk

    server mail_domain1_imap 172.17.0.12:993 weight 0
    server mail_domain2_imap 172.17.0.11:993 weight 0

No, this is not really possible, because you have to support a) cleartext and b) STARTTLS, neither allows SNI based routing decisions, because the SNI value, if it exist at all, is not in the first packet.

You can only do this with implicit TLS, meaning imaps on TCP Port 993 and SMTP submission with implicit TLS on port 465.

Port 587 is unencrypted until you start the TLS session with STARTTLS. It’s therefor impossible to route based on SNI.

I don’t think SNI contains the domain. It usually contains the hostname you are connecting to.

I’m also not sure if MUA’s really do SNI, and if it’s supported widely enough so that it’s actually usable.

About the first packet part, I was under the assumption that “tcp-request inspect-delay 5s” would help with waiting for the sni packet, maybe not. I haven’t tried smtp 465. I’ve encountered issues with port 993 mostly.

How would you suggest I go by making this work? Using certificates to decrypt and reading host and routing based on that? Should traffic be re-encrypted or not after this? Any tips?

Waiting does not fix the issue. This is a catch-22:

You need a information that only appears on the wire when there is already a bidirectional communication channel established between the client and the backend server, however you need that information in the very beginning to decide which backend to choose.

When you make the routing decision, you need to have the information. But you only have the information when you already made the routing decision. So this will never work for explicit TLS (or plaintext).

This only works for HTTPS and implicit TLS based protocols, because the very first packet is a client_hello that contains the SNI value. It cannot work if the SNI value appears in some a protocol conversation further down the road, when the connection is already setup.

No, there is no Host header here. This is not HTTP. And we cannot make routing decisions based on IMAP or SMTP protocol conversations either, because they too, just like SNI, come after the load-balancing decision.

Haproxy is a TCP and HTTP reverse proxy and load-balancer. It will never do this.

You need an application that knows about IMAP and SMTP and actively speaks those protocols. I suggest you research this topic in mail related forums, but frankly I doubt that this is a common configuration. Try researching nginx smtp proxy modules and dovecot’s proxying functionality.

But haproxy is definitely the wrong tool for this job.

As Lukas said, talking to a mail client is easy enough, they all speak SSL-encrypted IMAP and SMTP (and a variety of other protocols), and they all send a SNI-header that you can use to select a docker container. Not a problem to use haproxy there.

Do you need to receive email too?

There no longer exists a standardized and registered port for SMTP over SSL for the purposes of MTA-to-MTA communication.

To receive email from the outside world, you therefore need a listener on a plaintext port instead (namely port 25).

Basically all senders that you care about today will still use SSL, but… You first need to do a bunch of plaintext handshake before SSL is even started:

  • send an SMTP greeting (plaintext), wait for an EHLO command (plaintext), send a feature announcement containing STARTTLS (plaintext), wait for a STARTTLS command (plaintext), send an OK (plaintext), and then starts the SSL. 9 out of 10 MTA software will then send SNI headers, with the same name as the MX record pointing to the service, and you can use that for routing.

But there is one notable exception: Postfix.

To receive from Postfix you have to send it a random certificate and decrypt the communication on the reverse proxy, and do a full plaintext SMTP exchange, or at least buffer the MAIL command, read the RCPT command, and see what domain Postfix wants to send to.

And you have to be careful to avoid having any DANE or similar records in DNS (essentially running your own CA - sort of), otherwise Postfix will try and validate whatever random certificate you throw at it using those DNS records.

It’s doable, but it is perhaps easier to just install mail routing software on the reverse proxy next to the HAproxy software.

Not sure I agree, it’s perfectly fine if the purpose is just for email clients to reach docker containers.

For MTA-to-MTA, I agree, HAproxy is probably not the right tool.

(To talk the variety of STARTTLS-based plaintext-first protocols, haproxy would need to wait with SSL establishment until the plaintext handshake is done. And “something” would have to do the plaintext bits first, perhaps a lua script.)