From 64ce3d1c968548dd23658d526fd29ca0d1e83f53 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 9 Aug 2023 15:28:07 +0200 Subject: [PATCH 01/10] Implement a busy loop for letsencrypt --- core/nginx/letsencrypt.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/core/nginx/letsencrypt.py b/core/nginx/letsencrypt.py index 993e7f9f..ddac05b1 100755 --- a/core/nginx/letsencrypt.py +++ b/core/nginx/letsencrypt.py @@ -1,9 +1,15 @@ #!/usr/bin/env python3 +import logging as log import os -import time +import requests +import sys import subprocess +import time +from threading import Thread +from http.server import HTTPServer, SimpleHTTPRequestHandler +log.basicConfig(stream=sys.stderr, level="WARNING") hostnames = ','.join(set(host.strip() for host in os.environ['HOSTNAMES'].split(','))) command = [ @@ -39,8 +45,25 @@ command2 = [ # Wait for nginx to start time.sleep(5) +def serve_one_request(): + with HTTPServer(("0.0.0.0", 8008), SimpleHTTPRequestHandler) as server: + server.handle_request() + # Run certbot every day while True: + while True: + hostname = os.environ['HOSTNAMES'].split(' ')[0] + target = f'http://{hostname}/.well-known/acme-challenge/testing' + thread = Thread(target=serve_one_request) + thread.start() + r = requests.get(target) + if r.status_code != 404: + log.error(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.") + time.sleep(5) + else: + break + thread.join() + subprocess.call(command) subprocess.call(command2) time.sleep(86400) From f3cd4014500803a2729511e09cedc8d52093b776 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 9 Aug 2023 15:28:25 +0200 Subject: [PATCH 02/10] PROXY_PROTOCOL=all-but-http for traefik --- core/nginx/conf/nginx.conf | 12 ++++++------ core/nginx/dovecot/proxy.conf | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index fdc3e171..3e91e998 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -22,7 +22,7 @@ http { {% if REAL_IP_HEADER %} real_ip_header {{ REAL_IP_HEADER }}; - {% elif PROXY_PROTOCOL in ['all', 'http'] %} + {% elif PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} real_ip_header proxy_protocol; {% endif %} @@ -104,9 +104,9 @@ http { # Only enable HTTPS if TLS is enabled with no error and not on kubernetes {% if not KUBERNETES_INGRESS and TLS and not TLS_ERROR %} - listen 443 ssl http2{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen 443 ssl http2{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:443 ssl http2{% if PROXY_PROTOCOL in ['all', 'http'] %} proxy_protocol{% endif %}; + listen [::]:443 ssl http2{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'http'] %} proxy_protocol{% endif %}; {% endif %} include /etc/nginx/tls.conf; @@ -315,7 +315,7 @@ mail { ssl_session_cache shared:SSLMAIL:3m; {% endif %} - {% if PROXY_PROTOCOL in ['all', 'mail'] and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} + {% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] and REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %} set_real_ip_from {{ from_ip }}; {% endfor %}{% endif %} @@ -324,9 +324,9 @@ mail { # SMTP is always enabled, to avoid losing emails when TLS is failing server { - listen 25{% if PROXY_PROTOCOL in ['all', 'mail'] %} proxy_protocol{% endif %}; + listen 25{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} proxy_protocol{% endif %}; {% if SUBNET6 %} - listen [::]:25{% if PROXY_PROTOCOL in ['all', 'mail'] %} proxy_protocol{% endif %}; + listen [::]:25{% if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} proxy_protocol{% endif %}; {% endif %} {% if TLS and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} diff --git a/core/nginx/dovecot/proxy.conf b/core/nginx/dovecot/proxy.conf index 4ab54005..e1e35600 100644 --- a/core/nginx/dovecot/proxy.conf +++ b/core/nginx/dovecot/proxy.conf @@ -73,7 +73,7 @@ service managesieve-login { executable = managesieve-login inet_listener sieve { port = 4190 -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } @@ -90,7 +90,7 @@ protocol imap { service imap-login { inet_listener imap { port = 143 -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } @@ -99,7 +99,7 @@ service imap-login { {%- if TLS %} ssl = yes {% endif %} -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } @@ -111,7 +111,7 @@ service imap-login { service pop3-login { inet_listener pop3 { port = 110 -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } @@ -120,7 +120,7 @@ service pop3-login { {%- if TLS %} ssl = yes {% endif %} -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } @@ -137,7 +137,7 @@ service lmtp { service submission-login { inet_listener submission { port = 587 -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } @@ -146,7 +146,7 @@ service submission-login { {%- if TLS %} ssl = yes {% endif %} -{%- if PROXY_PROTOCOL in ['all', 'mail'] %} +{%- if PROXY_PROTOCOL in ['all', 'all-but-http', 'mail'] %} haproxy = yes {% endif %} } From b7e7f0d8b6abbb25dc2ce47e565c24703bfa8e65 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 9 Aug 2023 15:38:27 +0200 Subject: [PATCH 03/10] doc --- docs/compose/traefik/docker-compose.yml | 144 -------------------- docs/compose/traefik/traefik.toml | 33 ----- docs/reverse.rst | 174 +++++++++++++++--------- 3 files changed, 110 insertions(+), 241 deletions(-) delete mode 100644 docs/compose/traefik/docker-compose.yml delete mode 100644 docs/compose/traefik/traefik.toml diff --git a/docs/compose/traefik/docker-compose.yml b/docs/compose/traefik/docker-compose.yml deleted file mode 100644 index 37e89827..00000000 --- a/docs/compose/traefik/docker-compose.yml +++ /dev/null @@ -1,144 +0,0 @@ -version: '2' - -services: - - # This would normally not be here, but where you define your system services - traefik: - image: traefik:alpine - command: --docker - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - - "/data/traefik/acme.json:/acme.json" - - "/data/traefik/traefik.toml:/traefik.toml" - # This may be needed (plus defining mailu_default external: true) if traefik lives elsewhere - # networks: - # - mailu_default - - certdumper: - restart: always - image: mailu/traefik-certdumper:$VERSION - environment: - # Make sure this is the same as the main=-domain in traefik.toml - # !!! Also don’t forget to add "TRAEFIK_DOMAIN=[...]" to your .env! - - DOMAIN=$TRAEFIK_DOMAIN - # Set TRAEFIK_VERSION to v2 in your .env if you're using Traefik v2 - - TRAEFIK_VERSION=${TRAEFIK_VERSION:-v1} - volumes: - - "/data/traefik:/traefik" - - "$ROOT/certs:/output" - - front: - image: mailu/nginx:$VERSION - restart: always - env_file: .env - labels: # Traefik labels for simple reverse-proxying - - "traefik.enable=true" - - "traefik.port=80" - - "traefik.frontend.rule=Host:$TRAEFIK_DOMAIN" - - "traefik.docker.network=mailu_default" - ports: - - "$BIND_ADDRESS4:110:110" - - "$BIND_ADDRESS4:143:143" - - "$BIND_ADDRESS4:993:993" - - "$BIND_ADDRESS4:995:995" - - "$BIND_ADDRESS4:25:25" - - "$BIND_ADDRESS4:465:465" - - "$BIND_ADDRESS4:587:587" - - "$BIND_ADDRESS6:110:110" - - "$BIND_ADDRESS6:143:143" - - "$BIND_ADDRESS6:993:993" - - "$BIND_ADDRESS6:995:995" - - "$BIND_ADDRESS6:25:25" - - "$BIND_ADDRESS6:465:465" - - "$BIND_ADDRESS6:587:587" - volumes: - - "$ROOT/overrides/nginx:/overrides" - - /data/traefik/ssl/$TRAEFIK_DOMAIN.crt:/certs/cert.pem - - /data/traefik/ssl/$TRAEFIK_DOMAIN.key:/certs/key.pem - - redis: - image: redis:alpine - restart: always - volumes: - - "$ROOT/redis:/data" - - imap: - image: mailu/dovecot:$VERSION - restart: always - env_file: .env - volumes: - - "$ROOT/mail:/mail" - - "$ROOT/overrides:/overrides" - depends_on: - - front - - smtp: - image: mailu/postfix:$VERSION - restart: always - env_file: .env - volumes: - - "$ROOT/overrides:/overrides" - depends_on: - - front - - antispam: - image: mailu/rspamd:$VERSION - restart: always - env_file: .env - volumes: - - "$ROOT/filter:/var/lib/rspamd" - - "$ROOT/dkim:/dkim" - - "$ROOT/overrides/rspamd:/etc/rspamd/override.d" - depends_on: - - front - - antivirus: - image: mailu/$ANTIVIRUS:$VERSION - restart: always - env_file: .env - volumes: - - "$ROOT/filter:/data" - - webdav: - image: mailu/$WEBDAV:$VERSION - restart: always - env_file: .env - volumes: - - "$ROOT/dav:/data" - - admin: - image: mailu/admin:$VERSION - restart: always - env_file: .env - volumes: - - "$ROOT/data:/data" - - "$ROOT/dkim:/dkim" - depends_on: - - redis - - webmail: - image: "mailu/$WEBMAIL:$VERSION" - restart: always - env_file: .env - volumes: - - "$ROOT/webmail:/data" - - "$ROOT/overrides/$WEBMAIL:/overrides:ro" - depends_on: - - imap - - fetchmail: - image: mailu/fetchmail:$VERSION - restart: always - env_file: .env - -networks: - default: - driver: bridge - ipam: - driver: default - config: - - subnet: $SUBNET diff --git a/docs/compose/traefik/traefik.toml b/docs/compose/traefik/traefik.toml deleted file mode 100644 index 7e09de58..00000000 --- a/docs/compose/traefik/traefik.toml +++ /dev/null @@ -1,33 +0,0 @@ -# This is just boilerplate stuff you probably have in your own config -logLevel = "INFO" -defaultEntryPoints = ["https","http"] - -[entryPoints] - [entryPoints.http] - address = ":80" - [entryPoints.http.redirect] - entryPoint = "https" - [entryPoints.https] - address = ":443" - [entryPoints.https.tls] - -[docker] -endpoint = "unix:///var/run/docker.sock" -watch = true -exposedByDefault = false - -# Make sure we get acme.json saved, and onHostRule enabled -[acme] -email = "your@mail.tld" -storage = "acme.json" -entryPoint = "https" -onHostRule = true - -[acme.httpChallenge] -entryPoint = "http" - -# This should include all of your mail domains, and main= should be your $TRAEFIK_DOMAIN -[[acme.domains]] - main = "mail.example.com" - sans = ["web.mail.example.com", "smtp.mail.example.com", "imap.mail.example.com"] - diff --git a/docs/reverse.rst b/docs/reverse.rst index 2fec15f7..baeb7ff0 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -162,81 +162,127 @@ This will stop redirects (301 and 302) sent by the Webmail, nginx front and admi Traefik as reverse proxy ------------------------ -`Traefik`_ is a popular reverse-proxy aimed at containerized systems. -As such, many may wish to integrate Mailu into a system which already uses Traefik as its sole ingress/reverse-proxy. +.. code-block:: yaml + reverse-proxy: + # The official v2 Traefik docker image + image: traefik:v2.10 + # Enables the web UI and tells Traefik to listen to docker + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.allowEmptyServices=true" + - "--entrypoints.web.address=:http" + - "--entrypoints.websecure.address=:https" + - "--entrypoints.smtp.address=:smtp" + - "--entrypoints.submission.address=:submission" + - "--entrypoints.submissions.address=:submissions" + - "--entrypoints.imap.address=:imap" + - "--entrypoints.imaps.address=:imaps" + - "--entrypoints.pop3.address=:pop3" + - "--entrypoints.pop3s.address=:pop3s" + - "--entrypoints.sieve.address=:sieve" + # - "--api.insecure=true" + - "--certificatesresolvers.myresolver.acme.tlschallenge=true" + - "--certificatesresolvers.myresolver.acme.email=test@example.com" + - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" + - "--log.level=DEBUG" + ports: + # The HTTP port + - "25:25" + - "80:80" + - "443:443" + - "465:465" + - "587:587" + - "993:993" + - "995:995" + - "110:110" + - "143:143" + - "4190:4190" + # The Web UI (enabled by --api.insecure=true) + #- "8080:8080" + volumes: + # So that Traefik can listen to the Docker events + - /var/run/docker.sock:/var/run/docker.sock -As the ``mailu/front`` container uses Nginx not only for ``HTTP`` forwarding, but also for the mail-protocols like ``SMTP``, ``IMAP``, etc -, we need to keep this container around even when using another ``HTTP`` reverse-proxy. Furthermore, Traefik is neither able to -forward non-HTTP, nor can it easily forward HTTPS-to-HTTPS. - -This, however, means 3 things: - -- ``mailu/front`` needs to listen internally on ``HTTP`` rather than ``HTTPS`` -- ``mailu/front`` is not exposed to the outside world on ``HTTP`` -- ``mailu/front`` still needs ``SSL`` certificates (here, we assume ``letsencrypt``) for a well-behaved mail service - -This makes the setup with Traefik a bit harder: Traefik saves its certificates in a proprietary *JSON* file, which is not readable -by Nginx in the ``front``-container. To solve this, your ``acme.json`` needs to be exposed to the host or a ``docker-volume``. -It will then be read by a script in another container, which will dump the certificates as ``PEM`` files, readable for -Nginx. The ``front`` container will automatically reload Nginx whenever these certificates change. - -To set this up, first set ``TLS_FLAVOR=mail`` in your ``.env``. This tells ``mailu/front`` not to try to request certificates using ``letsencrypt``, -but to read provided certificates, and use them only for mail-protocols, not for ``HTTP``. -Next, in your ``docker-compose.yml``, comment out the ``port`` lines of the ``front`` section for port ``…:80`` and ``…:443``. -Add the respective Traefik labels for your domain/configuration, like +and then for front: .. code-block:: yaml - - labels: + labels: - "traefik.enable=true" - - "traefik.port=80" - - "traefik.frontend.rule=Host:$TRAEFIK_DOMAIN" -.. note:: Please don’t forget to add ``TRAEFIK_DOMAIN=[...]`` TO YOUR ``.env`` + # the second part is important to ensure Mailu can get certificates for the main FQDN + - "traefik.http.routers.web.rule=Host(`fqdn.example.com`) || Path(`/.well-known/acme-challenge/`)" + - "traefik.http.routers.web.entrypoints=web" + - "traefik.http.services.web.loadbalancer.server.port=80" -If your Traefik is configured to automatically request certificates from *letsencrypt*, then you’ll have a certificate -for ``mail.your.example.com`` now. However, ``mail.your.example.com`` might only be the location where you want the Mailu web-interfaces -to live — your mail should be sent/received from ``your.example.com``, and this is the ``DOMAIN`` in your ``.env``? -To support that use-case, Traefik can request ``SANs`` for your domain. The configuration for this will depend on your Traefik version. + # add other FQDNS here too + - "traefik.tcp.routers.websecure.rule=HostSNI(`fqdn.example.com`) || HostSNI(`autoconfig.example.com`) || HostSNI(`mta-sts.example.com`)" + - "traefik.tcp.routers.websecure.entrypoints=websecure" + - "traefik.tcp.routers.websecure.tls.passthrough=true" + - "traefik.tcp.routers.websecure.service=websecure" + - "traefik.tcp.services.websecure.loadbalancer.server.port=443" + - "traefik.tcp.services.websecure.loadbalancer.proxyProtocol.version=2" -Mailu must also be configured with the information what header is used by the reverse proxy for passing the remote -client IP. This is configured in mailu.env: + - "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.submission.rule=HostSNI(`*`)" + - "traefik.tcp.routers.submission.entrypoints=submission" + - "traefik.tcp.routers.submission.service=submission" + - "traefik.tcp.services.submission.loadbalancer.server.port=587" + - "traefik.tcp.services.submission.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.imap.rule=HostSNI(`*`)" + - "traefik.tcp.routers.imap.entrypoints=imap" + - "traefik.tcp.routers.imap.service=imap" + - "traefik.tcp.services.imap.loadbalancer.server.port=143" + - "traefik.tcp.services.imap.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" + + - "traefik.tcp.routers.pop3.rule=HostSNI(`*`)" + - "traefik.tcp.routers.pop3.entrypoints=pop3" + - "traefik.tcp.routers.pop3.service=pop3" + - "traefik.tcp.services.pop3.loadbalancer.server.port=110" + - "traefik.tcp.services.pop3.loadbalancer.proxyProtocol.version=2" + + - "traefik.tcp.routers.pop3s.rule=HostSNI(`*`)" + - "traefik.tcp.routers.pop3s.entrypoints=pop3s" + - "traefik.tcp.routers.pop3s.service=pop3s" + - "traefik.tcp.services.pop3s.loadbalancer.server.port=995" + - "traefik.tcp.services.pop3s.loadbalancer.proxyProtocol.version=2" + + - "traefik.tcp.routers.sieve.rule=HostSNI(`*`)" + - "traefik.tcp.routers.sieve.entrypoints=sieve" + - "traefik.tcp.routers.sieve.service=sieve" + - "traefik.tcp.services.sieve.loadbalancer.server.port=4190" + - "traefik.tcp.services.sieve.loadbalancer.proxyProtocol.version=2" + healthcheck: + test: ['NONE'] + +in mailu.env: .. code-block:: docker #mailu.env file - REAL_IP_HEADER=X-Real-Ip - REAL_IP_FROM=x.x.x.x,y.y.y.y.y - #x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu. - -For more information see the :ref:`configuration reference ` for more information. - -Traefik 2.x using labels configuration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add the appropriate labels for your domain(s) to the ``front`` container in ``docker-compose.yml``. - -.. code-block:: yaml - - services: - front: - labels: - # Enable TLS - - "traefik.http.routers.mailu-secure.tls" - # Your main domain - - "traefik.http.routers.mailu-secure.tls.domains[0].main=your.example.com" - # Optional SANs for your main domain - - "traefik.http.routers.mailu-secure.tls.domains[0].sans=mail.your.example.com,webmail.your.example.com,smtp.your.example.com" - # Optionally add other domains - - "traefik.http.routers.mailu-secure.tls.domains[1].main=mail.other.example.com" - - "traefik.http.routers.mailu-secure.tls.domains[1].sans=mail2.other.example.com,mail3.other.example.com" - # Your ACME certificate resolver - - "traefik.http.routers.mailu-secure.tls.certResolver=foo" - -Of course, be sure to define the Certificate Resolver ``foo`` in the static configuration as well. - -Alternatively, you can define SANs in the Traefik static configuration using routers, or in the static configuration using entrypoints. -Refer to the Traefik documentation for more details. + REAL_IP_FROM=192.168.203.0/24 + PROXY_PROTOCOL=all-but-http + TRAEFIK_VERSION=v2 + TLS_FLAVOR=mail-letsencrypt + WEBROOT_REDIRECT=/sso/login .. _`Traefik`: https://traefik.io/ From 8b890a84e9ef51156b5c5c36555a338b8ff3ea50 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 9 Aug 2023 18:24:37 +0200 Subject: [PATCH 04/10] clarify --- docs/reverse.rst | 186 ++--------------------------------------------- 1 file changed, 8 insertions(+), 178 deletions(-) diff --git a/docs/reverse.rst b/docs/reverse.rst index baeb7ff0..66bc9f48 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -2,166 +2,18 @@ Using an external reverse proxy =============================== One of Mailu's use cases is as part of a larger services platform, where maybe -other Web services are available than just Mailu Webmail and Admin interfaces. +other Web services are available on other FQDNs served from the same IP address. In such a configuration, one would usually run a frontend reverse proxy to serve all -Web contents based on criteria like the requested hostname (virtual hosts) -and/or the requested path. - -The Mailu Admin Web frontend is disabled in the default setup for security reasons, -it is however expected that most users will enable it at some point. Also, due -to the Docker Compose configuration structure, it is impossible for us to facilitate -disabling the Web frontend with a configuration variable. This guide was written to -help users setup such an architecture. - -There are basically three options, from the most to the least recommended one: - -- `have Mailu Web frontend listen locally and use your own Web frontend on top of it`_ -- `use Traefik in another container as central system-reverse-proxy`_ -- `override Mailu Web frontend configuration`_ - -All options will require that you modify the ``docker-compose.yml`` and ``mailu.env`` file. - -Mailu must also be configured with the information what header is used by the reverse proxy for passing the remote client IP. -This is configured in the mailu.env file. See the :ref:`configuration reference ` for more information. - -Have Mailu Web frontend listen locally --------------------------------------- - -The simplest and safest option is to modify the port forwards for Mailu Web frontend and have your own frontend point there. -For instance, in the ``front`` section of Mailu ``docker-compose.yml``, use local ports 8080 and 8443 respectively for HTTP and HTTPS: - -.. code-block:: yaml - - front: - # build: nginx - image: mailu/nginx:$VERSION - restart: always - env_file: .env - ports: - - "127.0.0.1:8080:80" - - "127.0.0.1:8443:443" - ... - volumes: - - "$ROOT/certs:/certs" - -Then on your own frontend, point to these local ports. In practice, you only need to point to the HTTPS port -(as the HTTP port simply redirects there). Here is an example Nginx configuration: - -.. code-block:: nginx - - server { - listen 443; - server_name mymailhost.tld; - - # [...] here goes your standard configuration - - location / { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass https://localhost:8443; - } - } - -.. code-block:: docker - - #mailu.env file - REAL_IP_HEADER=X-Real-IP - REAL_IP_FROM=x.x.x.x,y.y.y.y.y - #x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu. - -Because the admin interface is served as ``/admin``, the RESTful API as ``/api``, the Webmail as ``/webmail``, the single sign on page as ``/sso``, webdav as ``/webdav``, the client-autoconfiguration and the static files endpoint as ``/static``, you may also want to use a single virtual host and serve other applications (still Nginx): - -.. code-block:: nginx - - server { - # [...] here goes your standard configuration - - location ~* ^/(admin|api|sso|static|webdav|webmail|(apple\.)?mobileconfig|(\.well\-known/autoconfig/)?mail/|Autodiscover/Autodiscover) { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass https://localhost:8443; - } - - location /main_app { - proxy_pass https://some-host; - } - - location /other_app { - proxy_pass https://some-other-host; - } - - location /local_app { - root /path/to/your/files; - } - - location / { - return 301 $scheme://$host/main_app; - } - } - -.. note:: Please don’t add a ``/`` at the end of the location pattern or all your redirects will fail with 404 because the ``/`` would be missing, and you would have to add it manually to move on - -.. code-block:: docker - - #mailu.env file - REAL_IP_HEADER=X-Real-IP - REAL_IP_FROM=x.x.x.x,y.y.y.y.y - #x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu. - -Finally, you might want to serve the admin interface on a separate virtual host but not expose the admin container -directly (have your own HTTPS virtual hosts on top of Mailu, one public for the Webmail and one internal for administration for instance). - -Here is an example configuration : - -.. code-block:: nginx - - server { - listen :443; - server_name external.example.com; - # [...] here goes your standard configuration - - location /webmail { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass https://localhost:8443/webmail; - } - } - - server { - listen :443; - server_name internal.example.com; - # [...] here goes your standard configuration - - location /admin { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass https://localhost:8443/admin; - proxy_set_header Host $http_host; - } - - } - -.. code-block:: docker - - #mailu.env file - REAL_IP_HEADER=X-Real-IP - REAL_IP_FROM=x.x.x.x,y.y.y.y.y - #x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu. - -Depending on how you access the front server, you might want to add a ``proxy_redirect`` directive to your ``location`` blocks: - -.. code-block:: nginx - - proxy_redirect https://localhost https://example.com; - -This will stop redirects (301 and 302) sent by the Webmail, nginx front and admin interface from sending you to ``localhost``. +Web contents based on criteria like the requested hostname (virtual hosts). .. _traefik_proxy: Traefik as reverse proxy ------------------------ +In your docker-compose.yml, add a section like follows: + .. code-block:: yaml reverse-proxy: # The official v2 Traefik docker image @@ -182,9 +34,6 @@ Traefik as reverse proxy - "--entrypoints.pop3s.address=:pop3s" - "--entrypoints.sieve.address=:sieve" # - "--api.insecure=true" - - "--certificatesresolvers.myresolver.acme.tlschallenge=true" - - "--certificatesresolvers.myresolver.acme.email=test@example.com" - - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" - "--log.level=DEBUG" ports: # The HTTP port @@ -204,18 +53,18 @@ Traefik as reverse proxy # So that Traefik can listen to the Docker events - /var/run/docker.sock:/var/run/docker.sock -and then for front: +and then add the following to the front section: .. code-block:: yaml labels: - "traefik.enable=true" - # the second part is important to ensure Mailu can get certificates for the main FQDN + # the second part is important to ensure Mailu can get certificates from letsencrypt - "traefik.http.routers.web.rule=Host(`fqdn.example.com`) || Path(`/.well-known/acme-challenge/`)" - "traefik.http.routers.web.entrypoints=web" - "traefik.http.services.web.loadbalancer.server.port=80" - # add other FQDNS here too + #other FQDNS can be added here: - "traefik.tcp.routers.websecure.rule=HostSNI(`fqdn.example.com`) || HostSNI(`autoconfig.example.com`) || HostSNI(`mta-sts.example.com`)" - "traefik.tcp.routers.websecure.entrypoints=websecure" - "traefik.tcp.routers.websecure.tls.passthrough=true" @@ -277,29 +126,10 @@ in mailu.env: .. code-block:: docker - #mailu.env file REAL_IP_FROM=192.168.203.0/24 PROXY_PROTOCOL=all-but-http TRAEFIK_VERSION=v2 TLS_FLAVOR=mail-letsencrypt WEBROOT_REDIRECT=/sso/login -.. _`Traefik`: https://traefik.io/ - -Override Mailu configuration ----------------------------- - -If you do not have the resources for running a separate reverse proxy, you could override Mailu reverse proxy configuration by using :ref:`an override`. -Simply store your configuration file (Nginx format), in ``/mailu/overrides/nginx.conf``. -All ``*.conf`` files will be included in the main server block of Mailu in nginx which listens on port 80/443. -Add location blocks for any services that must be proxied. - -You can also download the example configuration files: - -- :download:`compose/traefik/docker-compose.yml` -- :download:`compose/traefik/traefik.toml` - -.. _have Mailu Web frontend listen locally and use your own Web frontend on top of it: #have-mailu-web-frontend-listen-locally -.. _use Traefik in another container as central system-reverse-proxy: #traefik-as-reverse-proxy -.. _override Mailu Web frontend configuration: #override-mailu-configuration - +Using the above configuration, Traefik will proxy all the traffic related to Mailu's FQDNs without requiring dupplicate certificates. From e7e169f1c1d5ec18e6bc2f07f7aad3a2b6695463 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 9 Aug 2023 19:10:07 +0200 Subject: [PATCH 05/10] Fix the obvious issue --- core/nginx/letsencrypt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/nginx/letsencrypt.py b/core/nginx/letsencrypt.py index ddac05b1..3ca2446a 100755 --- a/core/nginx/letsencrypt.py +++ b/core/nginx/letsencrypt.py @@ -45,8 +45,13 @@ command2 = [ # Wait for nginx to start time.sleep(5) +class MyRequestHandler(SimpleHTTPRequestHandler): + def do_GET(self): + self.path = '/var/empty/' + return SimpleHTTPRequestHandler.do_GET(self) + def serve_one_request(): - with HTTPServer(("0.0.0.0", 8008), SimpleHTTPRequestHandler) as server: + with HTTPServer(("0.0.0.0", 8008), MyRequestHandler) as server: server.handle_request() # Run certbot every day From 0cd2cbfa72b6e701990a36e5a50be387ae642adf Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 9 Aug 2023 19:10:34 +0200 Subject: [PATCH 06/10] Make it more explicit --- docs/reverse.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reverse.rst b/docs/reverse.rst index 66bc9f48..9e70d40c 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -60,12 +60,12 @@ and then add the following to the front section: - "traefik.enable=true" # the second part is important to ensure Mailu can get certificates from letsencrypt - - "traefik.http.routers.web.rule=Host(`fqdn.example.com`) || Path(`/.well-known/acme-challenge/`)" + - "traefik.http.routers.web.rule=Host(`mail.example.com`) || Path(`/.well-known/acme-challenge/`)" - "traefik.http.routers.web.entrypoints=web" - "traefik.http.services.web.loadbalancer.server.port=80" #other FQDNS can be added here: - - "traefik.tcp.routers.websecure.rule=HostSNI(`fqdn.example.com`) || HostSNI(`autoconfig.example.com`) || HostSNI(`mta-sts.example.com`)" + - "traefik.tcp.routers.websecure.rule=HostSNI(`mail.example.com`) || HostSNI(`autoconfig.example.com`) || HostSNI(`mta-sts.example.com`)" - "traefik.tcp.routers.websecure.entrypoints=websecure" - "traefik.tcp.routers.websecure.tls.passthrough=true" - "traefik.tcp.routers.websecure.service=websecure" From 497eb867c68f0dcd47a7b1130e2a680425d45d75 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 13 Aug 2023 08:03:31 +0200 Subject: [PATCH 07/10] Update reverse.rst --- docs/reverse.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reverse.rst b/docs/reverse.rst index 9e70d40c..7b89c09e 100644 --- a/docs/reverse.rst +++ b/docs/reverse.rst @@ -15,6 +15,7 @@ Traefik as reverse proxy In your docker-compose.yml, add a section like follows: .. code-block:: yaml + reverse-proxy: # The official v2 Traefik docker image image: traefik:v2.10 @@ -56,6 +57,7 @@ In your docker-compose.yml, add a section like follows: and then add the following to the front section: .. code-block:: yaml + labels: - "traefik.enable=true" From c9eae7bfbf1446713deeba5895ed363a67a0b696 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 18 Aug 2023 17:01:15 +0200 Subject: [PATCH 08/10] Fix exception reported by diginzm --- core/admin/mailu/internal/views/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index cea90e6d..6d37d923 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -28,7 +28,7 @@ def nginx_authentication(): response = flask.Response() response.headers['Auth-Status'] = status response.headers['Auth-Error-Code'] = code - if int(flask.request.headers['Auth-Login-Attempt']) < 10: + if int(flask.request.headers('Auth-Login-Attempt',0)) < 10: response.headers['Auth-Wait'] = '3' return response raw_password = urllib.parse.unquote(headers['Auth-Pass']) if 'Auth-Pass' in headers else '' From c66934e6bfeaa5e87fb8d13c48ee6ea7447b9f86 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 18 Aug 2023 18:47:49 +0200 Subject: [PATCH 09/10] Auth-Login-Attempt is no more since we don't use nginx --- core/admin/mailu/internal/views/auth.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 6d37d923..7de885b8 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -28,8 +28,6 @@ def nginx_authentication(): response = flask.Response() response.headers['Auth-Status'] = status response.headers['Auth-Error-Code'] = code - if int(flask.request.headers('Auth-Login-Attempt',0)) < 10: - response.headers['Auth-Wait'] = '3' return response raw_password = urllib.parse.unquote(headers['Auth-Pass']) if 'Auth-Pass' in headers else '' headers = nginx.handle_authentication(flask.request.headers) @@ -45,8 +43,6 @@ def nginx_authentication(): response = flask.Response() response.headers['Auth-Status'] = status response.headers['Auth-Error-Code'] = code - if int(flask.request.headers['Auth-Login-Attempt']) < 10: - response.headers['Auth-Wait'] = '3' return response is_valid_user = True if headers.get("Auth-Status") == "OK": From 45ef205887ec3866c125130d248832aaee5e79bc Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 28 Aug 2023 17:42:19 +0200 Subject: [PATCH 10/10] Serve actual content as requested in review --- core/nginx/letsencrypt.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/nginx/letsencrypt.py b/core/nginx/letsencrypt.py index 3ca2446a..a14dfa15 100755 --- a/core/nginx/letsencrypt.py +++ b/core/nginx/letsencrypt.py @@ -47,8 +47,12 @@ time.sleep(5) class MyRequestHandler(SimpleHTTPRequestHandler): def do_GET(self): - self.path = '/var/empty/' - return SimpleHTTPRequestHandler.do_GET(self) + if self.path == '/testing': + self.send_response(204) + else: + self.send_response(404) + self.send_header('Content-Type', 'text/plain') + self.end_headers() def serve_one_request(): with HTTPServer(("0.0.0.0", 8008), MyRequestHandler) as server: @@ -62,7 +66,7 @@ while True: thread = Thread(target=serve_one_request) thread.start() r = requests.get(target) - if r.status_code != 404: + if r.status_code != 204: log.error(f"Can't reach {target}!, please ensure it's fixed or change the TLS_FLAVOR.") time.sleep(5) else: