mailcow / mailcow-dockerized

mailcow: dockerized - ๐Ÿฎ + ๐Ÿ‹ = ๐Ÿ’•
https://mailcow.email
GNU General Public License v3.0
8.73k stars 1.17k forks source link

Please add Traefik support/documentation #667

Closed michacassola closed 6 years ago

michacassola commented 6 years ago

Hi,

I didn't see Traefik in this documentation: https://mailcow.github.io/mailcow-dockerized-docs/firststeps-rp/ Can you please add it? Thank you in advance!

mkuron commented 6 years ago

Feel free to submit a pull request to the docs repository if you have a working configuration. It canโ€˜t hurt to have documentation on another possible reverse proxy.

Braintelligence commented 6 years ago

Never heard of it, thanks for pointing it out. Sounds interesting!

michacassola commented 6 years ago

I stumble upon a lot of tutorials with using treafik for their docker reverse proxy needs. Thats why I wanted to ask to include it. Unfortunately I am not code fit yet to help out with making a documentation. I am still learning myself and was hoping to host some other docker stuff on the mailcow server. Thanks for your quick responses!

svengo commented 6 years ago

Integrating mailcow in traefik is rather simple.

  1. Comment out the two ports in the nginx-mailcow section in docker-compose.yml.
  2. Use the following docker-compose.override.yml or add the statements to the original docker-compose.yml (change traefik_web to your trafik-network):
version: '2.3'

services:
    nginx-mailcow:
      expose:
         - "80"
      labels:
        - "traefik.backend=mailcow"
        - "traefik.docker.network=traefik_web"
        - "traefik.enable=true"
        - "traefik.port=80"
        - "traefik.frontend.rule=Host:${MAILCOW_HOSTNAME}"
      networks:
        web:

networks:
  web:
    external:
      name: traefik_web
michacassola commented 6 years ago

Thanks!

Braintelligence commented 6 years ago

This should be included in the docs ๐Ÿ˜ธ

michacassola commented 6 years ago

Maybe add some SSL support with Lets Encrypt please?

MichelDiz commented 6 years ago

Not coming from an expert, but... This is wrong. If it was to do some real support, it would have to be direct. And not this whole turns around. A reverse proxy from another proxy ... Makes no sense.

By doing this you can not take full advantage of Traefik. Like that Traefik can't balance the instances with autonomy for example. Doing it as shown above. this becomes unviable.

Traefik is a reverse proxy, in addition to balance and have integration with Prometheus. Generating good control and stats. Through it you can see the situation of the server in real time and advance the resolutions of failures. Has Encryption out of the box and Supports gRPC.

By the way, to give support with Traefik. You just need to pass the Labels on instances that need to have ports exposed externally. And configure Traefik to manage these ports. That would be port 80 and the other e-mail ports like IMAP, POP3 and so on.

Anyway, Traefik is much better option.

ledodev commented 6 years ago

Isnt Traefik http/s only? How do you proxy IMAPS, POP3S, SMTPS and Submission with it? AFAIK Traefik cant handle TCP.

Braintelligence commented 6 years ago

@ledodev Your comment would only make any sense if you would want to run multiple mail servers on the same IP address...

EDIT: I guess it was directed at @MichelDiz . I don't think Traefik can be used as a load-balancer for mail servers just like that.

ledodev commented 6 years ago

MichelDiz mentioned in his last comment he would proxy these ports. I was just curious how with Traefik.

Braintelligence commented 6 years ago

Yeah, I mentioned it in my edit. There's no point in trying to "load-balance" mail servers via domain-routing on traefik-level on the same IP address. It just makes no sense to me at all. Instead you would employ several MX records on the DNS of different priority and then keep the servers synchronized I think. @andryyy correct me if I'm wrong.

ledodev commented 6 years ago

@Braintelligence sorry i missed your edit before writing :) i think the same

