diff --git a/.env.dist b/.env.dist index cb3a97d0..90b1ba0c 100644 --- a/.env.dist +++ b/.env.dist @@ -18,7 +18,8 @@ VERSION=stable SECRET_KEY=ChangeMeChangeMe # Address where listening ports should bind -BIND_ADDRESS=127.0.0.1 +BIND_ADDRESS4=127.0.0.1 +BIND_ADDRESS6=::1 # Main mail domain DOMAIN=mailu.io @@ -94,4 +95,3 @@ COMPOSE_PROJECT_NAME=mailu # Default password scheme used for newly created accounts and changed passwords # (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT) PASSWORD_SCHEME=SHA512-CRYPT - diff --git a/README.md b/README.md index 076083d5..2932d4fc 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,3 @@ - :warning: Warning -================== - -**Be very careful when using `master`**, especially if you are currently running -`1.4`, development of version `1.5` includes refactoring the frontend and -authentication mechanisms. At best your server will stop working, at worst you -could expose your data to malicious attackers! - -**Do not start using `traefik`** as a frontend server. Traefik was first tested -to replace nginx because certificate generation was a nightmare. As we are in the -process of completely rewriting the frontend and authentication interface, it will -probably be deprecated before `1.5` is out. - ![Logo](logo.png) [Join us and chat about the project.](https://riot.im/app/#/room/#mailu:tedomum.net) diff --git a/admin/mailu/internal/nginx.py b/admin/mailu/internal/nginx.py index a7a8c5b4..4c5cc334 100644 --- a/admin/mailu/internal/nginx.py +++ b/admin/mailu/internal/nginx.py @@ -1,10 +1,12 @@ from mailu import db, models import socket +import urllib SUPPORTED_AUTH_METHODS = ["none", "plain"] + STATUSES = { "authentication": ("Authentication credentials invalid", { "imap": "AUTHENTICATIONFAILED", @@ -14,21 +16,15 @@ STATUSES = { } -SERVER_MAP = { - "imap": ("imap", 143), - "smtp": ("smtp", 25) -} - - def handle_authentication(headers): """ Handle an HTTP nginx authentication request See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol """ method = headers["Auth-Method"] protocol = headers["Auth-Protocol"] - server, port = get_server(headers["Auth-Protocol"]) # Incoming mail, no authentication if method == "none" and protocol == "smtp": + server, port = get_server(headers["Auth-Protocol"], False) return { "Auth-Status": "OK", "Auth-Server": server, @@ -36,8 +32,9 @@ def handle_authentication(headers): } # Authenticated user elif method == "plain": - user_email = headers["Auth-User"] - password = headers["Auth-Pass"] + server, port = get_server(headers["Auth-Protocol"], True) + user_email = urllib.parse.unquote(headers["Auth-User"]) + password = urllib.parse.unquote(headers["Auth-Pass"]) user = models.User.query.get(user_email) if user and user.check_password(password): return { @@ -64,7 +61,13 @@ def get_status(protocol, status): return status, codes[protocol] -def get_server(protocol): - hostname, port = SERVER_MAP[protocol] +def get_server(protocol, authenticated=False): + if protocol == "imap": + hostname, port = "imap", 143 + elif protocol == "pop3": + hostname, port = "imap", 110 + elif protocol == "smtp": + hostname = "smtp" + port = 10025 if authenticated else 25 address = socket.gethostbyname(hostname) return address, port diff --git a/admin/mailu/internal/views.py b/admin/mailu/internal/views.py index 04d3268a..179b0cf5 100644 --- a/admin/mailu/internal/views.py +++ b/admin/mailu/internal/views.py @@ -4,8 +4,10 @@ from mailu.internal import internal, nginx import flask -@internal.route("/nginx") +@internal.route("/auth/email") def nginx_authentication(): + """ Main authentication endpoint for Nginx email server + """ headers = nginx.handle_authentication(flask.request.headers) response = flask.Response() for key, value in headers.items(): diff --git a/docker-compose.yml.dist b/docker-compose.yml.dist index 35f6ac5f..54b6439f 100644 --- a/docker-compose.yml.dist +++ b/docker-compose.yml.dist @@ -8,15 +8,24 @@ services: restart: always env_file: .env ports: - - "$BIND_ADDRESS:80:80" - - "$BIND_ADDRESS:443:443" - - "$BIND_ADDRESS:110:110" - - "$BIND_ADDRESS:143:143" - - "$BIND_ADDRESS:993:993" - - "$BIND_ADDRESS:995:995" - - "$BIND_ADDRESS:25:25" - - "$BIND_ADDRESS:465:465" - - "$BIND_ADDRESS:587:587" + - "$BIND_ADDRESS4:80:80" + - "$BIND_ADDRESS4:443:443" + - "$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:80:80" + - "$BIND_ADDRESS6:443:443" + - "$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/certs:/certs" diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf index 446c039f..b684bbec 100644 --- a/nginx/conf/nginx.conf +++ b/nginx/conf/nginx.conf @@ -19,12 +19,17 @@ http { server_tokens off; absolute_redirect off; + # Main HTTP server server { + # Always listen over HTTP listen 80; + listen [::]:80; - # TLS configuration + # Only enable HTTPS if TLS is enabled with no error {% if TLS and not TLS_ERROR %} listen 443 ssl; + listen [::]:443 ssl; + include /etc/nginx/tls.conf; ssl_session_cache shared:SSLHTTP:50m; add_header Strict-Transport-Security max-age=15768000; @@ -34,18 +39,21 @@ http { } {% endif %} + # In any case, enable the proxy for certbot if the flavor is letsencrypt {% if TLS_FLAVOR == 'letsencrypt' %} location ^~ /.well-known/acme-challenge/ { proxy_pass http://localhost:8000; } {% endif %} - # Actual logic + # If TLS is failing, prevent access to anything except certbot {% if TLS_ERROR %} location / { - return 403 + return 403; } {% else %} + + # Actual logic {% if WEBMAIL != 'none' %} location / { return 301 $scheme://$host/webmail/; @@ -76,11 +84,20 @@ http { {% endif %} {% endif %} } + + # Forwarding authentication server + server { + listen 127.0.0.1:8000; + + location / { + proxy_pass http://admin/internal/; + } + } } mail { server_name {{ HOSTNAMES.split(",")[0] }}; - auth_http http://{{ ADMIN_ADDRESS }}/internal/nginx; + auth_http http://127.0.0.1:8000/auth/email; proxy_pass_error_message on; {% if TLS and not TLS_ERROR %} @@ -88,18 +105,36 @@ mail { ssl_session_cache shared:SSLMAIL:50m; {% endif %} + # Default SMTP server for the webmail (no encryption, but authentication) + server { + listen 10025; + protocol smtp; + smtp_auth plain; + } + + # Default IMAP server for the webmail (no encryption, but authentication) + server { + listen 10143; + protocol imap; + smtp_auth plain; + } + + # SMTP is always enabled, to avoid losing emails when TLS is failing server { listen 25; - {% if TLS_FLAVOR != 'notls' %} + listen [::]:25; + {% if TLS and not TLS_ERROR %} starttls on; {% endif %} protocol smtp; smtp_auth none; } + # All other protocols are disabled if TLS is failing {% if not TLS_ERROR %} server { listen 143; + listen [::]:143; {% if TLS %} starttls only; {% endif %} @@ -107,22 +142,27 @@ mail { imap_auth plain; } - {% if TLS %} server { - listen 465 ssl; + listen 587; + listen [::]:587; + {% if TLS %} + starttls only; + {% endif %} protocol smtp; smtp_auth plain; } + {% if TLS %} server { - listen 597; - starttls only; + listen 465 ssl; + listen [::]:465 ssl; protocol smtp; smtp_auth plain; } server { listen 993 ssl; + listen [::]:993 ssl; protocol imap; imap_auth plain; } diff --git a/nginx/config.py b/nginx/config.py index 5f1e0355..714ad037 100755 --- a/nginx/config.py +++ b/nginx/config.py @@ -2,15 +2,11 @@ import jinja2 import os -import socket convert = lambda src, dst, args: open(dst, "w").write(jinja2.Template(open(src).read()).render(**args)) args = os.environ.copy() -if "ADMIN_ADDRESS" not in os.environ: - args["ADMIN_ADDRESS"] = socket.gethostbyname("admin") - args["TLS"] = { "cert": ("/certs/cert.pem", "/certs/key.pem"), "letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem", diff --git a/postfix/conf/main.cf b/postfix/conf/main.cf index e23dcba7..60887a47 100644 --- a/postfix/conf/main.cf +++ b/postfix/conf/main.cf @@ -31,8 +31,8 @@ relayhost = {{ RELAYHOST }} # Recipient delimiter for extended addresses recipient_delimiter = {{ RECIPIENT_DELIMITER }} -# XClient for connection from the frontend -smtpd_authorized_xclient_hosts = {{ FRONT_ADDRESS }} +# Only the front server is allowed to perform xclient +smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }} ############### # TLS @@ -78,25 +78,16 @@ smtpd_delay_reject = yes # Allowed senders are: the user or one of the alias destinations smtpd_sender_login_maps = $virtual_alias_maps -# Helo restrictions are specified for smtp only in master.cf +# Restrictions for incoming SMTP, other restrictions are applied in master.cf smtpd_helo_required = yes -# Sender restrictions -smtpd_sender_restrictions = - permit_mynetworks, - reject_non_fqdn_sender, - reject_unknown_sender_domain, - reject_unlisted_sender, - reject_sender_login_mismatch, - permit - -# Recipient restrictions: smtpd_recipient_restrictions = - permit_mynetworks, - reject_unauth_pipelining, - reject_non_fqdn_recipient, - reject_unknown_recipient_domain, - permit + permit_mynetworks, + check_sender_access ${sql}sqlite-reject-spoofed.cf, + reject_non_fqdn_sender, + reject_unknown_sender_domain, + reject_unknown_recipient_domain, + permit ############### # Milter diff --git a/postfix/conf/master.cf b/postfix/conf/master.cf index 47cb4af6..d64645a1 100644 --- a/postfix/conf/master.cf +++ b/postfix/conf/master.cf @@ -1,13 +1,16 @@ # service type private unpriv chroot wakeup maxproc command + args # (yes) (yes) (yes) (never) (100) -# Exposed SMTP services +# Exposed SMTP service smtp inet n - n - - smtpd - -o cleanup_service_name=outclean -# Additional services -outclean unix n - n - 0 cleanup - -o header_checks=pcre:/etc/postfix/outclean_header_filter +# Internal SMTP service +10025 inet n - n - - smtpd + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=reject_unlisted_sender,reject_sender_login_mismatch,permit + -o cleanup_service_name=outclean +outclean unix n - n - 0 cleanup + -o header_checks=pcre:/etc/postfix/outclean_header_filter.cf # Internal postfix services pickup unix n - n 60 1 pickup diff --git a/postfix/conf/outclean_header_filter b/postfix/conf/outclean_header_filter.cf similarity index 100% rename from postfix/conf/outclean_header_filter rename to postfix/conf/outclean_header_filter.cf diff --git a/postfix/conf/sqlite-reject-spoofed.cf b/postfix/conf/sqlite-reject-spoofed.cf new file mode 100644 index 00000000..9cdd6c45 --- /dev/null +++ b/postfix/conf/sqlite-reject-spoofed.cf @@ -0,0 +1,5 @@ +dbpath = /data/main.db +query = + SELECT 'REJECT' FROM domain WHERE name='%s' + UNION + SELECT 'REJECT' FROM alternative WHERE name='%s' diff --git a/radicale/radicale.conf b/radicale/radicale.conf index 0818c8f0..90979320 100644 --- a/radicale/radicale.conf +++ b/radicale/radicale.conf @@ -14,9 +14,9 @@ stock = utf-8 [auth] type = IMAP -imap_hostname = imap -imap_port = 993 -imap_ssl = True +imap_hostname = front +imap_port = 10143 +imap_ssl = False [git] diff --git a/rainloop/default.ini b/rainloop/default.ini index 53545bef..0cb96d69 100644 --- a/rainloop/default.ini +++ b/rainloop/default.ini @@ -1,5 +1,5 @@ imap_host = "front" -imap_port = 143 +imap_port = 10143 imap_secure = "None" imap_short_login = Off sieve_use = On @@ -8,7 +8,7 @@ sieve_host = "imap" sieve_port = 4190 sieve_secure = "TLS" smtp_host = "front" -smtp_port = 25 +smtp_port = 10025 smtp_secure = "None" smtp_short_login = Off smtp_auth = On diff --git a/roundcube/config.inc.php b/roundcube/config.inc.php index a867a169..60deb614 100644 --- a/roundcube/config.inc.php +++ b/roundcube/config.inc.php @@ -18,9 +18,9 @@ $config['plugins'] = array( // Mail servers $config['default_host'] = 'front'; -$config['default_port'] = 143; +$config['default_port'] = 10143; $config['smtp_server'] = 'front'; -$config['smtp_port'] = 25; +$config['smtp_port'] = 10025; $config['smtp_user'] = '%u'; $config['smtp_pass'] = '%p';