TLS Connection fails with passthrough

Hey folks, i'm struggelig with this for hours, maybe someone can help me out:
I'm putting mailu (Mail framework for docker) behind traefik. Mailu will use letsencrypt to generate the certificates for itself and thus needs http-access to ./well-known and also a TCP router to the main TLS secured webservice.
My current setup is this.

traefik.yml:

api:
  dashboard: true                             # Enable the dashboard

log:
  filePath: "/logs/traefik.log"
  level: INFO

certificatesResolvers:
  letsEncrypt:
    acme:
      email: "letsEncrypt@maildomain"  # Email address used for registration
      storage: "/etc/traefik/acme/acme.json"    # File or key used for certificates storage
      caServer: https://acme-v02.api.letsencrypt.org/directory # prod (default)
      tlsChallenge: {}

entryPoints:
  http:
    address: ":80"                            # Create the HTTP entrypoint on port 80
    http:
      redirections:                           # HTTPS redirection (80 to 443)
        entryPoint:
          to: "https"                         # The target element
          scheme: "https"                     # The redirection target scheme
          priority: 10000                     # The Priority for the generated router
  https:
    address: ":443"                           # Create the HTTPS entrypoint on port 443

global:
  checknewversion: true                       # Periodically check if a new version has been released.
  sendanonymoususage: true                    # Periodically send anonymous usage statistics.

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"   # Listen to the UNIX Docker socket
    exposedByDefault: false                   # Only expose container that are explicitly enabled (using label traefik.enabled)
    network: "traefik-net"                    # Default network to use for connections to all containers.
    watch: true                               # Watch Docker Swarm events
  file:
    filename: "/etc/traefik/config.yml"       # Link to the dynamic configuration
    watch: true                               # Watch for modifications
  providersThrottleDuration: 10               # Configuration reload frequency

config.yaml:


tcp:
  middlewares:
    internal:
      ipAllowList:
        sourceRange:
          - "127.0.0.1/8"
          - "192.168.1.7"
          - "10.0.0.0/8"

http:
  middlewares:
    # A basic authentification middleware, to protect the Traefik dashboard to anyone except myself
    # Use with traefik.http.routers.myRouter.middlewares: "traefikAuth@file"
    traefikAuth:
      basicAuth:
        users:
          - "admin:$apr1$ccwG8qS8$bAyTry5OU00VbG2XAgjBG0"
    internal:
      ipAllowList:
        sourceRange:
          - "127.0.0.1/8"
          - "192.168.1.7"
          - "10.0.0.0/8"
    ratelimit:
      rateLimit:
        average: 100
        burst: 200

    # Recommended default middleware for most of the services
    # Use with traefik.http.routers.myRouter.middlewares: "default@file"
    # Equivalent of traefik.http.routers.myRouter.middlewares: "default-security-headers@file,error-pages@file,gzip@file"
    default:
      chain:
        middlewares:
          - default-security-headers
          # - error-pages
          - gzip

    # Add automatically some security headers
    # Use with traefik.http.routers.myRouter.middlewares: "default-security-headers@file"
    default-security-headers:
      headers:
        browserXssFilter: true                            # X-XSS-Protection=1; mode=block
        contentTypeNosniff: true                          # X-Content-Type-Options=nosniff
        forceSTSHeader: true                              # Add the Strict-Transport-Security header even when the connection is HTTP
        frameDeny: true                                   # X-Frame-Options=deny
        referrerPolicy: "strict-origin-when-cross-origin"
        sslRedirect: true                                 # Allow only https requests
        stsIncludeSubdomains: true                        # Add includeSubdomains to the Strict-Transport-Security header
        stsPreload: true                                  # Add preload flag appended to the Strict-Transport-Security header
        stsSeconds: 63072000                              # Set the max-age of the Strict-Transport-Security header (63072000 = 2 years)

    # Enables the GZIP compression (https://docs.traefik.io/middlewares/compress/)
    #   if the response body is larger than 1400 bytes
    #   if the Accept-Encoding request header contains gzip
    #   if the response is not already compressed (Content-Encoding is not set)
    # Use with traefik.http.routers.myRouter.middlewares: "gzip@file"
    gzip:
      compress: {}