To whom it might help:

  certdumper:
    container_name: traefik_certdumper
    image: alpine:latest
    depends_on:
      - traefik
    restart: unless-stopped
    volumes:
      - $PWD:/traefik
      - /opt/mailcow-dockerized/data/assets/ssl:/ssl
    command: >
      ash -c " \
        apk --no-cache add inotify-tools jq openssl util-linux bash && \
        wget https://raw.githubusercontent.com/containous/traefik/91ff94ea56587a3afa398b572bb7ae4185849091/contrib/scripts/dumpcerts.sh -O dumpcerts.sh && \
        mkdir -p /traefik/ssl/ && \
        while true; do \
          inotifywait -e modify /traefik/acme.json && \
            bash dumpcerts.sh /traefik/acme.json /traefik/ssl/ && \
            ln -f /traefik/ssl/certs/* /traefik/ssl/ && \
            ln -f /traefik/ssl/private/* /traefik/ssl/ && \
            mv /traefik/ssl/private/${MAILCOW_HOSTNAME}.key /ssl/key.pem && \
            mv /traefik/ssl/certs/${MAILCOW_HOSTNAME}.crt /ssl/cert.pem; \
        done"

The container watches acme.json and explodes it on change to certs&keys for each domain. After certs get splited push cert for ${MAILCOW_HOSTNAME} to /data/assets/ssl for reuse in SMTPS, IMAPS, Submission. Maybe Dovecot and Postfix need to be reloaded after pushing new cert, I dont know.

MichelDiz commented 6 years ago

In fact, at the time I said I had no idea that there was no support for email protocols. I went to question and I discovered that Traefik does not deal with these protocols other than gRPC. Sorry about that.

here: https://github.com/containous/traefik/issues/1047

I commented because I had seen you using nginx and using Traefik to point to it. For me this adds load to the server. I believe that nginx should also not deal with email protocols. I am wrong? (But as it is not being used for this, it would not come to the case.)

Keridos commented 5 years ago

How can I get this working when traefik is deployed with network_mode: bridge?

danse-gist commented 5 years ago

docker-compose override is a good choice for adding reverse proxy capabilities to mailcow, but there is a twist. Unfortunatly, it is not possible to override already declared options for a service or a service as a whole (like ports or volumes). Is it possible or feasable to export all custom options that we have into an docker-compose.override.yml and remove them from the main docker-compose?

For example, i use traefik for reverse proxying the web interface as well as mailcow's acme client. i can override every aspect of mailcow suited for my setup except mailcow-nginx's port definition.

nginx service in modified docker-compose.yml

    nginx-mailcow:
      depends_on:
        - sogo-mailcow
        - php-fpm-mailcow
        - redis-mailcow
      image: nginx:mainline-alpine
      command: /bin/sh -c "envsubst < /etc/nginx/conf.d/templates/listen_plain.template > /etc/nginx/conf.d/listen_plain.active &&
        envsubst < /etc/nginx/conf.d/templates/listen_ssl.template > /etc/nginx/conf.d/listen_ssl.active &&
        envsubst < /etc/nginx/conf.d/templates/server_name.template > /etc/nginx/conf.d/server_name.active &&
        envsubst < /etc/nginx/conf.d/templates/sogo.template > /etc/nginx/conf.d/sogo.active &&
        envsubst < /etc/nginx/conf.d/templates/sogo_eas.template > /etc/nginx/conf.d/sogo_eas.active &&
        nginx -qt &&
        until ping phpfpm -c1 > /dev/null; do sleep 1; done &&
        until ping sogo -c1 > /dev/null; do sleep 1; done &&
        until ping redis -c1 > /dev/null; do sleep 1; done &&
        until ping rspamd -c1 > /dev/null; do sleep 1; done &&
        exec nginx -g 'daemon off;'"
      environment:
        - HTTPS_PORT=${HTTPS_PORT:-443}
        - HTTP_PORT=${HTTP_PORT:-80}
        - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
        - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
        - TZ=${TZ}
      volumes:
        - ./data/web:/web:ro
        - ./data/conf/rspamd/dynmaps:/dynmaps:ro
        - ./data/assets/ssl/:/etc/ssl/mail/:ro
        - ./data/conf/nginx/:/etc/nginx/conf.d/:rw
        - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro
      volumes_from:
        - sogo-mailcow
      restart: always
      dns:
        - ${IPV4_NETWORK:-172.22.1}.254
      networks:
        mailcow-network:
          aliases:
            - nginx

configurable options should go in docker-compose.override.yml by default

    nginx-mailcow:
      expose:
        - "80"
     # ports:
     #   - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
     #   - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
danse-gist commented 5 years ago

my setup with traefik as reverseproxy. acme challenges are redirected to nginx-mailcow, so postfix, dovecot and so on are getting independed, valid certificates.

traefik.toml

defaultEntryPoints = ["http", "https"]

[entryPoints]
  [entryPoints.http]
    address = ":80"
    [entryPoints.http.redirect]
      entryPoint = "https"
  [entryPoints.https]
    address = ":443"
    [entryPoints.https.tls]

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "example.org"
watch = true
exposedbydefault = false

[acme]
email = "user@example.org"
storage = "/etc/traefik/acme.json"
#caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
entryPoint = "https"
acmeLogging = true
onHostRule = true

  [acme.dnsChallenge]
  # i have to use dns for wildcard certificates
  provider = "ovh"
  delayBeforeCheck = 5

[[acme.domains]]
  main = "*.example.org"
  sans = ["example.org"]

in docker-compose.yml, removed port definition for nginx-mailcow service but add an exposed port

[..]
nginx-mailcow:
[..]
    # ports:
     #   - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
     #   - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
    expose:
      - "80"
[..]

created docker-compose.override.yml

services:
  nginx-mailcow:
    # autodiscover is used by acme container
    labels:
      - "traefik.enable=true"
      - "traefik.https.frontend.rule=Host:example.org"
      - "traefik.docker.network=reverseproxy"
      - "traefik.autodiscover.frontend.rule=Host:autodiscover.example.org"
    networks:
      reverseproxy:
  # make rspam accesable, too
  rspamd-mailcow:
    labels:
      - "traefik.rspamd.frontend.rule=Host:example.org;Path:/rspamd"

networks:
  reverseproxy:
    external: yes
aronmgv commented 5 years ago

@svengo could you please help me how would this work if I am running traefik on host network directly? Below is my setup:

Traefik

docker-compose.yml

version: '3.7'

services:
  traefik:
    image: traefik
    container_name: traefik
    restart: always
    ports:
      - 80:80
      - 443:443
      - 8081:8081
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/traefik.toml:/traefik.toml
      - $PWD/acme.json:/acme.json
    labels:
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:traefik.example.com"
      - "traefik.port=8081"
    command:
      - "--logLevel=INFO"

traefik.toml

debug = false

logLevel = "INFO"

defaultEntryPoints = ["https","http"]

[entryPoints]
  [entryPoints.http]
    address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"

  [entryPoints.https]
    address = ":443"
    [entryPoints.https.tls]

  [entryPoints.traefik]
    address = ":8081"
    [entryPoints.traefik.auth.basic]
    users = ["admin:XXXXXXXXXXXXXXXXXX",]

[api]
  dashboard = true
  entryPoint = "traefik"

[retry]

[web]
  address = ":8081"

[docker]
  endpoint = "unix:///var/run/docker.sock"
  domain = "traefik.example.com"
  watch = true
  exposedByDefault = false

[acme]
  email = "admin@example.com"
  storage = "acme.json"
  entryPoint = "https"
  onHostRule = true
  acmeLogging = true
  KeyType = "RSA4096"
  [acme.httpChallenge]
  entryPoint = "http"

[[acme.domains]]
  main = "example.com"
  sans = [
    "example.com",
    "www.example.com",
    "mail.example.com",
    "gitlab.example.com",
    "cloud.example.com",
  ]

[file]

############
# Backends #
############

[backends]

#mailcow
#  [backends.mailcow]
#    [backends.mailcow.servers.1]
#      url = "http://127.0.0.1:52180"
#      weight = 1

#netdata
  [backends.netdata]
    [backends.netdata.servers.1]
      url = "http://127.0.0.1:19999"
      weight = 1

#discourse
  [backends.discourse]
    [backends.discourse.servers.1]
      url = "http://127.0.0.1:59180"
      weight = 1

#############
# Frontends #
#############

[frontends]

#mailcow
#  [frontends.mailcow]
#    backend = "mailcow"
#    entryPoints = ["http", "https"]
#    passHostHeader = true
#   [frontends.mailcow.headers]
#     forceSTSHeader = true
#     STSSeconds = 315360000
#     STSIncludeSubdomains=true
#     STSPreload = true
#    [frontends.mailcow.routes.1]
#      rule = "Host:mail.example.com"

#netdata
  [frontends.netdata]
    backend = "netdata"
    entryPoints = ["http", "https"]
    passHostHeader = true
    basicAuth = ["admin:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",]
    [frontends.netdata.headers]
      forceSTSHeader = true
      STSSeconds = 315360000
      STSIncludeSubdomains=true
      STSPreload = true
    [frontends.netdata.routes.1]
      rule = "Host:stats.example.com"

#discourse
  [frontends.discourse]
    backend = "discourse"
    entryPoints = ["http", "https"]
    passHostHeader = true
    [frontends.discourse.headers]
      forceSTSHeader = true
      STSSeconds = 315360000
      STSIncludeSubdomains=true
      STSPreload = true
    [frontends.discourse.routes.1]
      rule = "Host:forum.example.com"
svengo commented 5 years ago

@MacGyver27, I don't know why you configure traefik manually (and use host base networking), maybe you should follow the quick start guide and the reference first.

ntimo commented 5 years ago

But how does mailcow get the certificate that Traefik generated with acme?

svengo commented 5 years ago

I don't know if there is an easier way, but I am doing the following:

I am using acme-cert-dump-all.py called by /etc/cron.weekly/traefik-acme-certificate-update to extract the certificates from traefik:

#!/bin/sh

cert_dump="/usr/local/bin/acme-cert-dump-all.py"
acme_json="/opt/docker/volumes/traefik/acme.json"
cert_dir="/opt/docker/volumes/traefik/certs"

