mailsystem: Add basic postfix configuration

This commit is contained in:
Thomas Preisner 2024-12-04 21:39:42 +01:00
parent bc01d4d2d0
commit b7fac23bd1
3 changed files with 268 additions and 1 deletions

194
mailsystem/postfix.nix Normal file
View file

@ -0,0 +1,194 @@
{
config,
lib,
pkgs,
...
}:
with (import ./common.nix {inherit config;}); let
cfg = config.mailsystem;
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
attrsToLookupTable = aliases: let
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
in
mergeLookupTables lookupTables;
lookupTableToString = attrs: let
valueToString = value: lib.concatStringsSep ", " value;
in
lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
account_virtual_aliases = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value: let
to = name;
in
map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
cfg.accounts));
extra_virtual_aliases = attrsToLookupTable cfg.extraVirtualAliases;
all_virtual_aliases = mergeLookupTables [account_virtual_aliases extra_virtual_aliases];
aliases_file = let
content = lookupTableToString all_virtual_aliases;
in
builtins.toFile "virtual_aliases" content;
# File containing all mappings of authenticated accounts and their sender mail addresses.
virtual_accounts_file = let
content = lookupTableToString all_virtual_aliases;
in
builtins.toFile "virtual_accounts" content;
virtual_domains_file = builtins.toFile "virtual_domains" (lib.concatStringsSep "\n" cfg.domains);
submission_header_cleanup_rules = pkgs.writeText "submission_header_cleanup_rules" ''
# Removes sensitive headers from mails handed in via the submission port.
# See https://thomas-leister.de/mailserver-debian-stretch/
# Uses "pcre" style regex.
/^Received:/ IGNORE
/^X-Originating-IP:/ IGNORE
/^X-Mailer:/ IGNORE
/^User-Agent:/ IGNORE
/^X-Enigmail:/ IGNORE
# Replace the user's submitted hostname with the server's FQDN to hide the
# user's host/network.
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
'';
tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
in {
config = lib.mkIf cfg.enable {
services.postfix = {
enable = true;
hostname = "${cfg.reverseFqdn}";
networksStyle = "host";
sslCert = sslCertPath;
sslKey = sslKeyPath;
enableSubmissions = true;
# TODO: create function to simplify this?
mapFiles."virtual_aliases" = aliases_file;
mapFiles."virtual_accounts" = virtual_accounts_file;
virtual = lookupTableToString all_virtual_aliases;
submissionsOptions = {
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
# use mappedFile -> different path?
smtpd_sender_login_maps = "hash:/etc/postfix/virtual_accounts";
smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup";
};
config = {
mydestination = "";
recipient_delimiter = "+";
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
disable_vrfy_command = true;
message_size_limit = toString cfg.messageSizeLimit;
virtual_uid_maps = "static:${toString cfg.vmailUID}";
virtual_gid_maps = "static:${toString cfg.vmailUID}";
virtual_mailbox_base = cfg.mailDirectory;
virtual_mailbox_domains = virtual_domains_file;
virtual_mailbox_maps = [
(mappedFile "virtual_aliases")
];
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
# sasl with dovecot (enforce authentication via dovecot)
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_auth_enable = true;
smtpd_relay_restrictions = [
"permit_mynetworks"
"permit_sasl_authenticated"
"reject_unauth_destination"
];
# TLS settings, inspired by https://github.com/jeaye/nix-files
# Submission by mail clients is handled in submissionOptions
smtpd_tls_security_level = "may";
# Disable obsolete protocols
smtpd_tls_protocols = tls_protocols;
smtp_tls_protocols = tls_protocols;
smtpd_tls_mandatory_protocols = tls_protocols;
smtp_tls_mandatory_protocols = tls_protocols;
smtp_tls_ciphers = "high";
smtpd_tls_ciphers = "high";
smtp_tls_mandatory_ciphers = "high";
smtpd_tls_mandatory_ciphers = "high";
# Disable deprecated ciphers
smtpd_tls_mandatory_exclude_ciphers = tls_exclude_ciphers;
smtpd_tls_exclude_ciphers = tls_exclude_ciphers;
smtp_tls_mandatory_exclude_ciphers = tls_exclude_ciphers;
smtp_tls_exclude_ciphers = tls_exclude_ciphers;
tls_preempt_cipherlist = true;
# Allowing AUTH on a non-encrypted connection poses a security risk
smtpd_tls_auth_only = true;
# Log only a summary message on TLS handshake completion
smtpd_tls_loglevel = "1";
# Configure a non-blocking source of randomness
tls_random_source = "dev:/dev/urandom";
# Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = "yes";
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
};
masterConfig = {
"lmtp" = {
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
# D => Delivered-To, O => X-Original-To, R => Return-Path
args = ["flags=O"];
};
"submission-header-cleanup" = {
type = "unix";
private = false;
chroot = false;
maxproc = 0;
command = "cleanup";
args = ["-o" "header_checks=pcre:${submission_header_cleanup_rules}"];
};
};
};
systemd.services.postfix = {
wants = sslCertService;
after =
["dovecot2.service"]
++ sslCertService;
requires = ["dovecot2.service"];
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
25 # smtp
465 # submissions
];
};
};
}