mailsystem: Add basic postfix configuration
This commit is contained in:
parent
bc01d4d2d0
commit
b7fac23bd1
3 changed files with 268 additions and 1 deletions
|
|
@ -1,4 +1,10 @@
|
||||||
{lib, ...}: {
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
cfg = config.mailsystem;
|
||||||
|
in {
|
||||||
options.mailsystem = {
|
options.mailsystem = {
|
||||||
enable = lib.mkEnableOption "nixos-mailsystem";
|
enable = lib.mkEnableOption "nixos-mailsystem";
|
||||||
|
|
||||||
|
|
@ -14,6 +20,32 @@
|
||||||
description = "Fully qualified domain name of the mail server.";
|
description = "Fully qualified domain name of the mail server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reverseFqdn = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = cfg.fqdn;
|
||||||
|
defaultText = lib.literalMD "{option}`mailsystem.fqdn`";
|
||||||
|
example = "server.example.com";
|
||||||
|
description = ''
|
||||||
|
Fully qualified domain name used by the server to identify
|
||||||
|
with other servers.
|
||||||
|
|
||||||
|
This needs to be set to the same value of the server's IP reverse DNS.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
domains = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
example = ["example.com"];
|
||||||
|
default = [];
|
||||||
|
description = "List of domains to be served by the mail server";
|
||||||
|
};
|
||||||
|
|
||||||
|
messageSizeLimit = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
default = 64 * 1024 * 1024;
|
||||||
|
description = "Maximum accepted mail size";
|
||||||
|
};
|
||||||
|
|
||||||
vmailUID = lib.mkOption {
|
vmailUID = lib.mkOption {
|
||||||
type = lib.types.int;
|
type = lib.types.int;
|
||||||
default = 5000;
|
default = 5000;
|
||||||
|
|
@ -60,6 +92,17 @@
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
aliases = lib.mkOption {
|
||||||
|
type = with lib.types; listOf types.str;
|
||||||
|
example = ["abuse@example.com" "postmaster@example.com"];
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
A list of aliases of this login account.
|
||||||
|
Note: Use list entries like "@example.com" to create a catchAll
|
||||||
|
that allows sending from all email addresses in these domain.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
isSystemUser = lib.mkOption {
|
isSystemUser = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
|
@ -83,11 +126,40 @@
|
||||||
description = "All available login account for the mailsystem.";
|
description = "All available login account for the mailsystem.";
|
||||||
default = {};
|
default = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extraVirtualAliases = lib.mkOption {
|
||||||
|
type = let
|
||||||
|
account = lib.mkOptionType {
|
||||||
|
name = "Login Account";
|
||||||
|
check = account: builtins.elem account (builtins.attrNames cfg.accounts);
|
||||||
|
};
|
||||||
|
in
|
||||||
|
with lib.types; attrsOf (either account (nonEmptyListOf account));
|
||||||
|
example = {
|
||||||
|
"info@example.com" = "user1@example.com";
|
||||||
|
"postmaster@example.com" = "user1@example.com";
|
||||||
|
"abuse@example.com" = "user1@example.com";
|
||||||
|
"multi@example.com" = ["user1@example.com" "user2@example.com"];
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
|
||||||
|
all mail to `info@example.com` is forwarded to `user1@example.com`. Note
|
||||||
|
that it is expected that `postmaster@example.com` and `abuse@example.com` is
|
||||||
|
forwarded to some valid email address. (Alternatively you can create login
|
||||||
|
accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows
|
||||||
|
the user `user1@example.com` to send emails as `info@example.com`.
|
||||||
|
It's also possible to create an alias for multiple accounts. In this
|
||||||
|
example all mails for `multi@example.com` will be forwarded to both
|
||||||
|
`user1@example.com` and `user2@example.com`.
|
||||||
|
'';
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
./dovecot.nix
|
./dovecot.nix
|
||||||
./nginx.nix
|
./nginx.nix
|
||||||
|
./postfix.nix
|
||||||
./user.nix
|
./user.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ in {
|
||||||
networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [80 443];
|
networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [80 443];
|
||||||
|
|
||||||
security.acme.certs."${cfg.fqdn}".reloadServices = [
|
security.acme.certs."${cfg.fqdn}".reloadServices = [
|
||||||
|
"postfix.service"
|
||||||
"dovecot2.service"
|
"dovecot2.service"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
194
mailsystem/postfix.nix
Normal file
194
mailsystem/postfix.nix
Normal 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
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue