diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index 988959f..0570b7f 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -1,8 +1,10 @@ FROM alpine:3.13 -RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python3 py3-jinja2 +RUN apk add --no-cache bash postfix postfix-sqlite postfix-pcre rsyslog -COPY conf /conf -COPY start.py /start.py +COPY conf /etc/postfix +COPY rsyslog.conf /etc/rsyslog.conf -CMD /start.py +COPY start.sh /start.sh + +CMD ["/start.sh"] diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index c538247..4e2a00e 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -4,12 +4,9 @@ # Main domain and hostname mydomain = {{ DOMAIN }} -myhostname = {{ HOSTNAMES.split(",")[0] }} +myhostname = {{ HOSTNAME }} myorigin = $mydomain -# Queue location -queue_directory = /queue - # Message size limit message_size_limit = {{ MESSAGE_SIZE_LIMIT }} @@ -28,12 +25,6 @@ mydestination = # Relayhost if any is configured relayhost = {{ RELAYHOST }} -# Recipient delimiter for extended addresses -recipient_delimiter = {{ RECIPIENT_DELIMITER }} - -# Only the front server is allowed to perform xclient -smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }} - ############### # TLS ############### @@ -41,14 +32,47 @@ smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }} # General TLS configuration tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA tls_preempt_cipherlist = yes -tls_ssl_options = NO_COMPRESSION + +# Only one key/certificate pair is used, SNI not being supported by all +# services and not a strong requirement. Also, TLS is enforced for submission +# and smtps in master.cf. +smtpd_tls_security_level = may +smtpd_tls_cert_file=/certs/cert.pem +smtpd_tls_key_file=/certs/key.pem +smtpd_tls_session_cache_database = lmdb:${data_directory}/smtpd_scache + +# Server-side TLS is hardened, it should be up to the client to update his or +# her TLS stack in order to connect to the mail server. Hardening is based on +# https://bettercrypto.org/static/applied-crypto-hardening.pdf +smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3 +smtpd_tls_protocols = !SSLv2, !SSLv3 +smtpd_tls_ciphers = high +smtpd_tls_mandatory_ciphers = high + # Outgoing TLS is more flexible because 1. not all receiving servers will # support TLS, 2. not all will have and up-to-date TLS stack. smtp_tls_security_level = may smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 -smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache +smtp_tls_session_cache_database = lmdb:${data_directory}/smtp_scache + +# General TLS hardening +tls_ssl_options = NO_COMPRESSION +tls_preempt_cipherlist = yes + +############### +# SASL +############### + +smtpd_sasl_local_domain = $myhostname + +# Authentication is done against dovecot, which acts as the main authention +# source +smtpd_sasl_type = dovecot +smtpd_sasl_path = inet:imap:2102 +smtpd_sasl_auth_enable = yes +smtpd_sasl_security_options = noanonymous ############### # Virtual @@ -60,8 +84,7 @@ virtual_alias_maps = ${sql}sqlite-virtual_alias_maps.cf virtual_mailbox_domains = ${sql}sqlite-virtual_mailbox_domains.cf virtual_mailbox_maps = $virtual_alias_maps -# Mails are transported if required, then forwarded to Dovecot for delivery -transport_maps = ${sql}sqlite-transport.cf +# Mails are forwarded to Dovecot for delivery virtual_transport = lmtp:inet:imap:2525 # In order to prevent Postfix from running DNS query, enforce the use of the @@ -78,22 +101,31 @@ smtpd_delay_reject = yes # Allowed senders are: the user or one of the alias destinations smtpd_sender_login_maps = $virtual_alias_maps -# Restrictions for incoming SMTP, other restrictions are applied in master.cf +# Helo restrictions are specified for smtp only 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, - check_sender_access ${sql}sqlite-reject-spoofed.cf, - reject_non_fqdn_sender, - reject_unknown_sender_domain, - reject_unknown_recipient_domain, - permit + reject_unauth_pipelining, + reject_non_fqdn_recipient, + reject_unknown_recipient_domain, + permit_mynetworks, + permit ############### # Milter ############### -smtpd_milters = inet:antispam:11332 +smtpd_milters = inet:milter:9900 milter_protocol = 6 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} milter_default_action = tempfail diff --git a/core/postfix/conf/master.cf b/core/postfix/conf/master.cf index cbcc5e5..a196f49 100644 --- a/core/postfix/conf/master.cf +++ b/core/postfix/conf/master.cf @@ -1,16 +1,24 @@ # service type private unpriv chroot wakeup maxproc command + args # (yes) (yes) (yes) (never) (100) -# Exposed SMTP service +# Exposed SMTP services smtp inet n - n - - smtpd - -# Internal SMTP service -10025 inet n - n - - smtpd + -o smtpd_helo_restrictions=permit_mynetworks,permit +submission inet n - n - - smtpd + -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes - -o smtpd_recipient_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit + -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o cleanup_service_name=outclean -outclean unix n - n - 0 cleanup - -o header_checks=pcre:/etc/postfix/outclean_header_filter.cf +smtps inet n - n - - smtpd + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_tls_wrappermode=yes + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o cleanup_service_name=outclean + +# Additional services +outclean unix n - n - 0 cleanup + -o header_checks=pcre:/etc/postfix/outclean_header_filter # Internal postfix services pickup unix n - n 60 1 pickup diff --git a/core/postfix/conf/outclean_header_filter.cf b/core/postfix/conf/outclean_header_filter.cf deleted file mode 100644 index 03e33ee..0000000 --- a/core/postfix/conf/outclean_header_filter.cf +++ /dev/null @@ -1,17 +0,0 @@ -# This configuration was copied from Mailinabox. The original version is available at: -# https://raw.githubusercontent.com/mail-in-a-box/mailinabox/master/conf/postfix_outgoing_mail_header_filters - -# Remove the first line of the Received: header. Note that we cannot fully remove the Received: header -# because OpenDKIM requires that a header be present when signing outbound mail. The first line is -# where the user's home IP address would be. -/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user (PRIMARY_HOSTNAME [PUBLIC_IP])$1 - -# Remove other typically private information. -/^\s*User-Agent:/ IGNORE -/^\s*X-Enigmail:/ IGNORE -/^\s*X-Mailer:/ IGNORE -/^\s*X-Originating-IP:/ IGNORE -/^\s*X-Pgp-Agent:/ IGNORE - -# The Mime-Version header can leak the user agent too, e.g. in Mime-Version: 1.0 (Mac OS X Mail 8.1 \(2010.6\)). -/^\s*(Mime-Version:\s*[0-9\.]+)\s.+/ REPLACE $1 diff --git a/core/postfix/conf/rsyslog.conf b/core/postfix/conf/rsyslog.conf deleted file mode 100644 index 13353b8..0000000 --- a/core/postfix/conf/rsyslog.conf +++ /dev/null @@ -1,4 +0,0 @@ -$ModLoad imuxsock -$template noTimestampFormat,"%syslogtag%%msg%\n" -$ActionFileDefaultTemplate noTimestampFormat -*.*;auth,authpriv.none /dev/stdout diff --git a/core/postfix/conf/sqlite-reject-spoofed.cf b/core/postfix/conf/sqlite-reject-spoofed.cf deleted file mode 100644 index 9cdd6c4..0000000 --- a/core/postfix/conf/sqlite-reject-spoofed.cf +++ /dev/null @@ -1,5 +0,0 @@ -dbpath = /data/main.db -query = - SELECT 'REJECT' FROM domain WHERE name='%s' - UNION - SELECT 'REJECT' FROM alternative WHERE name='%s' diff --git a/core/postfix/conf/sqlite-transport.cf b/core/postfix/conf/sqlite-transport.cf deleted file mode 100644 index 6295523..0000000 --- a/core/postfix/conf/sqlite-transport.cf +++ /dev/null @@ -1,3 +0,0 @@ -dbpath = /data/main.db -query = - SELECT 'smtp:['||smtp||']' FROM relay WHERE name='%s' diff --git a/core/postfix/conf/sqlite-virtual_alias_maps.cf b/core/postfix/conf/sqlite-virtual_alias_maps.cf index f53b65a..0a4896b 100644 --- a/core/postfix/conf/sqlite-virtual_alias_maps.cf +++ b/core/postfix/conf/sqlite-virtual_alias_maps.cf @@ -4,9 +4,7 @@ query = FROM (SELECT destination, email, wildcard, localpart FROM alias UNION - SELECT (CASE WHEN forward_enabled=1 THEN (CASE WHEN forward_keep=1 THEN email||',' ELSE '' END)||forward_destination ELSE email END) AS destination, email, 0 as wildcard, localpart FROM user - UNION - SELECT '@'||domain_name as destination, '@'||name as email, 0 as wildcard, '' as localpart FROM alternative) + SELECT email||(CASE WHEN forward_enabled=1 THEN ','||forward_destination ELSE '' END) AS destination, email, 0 as wildcard, localpart FROM user) WHERE ( wildcard = 0 diff --git a/core/postfix/conf/sqlite-virtual_mailbox_domains.cf b/core/postfix/conf/sqlite-virtual_mailbox_domains.cf index af453bc..2095ef2 100644 --- a/core/postfix/conf/sqlite-virtual_mailbox_domains.cf +++ b/core/postfix/conf/sqlite-virtual_mailbox_domains.cf @@ -1,5 +1,2 @@ dbpath = /data/main.db -query = - SELECT name FROM domain WHERE name='%s' - UNION - SELECT name FROM alternative WHERE name='%s' +query = SELECT name FROM domain WHERE name='%s' diff --git a/core/postfix/start.py b/core/postfix/start.py deleted file mode 100755 index 2402362..0000000 --- a/core/postfix/start.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/python3 - -import jinja2 -import os -import socket -import glob -import shutil - -convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) - -# Actual startup script -os.environ["FRONT_ADDRESS"] = socket.gethostbyname("front") - -for postfix_file in glob.glob("/conf/*.cf"): - convert(postfix_file, os.path.join("/etc/postfix", os.path.basename(postfix_file))) - -if os.path.exists("/overrides/postfix.cf"): - for line in open("/overrides/postfix.cf").read().strip().split("\n"): - os.system('postconf -e "{}"'.format(line)) - -if os.path.exists("/overrides/postfix.master"): - for line in open("/overrides/postfix.master").read().strip().split("\n"): - os.system('postconf -Me "{}"'.format(line)) - -for map_file in glob.glob("/overrides/*.map"): - destination = os.path.join("/etc/postfix", os.path.basename(map_file)) - shutil.copyfile(map_file, destination) - os.system("postmap {}".format(destination)) - os.remove(destination) - -convert("/conf/rsyslog.conf", "/etc/rsyslog.conf") - -# Run postfix -if os.path.exists("/var/run/rsyslogd.pid"): - os.remove("/var/run/rsyslogd.pid") -os.system("/usr/libexec/postfix/post-install meta_directory=/etc/postfix create-missing") -os.system("/usr/sbin/postfix start") -os.execv("/usr/sbin/rsyslogd", ["rsyslogd", "-n"])