Merge branch 'refactor-nginx'
This commit is contained in:
@@ -18,7 +18,8 @@ VERSION=stable
|
|||||||
SECRET_KEY=ChangeMeChangeMe
|
SECRET_KEY=ChangeMeChangeMe
|
||||||
|
|
||||||
# Address where listening ports should bind
|
# Address where listening ports should bind
|
||||||
BIND_ADDRESS=127.0.0.1
|
BIND_ADDRESS4=127.0.0.1
|
||||||
|
BIND_ADDRESS6=::1
|
||||||
|
|
||||||
# Main mail domain
|
# Main mail domain
|
||||||
DOMAIN=mailu.io
|
DOMAIN=mailu.io
|
||||||
@@ -94,4 +95,3 @@ COMPOSE_PROJECT_NAME=mailu
|
|||||||
# Default password scheme used for newly created accounts and changed passwords
|
# Default password scheme used for newly created accounts and changed passwords
|
||||||
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
|
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
|
||||||
PASSWORD_SCHEME=SHA512-CRYPT
|
PASSWORD_SCHEME=SHA512-CRYPT
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
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.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[Join us and chat about the project.](https://riot.im/app/#/room/#mailu:tedomum.net)
|
[Join us and chat about the project.](https://riot.im/app/#/room/#mailu:tedomum.net)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from mailu import db, models
|
from mailu import db, models
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
|
||||||
SUPPORTED_AUTH_METHODS = ["none", "plain"]
|
SUPPORTED_AUTH_METHODS = ["none", "plain"]
|
||||||
|
|
||||||
|
|
||||||
STATUSES = {
|
STATUSES = {
|
||||||
"authentication": ("Authentication credentials invalid", {
|
"authentication": ("Authentication credentials invalid", {
|
||||||
"imap": "AUTHENTICATIONFAILED",
|
"imap": "AUTHENTICATIONFAILED",
|
||||||
@@ -14,21 +16,15 @@ STATUSES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
SERVER_MAP = {
|
|
||||||
"imap": ("imap", 143),
|
|
||||||
"smtp": ("smtp", 25)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def handle_authentication(headers):
|
def handle_authentication(headers):
|
||||||
""" Handle an HTTP nginx authentication request
|
""" Handle an HTTP nginx authentication request
|
||||||
See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol
|
See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol
|
||||||
"""
|
"""
|
||||||
method = headers["Auth-Method"]
|
method = headers["Auth-Method"]
|
||||||
protocol = headers["Auth-Protocol"]
|
protocol = headers["Auth-Protocol"]
|
||||||
server, port = get_server(headers["Auth-Protocol"])
|
|
||||||
# Incoming mail, no authentication
|
# Incoming mail, no authentication
|
||||||
if method == "none" and protocol == "smtp":
|
if method == "none" and protocol == "smtp":
|
||||||
|
server, port = get_server(headers["Auth-Protocol"], False)
|
||||||
return {
|
return {
|
||||||
"Auth-Status": "OK",
|
"Auth-Status": "OK",
|
||||||
"Auth-Server": server,
|
"Auth-Server": server,
|
||||||
@@ -36,8 +32,9 @@ def handle_authentication(headers):
|
|||||||
}
|
}
|
||||||
# Authenticated user
|
# Authenticated user
|
||||||
elif method == "plain":
|
elif method == "plain":
|
||||||
user_email = headers["Auth-User"]
|
server, port = get_server(headers["Auth-Protocol"], True)
|
||||||
password = headers["Auth-Pass"]
|
user_email = urllib.parse.unquote(headers["Auth-User"])
|
||||||
|
password = urllib.parse.unquote(headers["Auth-Pass"])
|
||||||
user = models.User.query.get(user_email)
|
user = models.User.query.get(user_email)
|
||||||
if user and user.check_password(password):
|
if user and user.check_password(password):
|
||||||
return {
|
return {
|
||||||
@@ -64,7 +61,13 @@ def get_status(protocol, status):
|
|||||||
return status, codes[protocol]
|
return status, codes[protocol]
|
||||||
|
|
||||||
|
|
||||||
def get_server(protocol):
|
def get_server(protocol, authenticated=False):
|
||||||
hostname, port = SERVER_MAP[protocol]
|
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)
|
address = socket.gethostbyname(hostname)
|
||||||
return address, port
|
return address, port
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ from mailu.internal import internal, nginx
|
|||||||
import flask
|
import flask
|
||||||
|
|
||||||
|
|
||||||
@internal.route("/nginx")
|
@internal.route("/auth/email")
|
||||||
def nginx_authentication():
|
def nginx_authentication():
|
||||||
|
""" Main authentication endpoint for Nginx email server
|
||||||
|
"""
|
||||||
headers = nginx.handle_authentication(flask.request.headers)
|
headers = nginx.handle_authentication(flask.request.headers)
|
||||||
response = flask.Response()
|
response = flask.Response()
|
||||||
for key, value in headers.items():
|
for key, value in headers.items():
|
||||||
|
|||||||
@@ -8,15 +8,24 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
ports:
|
ports:
|
||||||
- "$BIND_ADDRESS:80:80"
|
- "$BIND_ADDRESS4:80:80"
|
||||||
- "$BIND_ADDRESS:443:443"
|
- "$BIND_ADDRESS4:443:443"
|
||||||
- "$BIND_ADDRESS:110:110"
|
- "$BIND_ADDRESS4:110:110"
|
||||||
- "$BIND_ADDRESS:143:143"
|
- "$BIND_ADDRESS4:143:143"
|
||||||
- "$BIND_ADDRESS:993:993"
|
- "$BIND_ADDRESS4:993:993"
|
||||||
- "$BIND_ADDRESS:995:995"
|
- "$BIND_ADDRESS4:995:995"
|
||||||
- "$BIND_ADDRESS:25:25"
|
- "$BIND_ADDRESS4:25:25"
|
||||||
- "$BIND_ADDRESS:465:465"
|
- "$BIND_ADDRESS4:465:465"
|
||||||
- "$BIND_ADDRESS:587:587"
|
- "$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:
|
volumes:
|
||||||
- "$ROOT/certs:/certs"
|
- "$ROOT/certs:/certs"
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,17 @@ http {
|
|||||||
server_tokens off;
|
server_tokens off;
|
||||||
absolute_redirect off;
|
absolute_redirect off;
|
||||||
|
|
||||||
|
# Main HTTP server
|
||||||
server {
|
server {
|
||||||
|
# Always listen over HTTP
|
||||||
listen 80;
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
# TLS configuration
|
# Only enable HTTPS if TLS is enabled with no error
|
||||||
{% if TLS and not TLS_ERROR %}
|
{% if TLS and not TLS_ERROR %}
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
include /etc/nginx/tls.conf;
|
include /etc/nginx/tls.conf;
|
||||||
ssl_session_cache shared:SSLHTTP:50m;
|
ssl_session_cache shared:SSLHTTP:50m;
|
||||||
add_header Strict-Transport-Security max-age=15768000;
|
add_header Strict-Transport-Security max-age=15768000;
|
||||||
@@ -34,18 +39,21 @@ http {
|
|||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
# In any case, enable the proxy for certbot if the flavor is letsencrypt
|
||||||
{% if TLS_FLAVOR == 'letsencrypt' %}
|
{% if TLS_FLAVOR == 'letsencrypt' %}
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
proxy_pass http://localhost:8000;
|
proxy_pass http://localhost:8000;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Actual logic
|
# If TLS is failing, prevent access to anything except certbot
|
||||||
{% if TLS_ERROR %}
|
{% if TLS_ERROR %}
|
||||||
location / {
|
location / {
|
||||||
return 403
|
return 403;
|
||||||
}
|
}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
# Actual logic
|
||||||
{% if WEBMAIL != 'none' %}
|
{% if WEBMAIL != 'none' %}
|
||||||
location / {
|
location / {
|
||||||
return 301 $scheme://$host/webmail/;
|
return 301 $scheme://$host/webmail/;
|
||||||
@@ -76,11 +84,20 @@ http {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Forwarding authentication server
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:8000;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://admin/internal/;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mail {
|
mail {
|
||||||
server_name {{ HOSTNAMES.split(",")[0] }};
|
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;
|
proxy_pass_error_message on;
|
||||||
|
|
||||||
{% if TLS and not TLS_ERROR %}
|
{% if TLS and not TLS_ERROR %}
|
||||||
@@ -88,18 +105,36 @@ mail {
|
|||||||
ssl_session_cache shared:SSLMAIL:50m;
|
ssl_session_cache shared:SSLMAIL:50m;
|
||||||
{% endif %}
|
{% 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 {
|
server {
|
||||||
listen 25;
|
listen 25;
|
||||||
{% if TLS_FLAVOR != 'notls' %}
|
listen [::]:25;
|
||||||
|
{% if TLS and not TLS_ERROR %}
|
||||||
starttls on;
|
starttls on;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
protocol smtp;
|
protocol smtp;
|
||||||
smtp_auth none;
|
smtp_auth none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# All other protocols are disabled if TLS is failing
|
||||||
{% if not TLS_ERROR %}
|
{% if not TLS_ERROR %}
|
||||||
server {
|
server {
|
||||||
listen 143;
|
listen 143;
|
||||||
|
listen [::]:143;
|
||||||
{% if TLS %}
|
{% if TLS %}
|
||||||
starttls only;
|
starttls only;
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -107,22 +142,27 @@ mail {
|
|||||||
imap_auth plain;
|
imap_auth plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if TLS %}
|
|
||||||
server {
|
server {
|
||||||
listen 465 ssl;
|
listen 587;
|
||||||
|
listen [::]:587;
|
||||||
|
{% if TLS %}
|
||||||
|
starttls only;
|
||||||
|
{% endif %}
|
||||||
protocol smtp;
|
protocol smtp;
|
||||||
smtp_auth plain;
|
smtp_auth plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{% if TLS %}
|
||||||
server {
|
server {
|
||||||
listen 597;
|
listen 465 ssl;
|
||||||
starttls only;
|
listen [::]:465 ssl;
|
||||||
protocol smtp;
|
protocol smtp;
|
||||||
smtp_auth plain;
|
smtp_auth plain;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 993 ssl;
|
listen 993 ssl;
|
||||||
|
listen [::]:993 ssl;
|
||||||
protocol imap;
|
protocol imap;
|
||||||
imap_auth plain;
|
imap_auth plain;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,11 @@
|
|||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
import os
|
import os
|
||||||
import socket
|
|
||||||
|
|
||||||
convert = lambda src, dst, args: open(dst, "w").write(jinja2.Template(open(src).read()).render(**args))
|
convert = lambda src, dst, args: open(dst, "w").write(jinja2.Template(open(src).read()).render(**args))
|
||||||
|
|
||||||
args = os.environ.copy()
|
args = os.environ.copy()
|
||||||
|
|
||||||
if "ADMIN_ADDRESS" not in os.environ:
|
|
||||||
args["ADMIN_ADDRESS"] = socket.gethostbyname("admin")
|
|
||||||
|
|
||||||
args["TLS"] = {
|
args["TLS"] = {
|
||||||
"cert": ("/certs/cert.pem", "/certs/key.pem"),
|
"cert": ("/certs/cert.pem", "/certs/key.pem"),
|
||||||
"letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem",
|
"letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem",
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ relayhost = {{ RELAYHOST }}
|
|||||||
# Recipient delimiter for extended addresses
|
# Recipient delimiter for extended addresses
|
||||||
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
|
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
|
||||||
|
|
||||||
# XClient for connection from the frontend
|
# Only the front server is allowed to perform xclient
|
||||||
smtpd_authorized_xclient_hosts = {{ FRONT_ADDRESS }}
|
smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }}
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# TLS
|
# TLS
|
||||||
@@ -78,25 +78,16 @@ smtpd_delay_reject = yes
|
|||||||
# Allowed senders are: the user or one of the alias destinations
|
# Allowed senders are: the user or one of the alias destinations
|
||||||
smtpd_sender_login_maps = $virtual_alias_maps
|
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
|
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 =
|
smtpd_recipient_restrictions =
|
||||||
permit_mynetworks,
|
permit_mynetworks,
|
||||||
reject_unauth_pipelining,
|
check_sender_access ${sql}sqlite-reject-spoofed.cf,
|
||||||
reject_non_fqdn_recipient,
|
reject_non_fqdn_sender,
|
||||||
reject_unknown_recipient_domain,
|
reject_unknown_sender_domain,
|
||||||
permit
|
reject_unknown_recipient_domain,
|
||||||
|
permit
|
||||||
|
|
||||||
###############
|
###############
|
||||||
# Milter
|
# Milter
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
# service type private unpriv chroot wakeup maxproc command + args
|
# service type private unpriv chroot wakeup maxproc command + args
|
||||||
# (yes) (yes) (yes) (never) (100)
|
# (yes) (yes) (yes) (never) (100)
|
||||||
|
|
||||||
# Exposed SMTP services
|
# Exposed SMTP service
|
||||||
smtp inet n - n - - smtpd
|
smtp inet n - n - - smtpd
|
||||||
-o cleanup_service_name=outclean
|
|
||||||
|
|
||||||
# Additional services
|
# Internal SMTP service
|
||||||
outclean unix n - n - 0 cleanup
|
10025 inet n - n - - smtpd
|
||||||
-o header_checks=pcre:/etc/postfix/outclean_header_filter
|
-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
|
# Internal postfix services
|
||||||
pickup unix n - n 60 1 pickup
|
pickup unix n - n 60 1 pickup
|
||||||
|
|||||||
5
postfix/conf/sqlite-reject-spoofed.cf
Normal file
5
postfix/conf/sqlite-reject-spoofed.cf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dbpath = /data/main.db
|
||||||
|
query =
|
||||||
|
SELECT 'REJECT' FROM domain WHERE name='%s'
|
||||||
|
UNION
|
||||||
|
SELECT 'REJECT' FROM alternative WHERE name='%s'
|
||||||
@@ -14,9 +14,9 @@ stock = utf-8
|
|||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
type = IMAP
|
type = IMAP
|
||||||
imap_hostname = imap
|
imap_hostname = front
|
||||||
imap_port = 993
|
imap_port = 10143
|
||||||
imap_ssl = True
|
imap_ssl = False
|
||||||
|
|
||||||
[git]
|
[git]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
imap_host = "front"
|
imap_host = "front"
|
||||||
imap_port = 143
|
imap_port = 10143
|
||||||
imap_secure = "None"
|
imap_secure = "None"
|
||||||
imap_short_login = Off
|
imap_short_login = Off
|
||||||
sieve_use = On
|
sieve_use = On
|
||||||
@@ -8,7 +8,7 @@ sieve_host = "imap"
|
|||||||
sieve_port = 4190
|
sieve_port = 4190
|
||||||
sieve_secure = "TLS"
|
sieve_secure = "TLS"
|
||||||
smtp_host = "front"
|
smtp_host = "front"
|
||||||
smtp_port = 25
|
smtp_port = 10025
|
||||||
smtp_secure = "None"
|
smtp_secure = "None"
|
||||||
smtp_short_login = Off
|
smtp_short_login = Off
|
||||||
smtp_auth = On
|
smtp_auth = On
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ $config['plugins'] = array(
|
|||||||
|
|
||||||
// Mail servers
|
// Mail servers
|
||||||
$config['default_host'] = 'front';
|
$config['default_host'] = 'front';
|
||||||
$config['default_port'] = 143;
|
$config['default_port'] = 10143;
|
||||||
$config['smtp_server'] = 'front';
|
$config['smtp_server'] = 'front';
|
||||||
$config['smtp_port'] = 25;
|
$config['smtp_port'] = 10025;
|
||||||
$config['smtp_user'] = '%u';
|
$config['smtp_user'] = '%u';
|
||||||
$config['smtp_pass'] = '%p';
|
$config['smtp_pass'] = '%p';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user