# clean up
rm --force ${cert_dir}/*

# dump certs
"${cert_dump}" "${acme_json}" "${cert_dir}" > /dev/null

# fix permissions
chmod 0700 "${cert_dir}"
find "${cert_dir}" -type f -exec chmod 0600 {} \;

# restart containers
postfix_c=$(docker ps -qaf name=postfix-mailcow)
dovecot_c=$(docker ps -qaf name=dovecot-mailcow)
nginx_c=$(docker ps -qaf name=nginx-mailcow)
docker restart "${postfix_c}" "${dovecot_c}" "${nginx_c}"

The certificate must be mounted in the containers so I added some lines to docker-compose.override.yml:

version: '2.1'

services:
  dovecot-mailcow:
    userns_mode: host
    volumes:
      - /opt/docker/volumes/traefik/certs/${MAILCOW_HOSTNAME}.pem:/etc/ssl/mail/mail.pem:ro

  postfix-mailcow:
    userns_mode: host
    volumes:
      - /opt/docker/volumes/traefik/certs/${MAILCOW_HOSTNAME}.pem:/etc/ssl/mail/mail.pem:ro

  nginx-mailcow:
    userns_mode: host
    networks:
      - web
    labels:
      - "traefik.backend=nginx-mailcow"
      - "traefik.docker.network=traefik_web"
      - "traefik.enable=true"
      - "traefik.port=8080"
      - "traefik.frontend.rule=Host:${MAILCOW_HOSTNAME}"
    volumes:
      - /opt/docker/volumes/traefik/certs/${MAILCOW_HOSTNAME}.pem:/etc/ssl/mail/mail.pem:ro

Dovecot must be configured to use the certificate, in data/conf/dovecot/extra.conf add

# SSL/TLS support: yes, no, required. <doc/wiki/SSL.txt>
ssl = required

ssl_cert = </etc/ssl/mail/mail.pem
ssl_key = </etc/ssl/mail/mail.pem

and for postfix, in data/conf/postfix/main.cf add

smtpd_tls_cert_file = /etc/ssl/mail/mail.pem
smtpd_tls_key_file = ${smtpd_tls_cert_file}
aronmgv commented 5 years ago

@svengo because some dockerized apps does not allow you to specify network they should connect to - simple as that.. one example is discourse.. then I just asked myself, why not to run in natively on host. Saw no conflict went ahead..

I managed to solve this think today and I think the best way so far possible.

All I did was put this adapter into traefik compose file, set it up it matches my setup of mailcow and traefik. I strongly recommend using SANs for your subdomains. Happened to me I run out of tries (50 generations per week, this way 1 cert is used also for the specified subdomains in SANs array - limit 100).

My final settings then:

Traefik

docker-compose.yml

version: '3.7'

services:
  traefik:
    image: traefik:latest
    container_name: traefik
    domainname: example.com
    restart: always
    ports:
      - 80:80
      - 443:443
      - 8081:8081
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /docker/.shared:/shared
      - $PWD/traefik/traefik.toml:/traefik.toml
      - $PWD/traefik/acme.json:/acme.json
      #- $PWD/traefik:/etc/traefik
    environment:
      - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL}
      - CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY}
    labels:
      - "traefik.enable=true"
      - "traefik.backend=traefik"
      - "traefik.frontend.rule=Host:traefik.example.com"
      - "traefik.port=8081"
      - "traefik.frontend.headers.SSLRedirect=true"
      - "traefik.frontend.headers.STSSeconds=315360000"
      - "traefik.frontend.headers.browserXSSFilter=true"
      - "traefik.frontend.headers.contentTypeNosniff=true"
      - "traefik.frontend.headers.forceSTSHeader=true"
      - "traefik.frontend.headers.SSLHost=example.com"
      - "traefik.frontend.headers.STSIncludeSubdomains=true"
      - "traefik.frontend.headers.STSPreload=true"
      - "traefik.frontend.headers.frameDeny=true"

  certdumper:
    container_name: traefik_mailcow_cert_adapter
    image: jovobe/mailcow-traefik-acme-adapter:latest
    depends_on:
      - traefik
    restart: always
    volumes:
      - ./traefik:/traefik:ro
      - /docker/mailcow/data/assets/ssl:/ssl-share:rw
      - /var/run/docker.sock:/var/run/docker.sock:rw
    environment:
      - DOMAIN=example.com

traefik.toml

debug = false

logLevel = "INFO"

defaultEntryPoints = ["https","http"]

[entryPoints]
  [entryPoints.http]
    address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"

  [entryPoints.https]
    address = ":443"
    [entryPoints.https.tls]

  [entryPoints.traefik]
    address = ":8081"
    [entryPoints.traefik.auth.basic]
    users = ["admin:XXXXXXXXXXXXXXXXXXXXXXXXX",]

[api]
  dashboard = true
  entryPoint = "traefik"

[retry]

[web]
  address = ":8081"

#[metrics]
#  [metrics.prometheus]
#    entryPoint = "traefik"
#   buckets = [0.1,0.3,1.2,5.0]

[docker]
  endpoint = "unix:///var/run/docker.sock"
  domain = "example.com"
  watch = true
  exposedByDefault = false

[acme]
  #caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" <- use for testing purposes!!!
  email = "admin@example.com"
  storage="acme.json"
  entryPoint = "https"
  onHostRule = true
  acmeLogging = true
  onDemand = false #create certificate when container is created
  [acme.httpChallenge]
    entryPoint = "http"
  [acme.dnsChallenge]
    provider = "cloudflare"
    delayBeforeCheck = 0

[[acme.domains]]
  main = "example.com"
  sans = [
    "www.example.com",
"chat.example.com",
"cloud.example.com",
"collabora.example.com",
"forum.example.com",
"gitlab.example.com",
"glances.example.com",
"mail.example.com",
"whoami.example.com"
  ]

[file]

############
# Backends #
############

[backends]

#mailcow
  [backends.mailcow]
    [backends.mailcow.servers.1]
      url = "http://127.0.0.1:52180"
      weight = 1eight = 1

#discourse
  [backends.discourse]
    [backends.discourse.servers.1]
      url = "http://127.0.0.1:59180"
      weight = 1

#############
# Frontends #
#############

[frontends]

#mailcow
  [frontends.mailcow]
    backend = "mailcow"
    entryPoints = ["http", "https"]
    passHostHeader = true
    [frontends.mailcow.headers]
      forceSTSHeader = true
      STSSeconds = 315360000
      STSIncludeSubdomains=true
      STSPreload = true
    [frontends.mailcow.routes.1]
      rule = "Host:mail.example.com"

#discourse
  [frontends.discourse]
    backend = "discourse"
    entryPoints = ["http", "https"]
    passHostHeader = true
    [frontends.discourse.headers]
      forceSTSHeader = true
      STSSeconds = 315360000
      STSIncludeSubdomains=true
      STSPreload = true
    [frontends.discourse.routes.1]
      rule = "Host:forum.example.com"
flexguse commented 5 years ago

@DanseAtEntropy: could you please explain which of your traefik.toml settings cause the acme redirect to mailcow? I use the acme.httpChallenge. With your settings the mailcow UI, rspamd and SoGO works fine, but postfix, dovecot and so on are NOT getting independent certificates but use self-signed certificates.

romprod commented 5 years ago

@DanseAtEntropy: could you please explain which of your traefik.toml settings cause the acme redirect to mailcow? I use the acme.httpChallenge. With your settings the mailcow UI, rspamd and SoGO works fine, but postfix, dovecot and so on are NOT getting independent certificates but use self-signed certificates.

Did you figure this out? I'm at this same point.

Edit: FYI I've gone a different route. Traefik v2 is getting my wildcard cert which applies against 443 and mailcow/acme gets the autodiscover,autocconfig,mail cert which only gets loaded by the imaps service currently (a different problem)

adrinux commented 4 years ago

@romprod Did you get everything working? I have a single server docker swarm with Traefik v2 automatic letsencrypt working from wildcard DNS and I'm looking to have mailcow-dockerized running along side it. I'm thinking it might be easier just to run certbot on the host and have a renew post-hook script copy the certs to a bind mount for whichever containers need them.

romprod commented 4 years ago

I just let the mailcow acme client grab the certs for the subdomains that it needed and left Traefik to grab a wildcard certificate which covered what was being passed through Traefik.

I had issues witht he mailcow cert and the HTTP challenge passing but I think that's down to HTTP redirect to HTTPS which looked like it was causing it to fail. In the end I found that I have somehow received certs for the mailcow stuff (but was still getting the HTTP challenge failed error) and everything was working.

I'll revisit the mailcow acme client in a few months when the cert needs renewing, maybe the acme will have been updated or fixed by then, or maybe I'll be able to spot the problem!

fabianschurz commented 4 years ago

You can pass through HTTP and HTTPS with the following labels on nginx-mailcow in mailcows docker-compose.yml:

        - traefik.enable=true
       # delete if you have a general https redirection in traefik before
       # otherwise you have to define the redirect for mailcow in mailcow-nginx
        - traefik.http.routers.mail.rule=Host(`mail.example.com`)
        - traefik.http.routers.mail.entrypoints=web
       # end of delete
        - traefik.tcp.routers.mailsecure.rule=HostSNI(`mail.example.com`)
        - traefik.tcp.routers.mailsecure.entrypoints=websecure
        - traefik.tcp.routers.mailsecure.tls.passthrough=true
        - traefik.tcp.services.mailsecure.loadbalancer.server.port=443

Additionally, remove the port bindings (to host ports) for nginx-mailcow.

After these changes, the traefik debug log indicated that traefik was catching LetsEncrypt's challenge call ("Error getting challenge for token retrying in...") instead of passing it to nginx from mailcow. We managed to let mailcow handle the certificates itself by switching to TLS challenges (instead of default HTTP) by using the following config:

[certificatesResolvers.le.acme]
  email = "mail@example.com"
  storage = "acme.json"
  [certificatesResolvers.le.acme.tlsChallenge]

When traefik uses this challenge type, it apparently does not try to respond to all urls containing ".well-known/acme-challenge" as it did before, so the challenge call from LE reaches the mailcow nginx. Alternatively, if you need to use validation via HTTP challenge in traefik, you could try to "convert" this to traefik 2.0 config format: https://gist.github.com/micw/67faf5cd3d4a6f64568ca2bb9a051230

For testing you should be using LE staging in order to not exceed the rate limits to fast

Braintelligence commented 4 years ago

@andryyy Can you confirm whenever you find the time if this configuration doesn't leave anything to be desired concerning the mailcow stack? From what you always wrote I could imagine that some kind of inter-container communication could make some trouble.

andryyy commented 4 years ago

I can't verify it. Someone needs to test it, confirm and add it to the docs. :) You can cross check it against the existing configuration presets. That would help me a lot!

fabianschurz commented 4 years ago

This is my current setup now, and right now everything is working. We will create some accounts and sign up on some newsletters to see if it's working nice over a period of 1-2weeks.

I think this solutions is far better than copying certs via any type of job since it lets mailcow handle certs itself.

By the way, if you still want to copy over the certs there is an official docker image called jobber. With jobber everything happens in the container, not on the host machine.

jdorel commented 4 years ago

For Traefik 2 and using SSL Passthrough:

In docker-compose.yml, comment the ports in nginx-mailcow.

In docker-compose.override.yml, put the following:

version: '2.1'

services:
    nginx-mailcow:
      labels:
        # Docker
        - "traefik.enable=true"
        - "traefik.docker.network=mailcow-network"

        # Routing HTTP (for acme challenge)
        - "traefik.http.routers.mailcow.entrypoints=http"
        - "traefik.http.routers.mailcow.rule=Host(`example.com`) || Host(`autoconfigure.example.com`)"
        - "traefik.http.services.mailcow.loadBalancer.server.port=80"

        # Routing HTTPs
        - "traefik.tcp.routers.mailcow.entrypoints=https"
        - "traefik.tcp.routers.mailcow.tls.passthrough=true"
        - "traefik.tcp.routers.mailcow.rule=HostSNI(`mail.example`) || HostSNI(`autodiscover.example.com`)"
        - "traefik.tcp.services.mailcow.loadBalancer.server.port=443"

Finally, configure the http and https endpoints in traefik.


EDIT: Fix according to comment below

arosenhagen commented 4 years ago

there's a typo in the above answer from @jdorel

        - "traefik.http.routers.mailcow.rule=Host(`mail.dorel.me`) || Host(`autoconfigure.dorel.me`)"
        - "traefik.http.services.mailcow.loadBalancer.server.port=80"
dm17 commented 3 years ago

For Traefik 2 and using SSL Passthrough:

In docker-compose.yml, comment the ports in nginx-mailcow.

In docker-compose.override.yml, put the following:

version: '2.1'

services:
    nginx-mailcow:
      labels:
        # Docker
        - "traefik.enable=true"
        - "traefik.docker.network=mailcow-network"

        # Routing HTTP (for acme challenge)
        - "traefik.http.routers.mailcow.entrypoints=http"
        - "traefik.http.routers.mailcow.rule=Host(`example.com`) || Host(`autoconfigure.example.com`)"
        - "traefik.http.services.mailcow.loadBalancer.server.port=80"

        # Routing HTTPs
        - "traefik.tcp.routers.mailcow.entrypoints=https"
        - "traefik.tcp.routers.mailcow.tls.passthrough=true"
        - "traefik.tcp.routers.mailcow.rule=HostSNI(`mail.example`) || HostSNI(`autodiscover.example.com`)"
        - "traefik.tcp.services.mailcow.loadBalancer.server.port=443"

Finally, configure the http and https endpoints in traefik.

EDIT: Fix according to comment below

You can pass through HTTP and HTTPS with the following labels on nginx-mailcow in mailcows docker-compose.yml:

        - traefik.enable=true
       # delete if you have a general https redirection in traefik before
       # otherwise you have to define the redirect for mailcow in mailcow-nginx
        - traefik.http.routers.mail.rule=Host(`mail.example.com`)
        - traefik.http.routers.mail.entrypoints=web
       # end of delete
        - traefik.tcp.routers.mailsecure.rule=HostSNI(`mail.example.com`)
        - traefik.tcp.routers.mailsecure.entrypoints=websecure
        - traefik.tcp.routers.mailsecure.tls.passthrough=true
        - traefik.tcp.services.mailsecure.loadbalancer.server.port=443

Additionally, remove the port bindings (to host ports) for nginx-mailcow.

After these changes, the traefik debug log indicated that traefik was catching LetsEncrypt's challenge call ("Error getting challenge for token retrying in...") instead of passing it to nginx from mailcow. We managed to let mailcow handle the certificates itself by switching to TLS challenges (instead of default HTTP) by using the following config:

[certificatesResolvers.le.acme]
  email = "mail@example.com"
  storage = "acme.json"
  [certificatesResolvers.le.acme.tlsChallenge]

When traefik uses this challenge type, it apparently does not try to respond to all urls containing ".well-known/acme-challenge" as it did before, so the challenge call from LE reaches the mailcow nginx. Alternatively, if you need to use validation via HTTP challenge in traefik, you could try to "convert" this to traefik 2.0 config format: https://gist.github.com/micw/67faf5cd3d4a6f64568ca2bb9a051230

For testing you should be using LE staging in order to not exceed the rate limits to fast

@jdorel @xFirestorm
Are the above your full changes on top of the latest mailcow compose file? If not, then would you mind posting them (and citing any additional magic that has them playing nicely)? Thanks for your work! :)

fabianschurz commented 3 years ago

Hey @dm17 Actually we didn't write further comments in this issue, because our setup is working nicely for about a year now. We have the current compose in use, we directly pull from git regularly and we are working with an overwrite compose file.

Our plan was to update the mailcow documentation for traefik 2 but sadly time is scarce and we didn't yet manage to do it and probably won't be able to in the near future. So if someone here is motivated to do so, feel free to use our setup as an example for the docs.

The only change to the original compose is commenting out the port bindings at line 339-341 (at the time of this comment). The output of git diff docker-compose.yml shows:

         - ./data/conf/nginx/:/etc/nginx/conf.d/:rw,Z
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
         - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/:z
-      ports:
-        - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
-        - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
+      #ports:
+      #  - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
+      #  - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
       restart: always
       networks:
         mailcow-network:

The overwrite compose file looks like this:

version: '2.1'

services:
  nginx-mailcow:
    networks:
      traefik:
    labels:
      - traefik.enable=true
      - traefik.http.routers.webmail-redirect.rule=Host(`webmail.example.com`)
      - traefik.http.routers.webmail-redirect.tls.certresolver=le
      - traefik.http.routers.webmail-redirect.middlewares=webmail-redirect
      - traefik.http.middlewares.webmail-redirect.redirectregex.regex=^https://webmail.example.com(.*)
      - traefik.http.middlewares.webmail-redirect.redirectregex.replacement=https://mail.example.com/SOGo$${1}
      - traefik.tcp.routers.mailsecure.rule=${TRAEFIK_TCP_RULE}
      - traefik.tcp.routers.mailsecure.entrypoints=websecure
      - traefik.tcp.routers.mailsecure.tls.passthrough=true
      - traefik.tcp.services.mailsecure.loadbalancer.server.port=12443

networks:
  mailcow-network:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-mailcow
    enable_ipv6: false
    ipam:
      driver: default
      config:
        - subnet: ${IPV4_NETWORK:-172.22.1}.0/24
        - subnet: ${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
  traefik:
    external: true

From our env: TRAEFIK_TCP_RULE=HostSNI(`mail.example.com`) || HostSNI(`autoconfig.example.com`) || HostSNI(`autodiscover.example.com`)

The traefik.toml file looks like this:

[global]
  checkNewVersion = true
  sendAnonymousUsage = true

[entryPoints]
  [entryPoints.web]
    address = ":80"
  [entryPoints.websecure]
    address = ":443"

[log]
  #level = "DEBUG"

[api]
  dashboard = true

[ping]

[providers]
  providersThrottleDuration = "10s"

[providers.docker]
  exposedByDefault = false
  network = "traefik"

[certificatesResolvers.le.acme]
  email = "certs@example.com"
  storage = "/certs/acme.json"
#  caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" -> If testing to ensure not being limited by le
  [certificatesResolvers.le.acme.tlsChallenge]
itsb commented 3 years ago

- traefik.tcp.services.mailsecure.loadbalancer.server.port=12443 does this imply that you set HTTPS_PORT to 12443 in your mailcow.conf? docker-compose ps only shows nginx listening on port 80. Also do the Host and HostSNI here match those on mailcow.conf? SKIP_ACME=n? Would be nice to see all files modifications together. Has anyone else got this working?

fabianschurz commented 3 years ago

@itsb Right, we changed the HTTPS_PORT to 12443 (HTTP_PORT to 1280) and HTTP_BIND and HTTPS_BIND to 127.0.0.1. 12443/1280 were picked randomly, just to avoid collisions with traefik bound to 80 and 443. As the nginx-mailcow container is using this port internally, we need to tell traefik which one it is (via the loadbalancer.server.port label). With changing the HTTP_BIND and HTTPS_BIND to 127.0.0.1, you can even leave the docker-compose.yml unchanged (no need to comment out the port bindings), as the service is then listening on the loopback interface and only available to the outside world via the reverse proxy. The other way round, if you change the docker-compose.yml, you can leave the mailcow.conf/.env unchanged (but remember to tell traefik it's port 443 and not port 12443 then).

You can see nginx listening on 12443 after running netstat -tulpn in docker exec -it {nginx_container} sh

The hostname in the first part of the traefik rule (HostSNI(mail.example.com)) is the same as the MAILCOW_HOSTNAME in mailcow.conf/.env

We didn't change SKIP_ACME and as mailcow handles its certs itself, ACME is actived (SKIP_ACME=n).