# See https://doc.traefik.io/traefik/https/tls/
tls:
  options:
    # To use with the label "traefik.http.routers.myrouter.tls.options=modern@file"
    modern:
      minVersion: "VersionTLS13"                          # Minimum TLS Version
      sniStrict: true                                     # Strict SNI Checking
    
    # To use with the label "traefik.http.routers.myrouter.tls.options=intermediate@file"
    intermediate:
      cipherSuites:
        - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
        - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
        - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
        - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
      minVersion: "VersionTLS12"                          # Minimum TLS Version
      sniStrict: true                                     # Strict SNI Checking
    
    # To use with the label "traefik.http.routers.myrouter.tls.options=old@file"
    old:
      cipherSuites:
        - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
        - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
        - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
        - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
        - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256"
        - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"
        - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"
        - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
        - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"
        - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"
        - "TLS_RSA_WITH_AES_128_GCM_SHA256"
        - "TLS_RSA_WITH_AES_256_GCM_SHA384"
        - "TLS_RSA_WITH_AES_128_CBC_SHA256"
        - "TLS_RSA_WITH_AES_128_CBC_SHA"
        - "TLS_RSA_WITH_AES_256_CBC_SHA"
        - "TLS_RSA_WITH_3DES_EDE_CBC_SHA"
      minVersion: "TLSv1"                                 # Minimum TLS Version
      sniStrict: true                                     # Strict SNI Checking

docker-compose.yaml (only the container in question as it is long):

# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for compose flavor

services:

  # External dependencies
  redis:
    image: redis:alpine
    restart: always
    volumes:
      - "/mailu/redis:/data"
    depends_on:
      - resolver
    dns:
      - 192.168.203.254
[...]
# Core services
  front:
    image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}nginx:${MAILU_VERSION:-2024.06}
    restart: always
    env_file: stack.env
    logging:
      driver: journald
      options:
        tag: mailu-front
    # ports:
    #   - "127.0.0.1:80:80"
    #   - "127.0.0.1:443:443"
    #   - "127.0.0.1:25:25"
    #   - "127.0.0.1:465:465"
    #   - "127.0.0.1:587:587"
    #   - "127.0.0.1:110:110"
    #   - "127.0.0.1:995:995"
    #   - "127.0.0.1:143:143"
    #   - "127.0.0.1:993:993"
    #   - "127.0.0.1:4190:4190"
    networks:
      - default
      - traefik-net
    volumes:
      - "/mailu/certs:/certs"
      - "/mailu/overrides/nginx:/overrides:ro"
    depends_on:
      - resolver
    dns:
      - 192.168.203.254
    labels:
      traefik.enable: "true"

      # second part is important to ensure Mailu can get all certificates from letsencrypt for all the hostnames
      traefik.http.routers.mailuweb.rule: "(Host(`maildomain`) || HOST(`domain1`)) && PathPrefix(`/.well-known/acme-challenge/`)"
      traefik.http.routers.mailuweb.entrypoints: "http"
      traefik.http.routers.mailuweb.priority: 20000
      traefik.http.services.mailuweb.loadbalancer.server.port: "80"

      # other FQDNS can be added here:
      traefik.tcp.routers.mailuhttps.rule: "HostSNI(`maildomain`)"
      traefik.tcp.routers.mailuhttps.entrypoints: "https"
      traefik.tcp.routers.mailuhttps.middlewares: "internal@file"
      traefik.tcp.routers.mailuhttps.tls.passthrough: "true"
      traefik.tcp.routers.mailuhttps.tls.options: "intermediate@file"
      traefik.tcp.routers.mailuhttps.service: "mailuadmin"
      traefik.tcp.services.mailuadmin.loadbalancer.server.port: "443"
      traefik.tcp.services.mailuadmin.loadbalancer.proxyProtocol.version: "2"

      # traefik.tcp.routers.smtp.rule: "HostSNI(`*`)"
      # traefik.tcp.routers.smtp.entrypoints: "smtp"
      # traefik.tcp.routers.smtp.service: "smtp"
      # traefik.tcp.services.smtp.loadbalancer.server.port: "25"
      # traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version: "2"

      # traefik.tcp.routers.submissions.rule: "HostSNI(`*`)"
      # traefik.tcp.routers.submissions.entrypoints: "submissions"
      # traefik.tcp.routers.submissions.service: "submissions"
      # traefik.tcp.services.submissions.loadbalancer.server.port: "465"
      # traefik.tcp.services.submissions.loadbalancer.proxyProtocol.version: "2"

      # traefik.tcp.routers.imaps.rule: "HostSNI(`*`)"
      # traefik.tcp.routers.imaps.entrypoints: "imaps"
      # traefik.tcp.routers.imaps.service: "imaps"
      # traefik.tcp.services.imaps.loadbalancer.server.port: "993"
      # traefik.tcp.services.imaps.loadbalancer.proxyProtocol.version: "2"
    healthcheck:
      test: ['NONE']
[...]

The network traefik-net is generated externally (in portainer), traefik itself is deployed via docker in portainer as well.

The problem is: The container can get a certificate via acme-challenge but when I try to connect to it, Firefox says: PR_END_OF_FILE_ERROR or SSL_ERROR_RX_RECORD_TOO_LONG.
In curl it looks like this:

curl -vv https://maildomain/
* Host maildomain:443 was resolved.
* IPv6: 2003:xxxxxxxx
* IPv4: 124.xxxx
*   Trying [2003:xxxxxxxx]:443...
* Connected to maildomain (2003:xxxxxxxx) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version number
* Closing connection
curl: (35) OpenSSL/3.0.13: error:0A00010B:SSL routines::wrong version number

I checked from inside the actual traefik-docker and it seems, traefik can connect to docker with no problems at all:

/ # wget --no-check-certificate https://172.18.0.3
Connecting to 172.18.0.3 (172.18.0.3:443)
Connecting to 172.18.0.3 (172.18.0.3:443)
saving to 'index.html'
index.html           100% |********************************************************************************************************************************************|  7151  0:00:00 ETA
'index.html' saved

Mailu itself gives this output:

172.18.0.2 - - [25/Apr/2025:19:27:14 +0000] "\x00" 400 150 "-" "-"
172.18.0.2 - - [25/Apr/2025:19:27:14 +0000] "\x00" 400 150 "-" "-"
172.18.0.2 - - [25/Apr/2025:19:28:05 +0000] "\x00" 400 150 "-" "-"
172.18.0.2 - - [25/Apr/2025:19:28:05 +0000] "\x00" 400 150 "-" "-"
172.18.0.2 - - [25/Apr/2025:19:29:00 +0000] "\x00" 400 150 "-" "-"
172.18.0.2 - - [25/Apr/2025:19:29:00 +0000] "\x00" 400 150 "-" "-"

I've fiddled around with the configs a lot but cannot seem to find any hint on why whis fails. When putting traefik into debug-mode i get this:

2025-04-26T09:15:31+02:00 DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:41 > Handling TCP connection address=172.18.0.3:443 remoteAddr=10.0.99.10:35978
2025-04-26T09:15:31+02:00 DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:104 > Error while terminating TCP connection error="close tcp 172.18.0.2:55184->172.18.0.3:443: use of closed network connection"
2025-04-26T09:15:31+02:00 DBG github.com/traefik/traefik/v3/pkg/middlewares/tcp/ipallowlist/ip_allowlist.go:60 > Connection from 10.0.99.10:35990 accepted middlewareName=internal@file middlewareType=IPAllowListerTCP
2025-04-26T09:15:31+02:00 DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:41 > Handling TCP connection address=172.18.0.3:443 remoteAddr=10.0.99.10:35990
2025-04-26T09:15:31+02:00 DBG github.com/traefik/traefik/v3/pkg/tcp/proxy.go:104 > Error while terminating TCP connection error="close tcp 172.18.0.2:55192->172.18.0.3:443: use of closed network connection"

That is really odd, as the connection is working (like i checked from the container). Can anyone help me out on why this is failing?

In general I would use a http router for anything http related. You can have a websecure/https entrypoint using tlsChallengeand still have a service behind using httpChallenge.

Note that there is an official mailu Traefik doc.

Thanks a lot. It was actually a problem with the mailu-server behind the proxy. It was not proxy aware. Thanks for your help!