Securing LDAP with LDAPS via Traefik

TLDR; Scroll to the last section for setup instructions. Other sections detail why other approaches don’t work and why.

Summary: We want to traefik to get SSL certificates and present a public-facing LDAPS service backed by an LDAP service running inside a docker network. Most people use traefik to reverse proxy and handle certs for HTTPS/HTTP traffic, we want to do the same with LDAPS/LDAP.

The Desired Setup

Warning: Make sure there isn’t a Cloudflare Proxy on your desired LDAPS domain. Cloudflare will not proxy TCP traffic!

                       +-----------------------------------------------------+
                       |  +------------------------------------------------+ |
                       |  | +-----------+           +-------------+        | |
+----------+     +----------+-+         |     +-----+-----+       |        | |
| Internet | --> | LDAPS Port |         | --> | LDAP Port |       |        | |
+----------+     +------------+ traefik |     +-----------+ lldap |        | |
                       |  | | Container |           |   Container | Docker | |
                       |  | +-----------+           +-------------+ Env    | |
                       |  +------------------------------------------------+ |
                       |                                                Host |
                       +-----------------------------------------------------+

The diagram above shows the desired setup. The image depicts two docker containers, traefik and lldap, running on a host. There is an open port on our host, the LDAPS port, which passes traffic to the traefik docker container via published port, which is further passed along to a traefik TCP entrypoint. From there, traefik will terminate the TLS TCP traffic and forward unencrypted TCP traffic to the lldap container’s LDAP port. Note that lldap’s LDAP port remains unexposed to the wider internet.

What Does Not Work

Sample LDAPSearch command, for convenience:

ldapsearch -H ldaps://lldap.example.com:636 -LLL -D "uid=some_user_name,ou=people,DC=example,DC=com" -w 'password' -s "One" -b "dc=example,dc=com"

Say we have the following for entryPoints and certificatesResolvers in our traefik.yaml:

entryPoints:
  ldapsecure:
    address: :636
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: :443

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /var/traefik/certs/letsencrypt-acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

One might naively add the following labels to lldap container:

traefik.tcp.routers.lldap.rule=HostSNI(`lldap.example.com`)
traefik.tcp.routers.lldap.entrypoints=ldapsecure
traefik.tcp.routers.lldap.tls=true
traefik.tcp.routers.lldap.tls.certresolver=letsencrypt
traefik.tcp.services.lldap.loadbalancer.server.port=3890 # lldap, by default, uses 3890 as LDAP port

This won’t always work, since some legacy LDAP clients will not pass Server Name Indications (such as ldapsearch). You can tell if this is the case if you see the following in traefik debug logs Serving default certificate for request: "". We can set the router rule to HostSNI(`*`) so that all traffic on ldapsecure entrypoint goes to our lldap container. We can also set up a default SSL cert in traefik.yaml

Solution

Put together, we want a traefik.yaml similar to:

entryPoints:
  ldapsecure:
    address: :636
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: :443

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /var/traefik/certs/letsencrypt-acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"
tls:
  stores:
    default:
      defaultGeneratedCert:
        resolver: letsencrypt
        domain:
          main: example.com
          sans:
            - lldap.example.com

Set lldap container’s labels to:

traefik.tcp.routers.lldap.rule=HostSNI(`*`)
traefik.tcp.routers.lldap.entrypoints=ldapsecure
traefik.tcp.routers.lldap.tls=true
traefik.tcp.routers.lldap.tls.certresolver=letsencrypt
traefik.tcp.services.lldap.loadbalancer.server.port=389 # lldap, by default, uses 3890 as LDAP port

What About Cloudflare?

Neither Cloudflare proxy nor Cloudflare tunnels can be used to pass LDAPS TCP traffic over the public internet. Cloudflare Proxy refuses to forward non-http/https traffic. You cannot use the other ports Cloudflare Proxy accepts, as it’s not a passthrough. Cloudflare Tunnels will only forward TCP data if you set up cloudflared on the both the host and the client.