services/mail: Init

This commit is contained in:
kat witch 2021-09-10 22:17:32 +01:00
parent 1e869f3579
commit 377d4b45a2
No known key found for this signature in database
GPG key ID: 1B477797DCA5EC72
16 changed files with 865 additions and 118 deletions

View file

@ -5,7 +5,9 @@
profiles.network
services.nginx
services.keycloak
services.roundcube
services.openldap
services.mail
services.hedgedoc
services.dnscrypt-proxy
];

View file

@ -13,15 +13,14 @@ with lib;
services.filehost
services.gitea
services.logrotate
services.mail
# services.nixos-mailserver
services.matrix
services.murmur
services.nginx
services.postgres
services.prosody
services.radicale
# services.radicale
services.restic
services.roundcube
services.syncplay
services.taskserver
services.vaultwarden

View file

@ -18,6 +18,9 @@ in
type = types.bool;
default = options.nixos.ipv4.address.isDefined;
};
selfaddress = mkOption {
type = types.str;
};
address = mkOption {
type = types.str;
};
@ -27,6 +30,9 @@ in
type = types.bool;
default = options.nixos.ipv6.address.isDefined;
};
selfaddress = mkOption {
type = types.str;
};
address = mkOption {
type = types.str;
};
@ -145,6 +151,8 @@ in
ipv6.address = mkIf (cfg.tf.ipv6_attr != null) (tf.resources.${config.networking.hostName}.refAttr cfg.tf.ipv6_attr);
};
nixos = {
ipv4.selfaddress = mkIf (tf.state.enable && cfg.tf.ipv4_attr != null) (tf.resources.${config.networking.hostName}.getAttr cfg.tf.ipv4_attr);
ipv6.selfaddress = mkIf (tf.state.enable && cfg.tf.ipv6_attr != null) (tf.resources.${config.networking.hostName}.getAttr cfg.tf.ipv6_attr);
ipv4.address = mkIf (tf.state.resources ? ${tf.resources.${config.networking.hostName}.out.reference} && cfg.tf.ipv4_attr != null) (tf.resources.${config.networking.hostName}.importAttr cfg.tf.ipv4_attr);
ipv6.address = mkIf (tf.state.resources ? ${tf.resources.${config.networking.hostName}.out.reference} && cfg.tf.ipv6_attr != null) (tf.resources.${config.networking.hostName}.importAttr cfg.tf.ipv6_attr);
};

View file

@ -93,6 +93,7 @@ in
{
enable = true;
nixos.ipv6.address = mkIf tf.state.enable addr_ipv6_nix;
nixos.ipv6.selfaddress = mkIf tf.state.enable addr_ipv6_nix;
tf.ipv6.address = tf.resources."${config.networking.hostName}_ipv6".refAttr "ip_address";
};
};

View file

@ -0,0 +1,43 @@
{ pkgs, lib, config, ... }:
let
commonHeaders = lib.concatStringsSep "\n" (lib.filter (line: lib.hasPrefix "add_header" line) (lib.splitString "\n" config.services.nginx.commonHttpConfig));
in {
services.nginx.virtualHosts = {
"autoconfig.kittywit.ch" = {
enableACME = true;
forceSSL = true;
serverAliases = [
"autoconfig.dork.dev"
];
locations = {
"= /mail/config-v1.1.xml" = {
root = pkgs.writeTextDir "mail/config-v1.1.xml" ''
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="kittywit.ch">
<domain>kittywit.ch</domain>
<displayName>kittywit.ch Mail</displayName>
<displayShortName>em0lar</displayShortName>
<incomingServer type="imap">
<hostname>${config.network.addresses.public.domain}</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>${config.network.addresses.public.domain}</hostname>
<port>465</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>
'';
};
};
};
};
}

View file

@ -1,101 +1,10 @@
{ config, lib, tf, pkgs, sources, ... }:
with lib;
let
domains = [ "kittywitch" "dork" ];
users = [ "gitea" "kat" "keycloak" "vaultwarden" ];
in
{
imports = [ sources.nixos-mailserver.outPath ];
kw.secrets.variables = listToAttrs (map
(field:
nameValuePair "mail-${field}-hash" {
path = "secrets/mail-kittywitch";
field = "${field}-hash";
})
users
++ map
(domain:
nameValuePair "mail-domainkey-${domain}" {
path = "secrets/mail-${domain}";
field = "notes";
})
domains);
deploy.tf.dns.records = mkMerge (map
(domain:
let
zoneGet = domain: if domain == "dork" then "dork.dev." else config.network.dns.zone;
in
{
"services_mail_${domain}_mx" = {
zone = zoneGet domain;
mx = {
priority = 10;
target = "${config.network.addresses.public.domain}.";
};
};
"services_mail_${domain}_spf" = {
zone = zoneGet domain;
txt.value = "v=spf1 ip4:${config.network.addresses.public.nixos.ipv4.address} ip6:${config.network.addresses.public.nixos.ipv6.address} -all";
};
"services_mail_${domain}_dmarc" = {
zone = zoneGet domain;
domain = "_dmarc";
txt.value = "v=DMARC1; p=none";
};
"services_mail_${domain}_domainkey" = {
zone = zoneGet domain;
domain = "mail._domainkey";
txt.value = tf.variables."mail-domainkey-${domain}".ref;
};
})
domains);
secrets.files = listToAttrs (map
(user:
nameValuePair "mail-${user}-hash" {
text = ''
${tf.variables."mail-${user}-hash".ref}
'';
})
users);
mailserver = {
enable = true;
fqdn = config.network.addresses.public.domain;
domains = [ "kittywit.ch" "dork.dev" ];
certificateScheme = 1;
certificateFile = "/var/lib/acme/public_${config.networking.hostName}/cert.pem";
keyFile = "/var/lib/acme/public_${config.networking.hostName}/key.pem";
enableImap = true;
enablePop3 = true;
enableImapSsl = true;
enablePop3Ssl = true;
enableSubmission = false;
enableSubmissionSsl = true;
enableManageSieve = true;
virusScanning = false;
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
loginAccounts = mkMerge [
(listToAttrs (map
(user:
nameValuePair "${user}@kittywit.ch" {
hashedPasswordFile = config.secrets.files."mail-${user}-hash".path;
})
users))
{
"kat@kittywit.ch" = {
aliases = [ "postmaster@kittywit.ch" ];
catchAll = [ "kittywit.ch" "dork.dev" ];
};
}
];
};
{ ... }: {
imports = [
./dns.nix
./rspamd.nix
./postfix.nix
./dovecot.nix
./opendkim.nix
./autoconfig.nix
];
}

View file

@ -0,0 +1,51 @@
{ config, pkgs, lib, tf, ... }: with lib; let
domains = [ "dork" "kittywitch" ];
in {
kw.secrets.variables = listToAttrs (map
(domain:
nameValuePair "mail-domainkey-${domain}" {
path = "secrets/mail-${domain}";
field = "notes";
})
domains);
deploy.tf.dns.records = mkMerge (map
(domain:
let
zoneGet = domain: if domain == "dork" then "dork.dev." else config.network.dns.zone;
in
{
"services_mail_${domain}_autoconfig_cname" = {
zone = zoneGet domain;
domain = "autoconfig";
cname = { inherit (config.network.addresses.public) target; };
};
"services_mail_${domain}_mx" = {
zone = zoneGet domain;
mx = {
priority = 10;
inherit (config.network.addresses.public) target;
};
};
"services_mail_${domain}_spf" = {
zone = zoneGet domain;
txt.value = "v=spf1 ip4:${config.network.addresses.public.tf.ipv4.address} ip6:${config.network.addresses.public.tf.ipv6.address} -all";
};
"services_mail_${domain}_dmarc" = {
zone = zoneGet domain;
domain = "_dmarc";
txt.value = "v=DMARC1; p=none";
};
"services_mail_${domain}_domainkey" = {
zone = zoneGet domain;
domain = "mail._domainkey";
txt.value = tf.variables."mail-domainkey-${domain}".ref;
};
})
domains);
}

View file

@ -0,0 +1,177 @@
{ pkgs, config, lib, tf, ... }: with lib;
let
ldapConfig = pkgs.writeText "dovecot-ldap.conf" ''
uris = ldaps://auth.kittywit.ch:636
dn = cn=dovecot,dc=mail,dc=kittywit,dc=ch
dnpass = "@ldap-password@"
auth_bind = no
ldap_version = 3
base = ou=users,dc=kittywit,dc=ch
user_filter = (&(objectClass=mailAccount)(mail=%u))
user_attrs = \
quota=quota_rule=*:bytes=%$, \
=home=/var/vmail/%d/%n/, \
=mail=maildir:/var/vmail/%d/%n/Maildir
pass_attrs = mail=user,userPassword=password
pass_filter = (&(objectClass=mailAccount)(mail=%u))
iterate_attrs = =user=%{ldap:mail}
iterate_filter = (objectClass=mailAccount)
scope = subtree
default_pass_scheme = SSHA
'';
in
{
security.acme.certs.dovecot_domains = {
inherit (config.network.dns) domain;
group = "postfix";
dnsProvider = "rfc2136";
credentialsFile = config.secrets.files.dns_creds.path;
postRun = "systemctl restart dovecot2";
extraDomainNames =
[
config.network.dns.domain
config.network.addresses.public.domain
"dork.dev"
];
};
services.dovecot2 = {
enable = true;
enableImap = true;
enableLmtp = true;
enablePAM = false;
mailLocation = "maildir:/var/vmail/%d/%n/Maildir";
mailUser = "vmail";
mailGroup = "vmail";
extraConfig = ''
ssl = yes
ssl_cert = </var/lib/acme/dovecot_domains/fullchain.pem
ssl_key = </var/lib/acme/dovecot_domains/key.pem
local_name kittywit.ch {
ssl_cert = </var/lib/acme/dovecot_domains/fullchain.pem
ssl_key = </var/lib/acme/dovecot_domains/key.pem
}
local_name dork.dev {
ssl_cert = </var/lib/acme/dovecot_domains/fullchain.pem
ssl_key = </var/lib/acme/dovecot_domains/key.pem
}
ssl_min_protocol = TLSv1.2
ssl_cipher_list = EECDH+AESGCM:EDH+AESGCM
ssl_prefer_server_ciphers = yes
ssl_dh=<${config.security.dhparams.params.dovecot2.path}
mail_plugins = virtual fts fts_lucene
service lmtp {
user = vmail
unix_listener /var/lib/postfix/queue/private/dovecot-lmtp {
group = postfix
mode = 0600
user = postfix
}
}
service doveadm {
inet_listener {
port = 4170
ssl = yes
}
}
protocol lmtp {
postmaster_address=postmaster@kittywit.ch
hostname=${config.network.addresses.public.domain}
mail_plugins = $mail_plugins sieve
}
service auth {
unix_listener auth-userdb {
mode = 0640
user = vmail
group = vmail
}
# Postfix smtp-auth
unix_listener /var/lib/postfix/queue/private/auth {
mode = 0666
user = postfix
group = postfix
}
}
userdb {
args = /run/dovecot2/ldap.conf
driver = ldap
}
passdb {
args = /run/dovecot2/ldap.conf
driver = ldap
}
service imap-login {
client_limit = 1000
service_count = 0
inet_listener imaps {
port = 993
}
}
service managesieve-login {
inet_listener sieve {
port = 4190
}
}
protocol sieve {
managesieve_logout_format = bytes ( in=%i : out=%o )
}
plugin {
sieve_dir = /var/vmail/%d/%n/sieve/scripts/
sieve = /var/vmail/%d/%n/sieve/active-script.sieve
sieve_extensions = +vacation-seconds
sieve_vacation_min_period = 1min
fts = lucene
fts_lucene = whitespace_chars=@.
}
# If you have Dovecot v2.2.8+ you may get a significant performance improvement with fetch-headers:
imapc_features = $imapc_features fetch-headers
# Read multiple mails in parallel, improves performance
mail_prefetch_count = 20
'';
modules = [
pkgs.dovecot_pigeonhole
];
protocols = [
"sieve"
];
};
users.users.vmail = {
home = "/var/vmail";
createHome = true;
isSystemUser = true;
uid = 1042;
shell = "/run/current-system/sw/bin/nologin";
};
security.dhparams = {
enable = true;
params.dovecot2 = { };
};
kw.secrets.variables."dovecot-ldap-password" = {
path = "services/dovecot";
field = "password";
};
secrets.files.dovecot-ldap-password.text = ''
${tf.variables.dovecot-ldap-password.ref}
'';
systemd.services.dovecot2.preStart = ''
sed -e "s!@ldap-password@!$(<${config.secrets.files.dovecot-ldap-password.path})!" ${ldapConfig} > /run/dovecot2/ldap.conf
'';
networking.firewall.allowedTCPPorts = [
143 # imap
993 # imaps
4190 # sieve
];
}

View file

@ -0,0 +1,71 @@
{ config, lib, pkgs, ... }:
with lib;
let
dkimUser = config.services.opendkim.user;
dkimGroup = config.services.opendkim.group;
dkimKeyDirectory = "/var/dkim";
dkimKeyBits = 1024;
dkimSelector = "mail";
domains = [ "kittywit.ch" "dork.dev" ];
createDomainDkimCert = dom:
let
dkim_key = "${dkimKeyDirectory}/${dom}.${dkimSelector}.key";
dkim_txt = "${dkimKeyDirectory}/${dom}.${dkimSelector}.txt";
in
''
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
then
${pkgs.opendkim}/bin/opendkim-genkey -s "${dkimSelector}" \
-d "${dom}" \
--bits="${toString dkimKeyBits}" \
--directory="${dkimKeyDirectory}"
mv "${dkimKeyDirectory}/${dkimSelector}.private" "${dkim_key}"
mv "${dkimKeyDirectory}/${dkimSelector}.txt" "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${dkimSelector}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert domains);
keyTable = pkgs.writeText "opendkim-KeyTable"
(lib.concatStringsSep "\n" (lib.flip map domains
(dom: "${dom} ${dom}:${dkimSelector}:${dkimKeyDirectory}/${dom}.${dkimSelector}.key")));
signingTable = pkgs.writeText "opendkim-SigningTable"
(lib.concatStringsSep "\n" (lib.flip map domains (dom: "${dom} ${dom}")));
dkim = config.services.opendkim;
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
in
{
config = {
services.opendkim = {
enable = true;
selector = dkimSelector;
keyPath = dkimKeyDirectory;
domains = "csl:${builtins.concatStringsSep "," domains}";
configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization relaxed/simple
UMask 0002
Socket ${dkim.socket}
KeyTable file:${keyTable}
SigningTable file:${signingTable}
'');
};
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
postfix.extraGroups = [ "${dkimGroup}" ];
};
systemd.services.opendkim = {
preStart = lib.mkForce createAllCerts;
serviceConfig = {
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
PermissionsStartOnly = lib.mkForce false;
};
};
systemd.tmpfiles.rules = [
"d '${dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
];
};
}

View file

@ -0,0 +1,200 @@
{ pkgs, lib, config, tf, ... }:
let
publicCert = "public_${config.networking.hostName}";
ldaps = "ldaps://auth.${config.network.dns.domain}:636";
virtualRegex = pkgs.writeText "virtual-regex" ''
/^kat\.[^@.]+@kittywit\.ch$/ kat@kittywit.ch
/^kat\.[^@.]+@dork\.dev$/ kat@kittywit.ch
'';
helo_access = pkgs.writeText "helo_access" ''
${config.network.addresses.public.nixos.ipv4.selfaddress} REJECT Get lost - you're lying about who you are
${config.network.addresses.public.nixos.ipv6.selfaddress} REJECT Get lost - you're lying about who you are
kittywit.ch REJECT Get lost - you're lying about who you are
dork.dev REJECT Get lost - you're lying about who you are
'';
in {
kw.secrets.variables."postfix-ldap-password" = {
path = "services/dovecot";
field = "password";
};
secrets.files = {
domains-ldap = {
text = ''
server_host = ${ldaps}
search_base = dc=domains,dc=mail,dc=kittywit,dc=ch
query_filter = (&(dc=%s)(objectClass=mailDomain))
result_attribute = postfixTransport
bind = yes
bind_dn = cn=dovecot,dc=mail,dc=kittywit,dc=ch
bind_pw = ${tf.variables.postfix-ldap-password.ref}
scope = one
'';
owner = "postfix";
group = "postfix";
};
accountsmap-ldap = {
text = ''
server_host = ${ldaps}
search_base = ou=users,dc=kittywit,dc=ch
query_filter = (&(objectClass=mailAccount)(mail=%s))
result_attribute = mail
bind = yes
bind_dn = cn=dovecot,dc=mail,dc=kittywit,dc=ch
bind_pw = ${tf.variables.postfix-ldap-password.ref}
'';
owner = "postfix";
group = "postfix";
};
aliases-ldap = {
text = ''
server_host = ${ldaps}
search_base = dc=aliases,dc=mail,dc=kittywit,dc=ch
query_filter = (&(objectClass=mailAlias)(mail=%s))
result_attribute = maildrop
bind = yes
bind_dn = cn=dovecot,dc=mail,dc=kittywit,dc=ch
bind_pw = ${tf.variables.postfix-ldap-password.ref}
'';
owner = "postfix";
group = "postfix";
};
};
services.postfix = {
enable = true;
enableSubmission = true;
hostname = config.network.addresses.public.domain;
domain = config.network.dns.domain;
masterConfig."465" = {
type = "inet";
private = false;
command = "smtpd";
args = [
"-o smtpd_client_restrictions=permit_sasl_authenticated,reject"
"-o syslog_name=postfix/smtps"
"-o smtpd_tls_wrappermode=yes"
"-o smtpd_sasl_auth_enable=yes"
"-o smtpd_tls_security_level=none"
"-o smtpd_reject_unlisted_recipient=no"
"-o smtpd_recipient_restrictions="
"-o smtpd_relay_restrictions=permit_sasl_authenticated,reject"
"-o milter_macro_daemon_name=ORIGINATING"
];
};
mapFiles."virtual-regex" = virtualRegex;
mapFiles."helo_access" = helo_access;
extraConfig = ''
smtp_bind_address = ${if tf.state.enable then tf.resources.${config.networking.hostName}.getAttr "private_ip" else config.network.addresses.public.nixos.ipv4.selfaddress}
smtp_bind_address6 = ${config.network.addresses.public.nixos.ipv6.selfaddress}
mailbox_transport = lmtp:unix:private/dovecot-lmtp
masquerade_domains = ldap:${config.secrets.files.domains-ldap.path}
virtual_mailbox_domains = ldap:${config.secrets.files.domains-ldap.path}
virtual_alias_maps = ldap:${config.secrets.files.accountsmap-ldap.path},ldap:${config.secrets.files.aliases-ldap.path},regexp:/var/lib/postfix/conf/virtual-regex
virtual_transport = lmtp:unix:private/dovecot-lmtp
smtpd_milters = unix:/run/opendkim/opendkim.sock,unix:/run/rspamd/rspamd-milter.sock
non_smtpd_milters = unix:/run/opendkim/opendkim.sock
milter_protocol = 6
milter_default_action = accept
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}
# bigger attachement size
mailbox_size_limit = 202400000
message_size_limit = 51200000
smtpd_helo_required = yes
smtpd_delay_reject = yes
strict_rfc821_envelopes = yes
# send Limit
smtpd_error_sleep_time = 1s
smtpd_soft_error_limit = 10
smtpd_hard_error_limit = 20
smtpd_use_tls = yes
smtp_tls_note_starttls_offer = yes
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /var/lib/acme/${publicCert}/full.pem
smtpd_tls_key_file = /var/lib/acme/${publicCert}/key.pem
smtpd_tls_CAfile = /var/lib/acme/${publicCert}/fullchain.pem
smtpd_tls_dh512_param_file = ${config.security.dhparams.params.postfix512.path}
smtpd_tls_dh1024_param_file = ${config.security.dhparams.params.postfix2048.path}
smtpd_tls_session_cache_database = btree:''${data_directory}/smtpd_scache
smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
smtpd_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1
smtpd_tls_mandatory_ciphers = medium
tls_medium_cipherlist = AES128+EECDH:AES128+EDH
# authentication
smtpd_sasl_auth_enable = yes
smtpd_sasl_local_domain = $mydomain
smtpd_sasl_security_options = noanonymous
smtpd_sasl_tls_security_options = $smtpd_sasl_security_options
smtpd_sasl_type = dovecot
smtpd_sasl_path = /var/lib/postfix/queue/private/auth
smtpd_relay_restrictions = permit_mynetworks,
permit_sasl_authenticated,
defer_unauth_destination
smtpd_client_restrictions = permit_mynetworks,
permit_sasl_authenticated,
reject_invalid_hostname,
reject_unknown_client,
permit
smtpd_helo_restrictions = permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_pipelining,
reject_non_fqdn_hostname,
reject_invalid_hostname,
warn_if_reject reject_unknown_hostname,
permit
smtpd_recipient_restrictions = permit_mynetworks,
permit_sasl_authenticated,
reject_non_fqdn_sender,
reject_non_fqdn_recipient,
reject_non_fqdn_hostname,
reject_invalid_hostname,
reject_unknown_sender_domain,
reject_unknown_recipient_domain,
reject_unknown_client_hostname,
reject_unauth_pipelining,
reject_unknown_client,
permit
smtpd_sender_restrictions = permit_mynetworks,
permit_sasl_authenticated,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_unknown_client_hostname,
reject_unknown_address
smtpd_etrn_restrictions = permit_mynetworks, reject
smtpd_data_restrictions = reject_unauth_pipelining, reject_multi_recipient_bounce, permit
'';
};
systemd.services.postfix.wants = [ "openldap.service" "acme-${publicCert}.service" ];
systemd.services.postfix.after = [ "openldap.service" "acme-${publicCert}.service" "network.target" ];
security.dhparams = {
enable = true;
params.postfix512.bits = 512;
params.postfix2048.bits = 1024;
};
networking.firewall.allowedTCPPorts = [
25 # smtp
465 # stmps
587 # submission
];
}

View file

@ -0,0 +1,85 @@
{ config, pkgs, lib, ... }:
let
postfixCfg = config.services.postfix;
rspamdCfg = config.services.rspamd;
rspamdSocket = "rspamd.service";
in
{
config = {
services.rspamd = {
enable = true;
locals = {
"milter_headers.conf" = { text = ''
extended_spam_headers = yes;
''; };
"redis.conf" = { text = ''
servers = "127.0.0.1:${toString config.services.redis.port}";
''; };
"classifier-bayes.conf" = { text = ''
cache {
backend = "redis";
}
''; };
"dkim_signing.conf" = { text = ''
# Disable outbound email signing, we use opendkim for this
enabled = false;
''; };
};
overrides = {
"milter_headers.conf" = {
text = ''
extended_spam_headers = true;
'';
};
};
workers.rspamd_proxy = {
type = "rspamd_proxy";
bindSockets = [{
socket = "/run/rspamd/rspamd-milter.sock";
mode = "0664";
}];
count = 1; # Do not spawn too many processes of this type
extraConfig = ''
milter = yes; # Enable milter mode
timeout = 120s; # Needed for Milter usually
upstream "local" {
default = yes; # Self-scan upstreams are always default
self_scan = yes; # Enable self-scan
}
'';
};
workers.controller = {
type = "controller";
count = 1;
bindSockets = [{
socket = "/run/rspamd/worker-controller.sock";
mode = "0666";
}];
includes = [];
extraConfig = ''
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
'';
};
};
services.redis.enable = true;
systemd.services.rspamd = {
requires = [ "redis.service" ];
after = [ "redis.service" ];
};
systemd.services.postfix = {
after = [ rspamdSocket ];
requires = [ rspamdSocket ];
};
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
};
}

View file

@ -0,0 +1,101 @@
{ config, lib, tf, pkgs, sources, ... }:
with lib;
let
domains = [ "kittywitch" "dork" ];
users = [ "gitea" "kat" "keycloak" "vaultwarden" ];
in
{
imports = [ sources.nixos-mailserver.outPath ];
kw.secrets.variables = listToAttrs (map
(field:
nameValuePair "mail-${field}-hash" {
path = "secrets/mail-kittywitch";
field = "${field}-hash";
})
users
++ map
(domain:
nameValuePair "mail-domainkey-${domain}" {
path = "secrets/mail-${domain}";
field = "notes";
})
domains);
deploy.tf.dns.records = mkMerge (map
(domain:
let
zoneGet = domain: if domain == "dork" then "dork.dev." else config.network.dns.zone;
in
{
"services_mail_${domain}_mx" = {
zone = zoneGet domain;
mx = {
priority = 10;
target = "${config.network.addresses.public.domain}.";
};
};
"services_mail_${domain}_spf" = {
zone = zoneGet domain;
txt.value = "v=spf1 ip4:${config.network.addresses.public.nixos.ipv4.address} ip6:${config.network.addresses.public.nixos.ipv6.address} -all";
};
"services_mail_${domain}_dmarc" = {
zone = zoneGet domain;
domain = "_dmarc";
txt.value = "v=DMARC1; p=none";
};
"services_mail_${domain}_domainkey" = {
zone = zoneGet domain;
domain = "mail._domainkey";
txt.value = tf.variables."mail-domainkey-${domain}".ref;
};
})
domains);
secrets.files = listToAttrs (map
(user:
nameValuePair "mail-${user}-hash" {
text = ''
${tf.variables."mail-${user}-hash".ref}
'';
})
users);
mailserver = {
enable = true;
fqdn = config.network.addresses.public.domain;
domains = [ "kittywit.ch" "dork.dev" ];
certificateScheme = 1;
certificateFile = "/var/lib/acme/public_${config.networking.hostName}/cert.pem";
keyFile = "/var/lib/acme/public_${config.networking.hostName}/key.pem";
enableImap = true;
enablePop3 = true;
enableImapSsl = true;
enablePop3Ssl = true;
enableSubmission = false;
enableSubmissionSsl = true;
enableManageSieve = true;
virusScanning = false;
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
loginAccounts = mkMerge [
(listToAttrs (map
(user:
nameValuePair "${user}@kittywit.ch" {
hashedPasswordFile = config.secrets.files."mail-${user}-hash".path;
})
users))
{
"kat@kittywit.ch" = {
aliases = [ "postmaster@kittywit.ch" ];
catchAll = [ "kittywit.ch" "dork.dev" ];
};
}
];
};
}

View file

@ -56,18 +56,70 @@
olcRootDN = "cn=root,dc=kittywit,dc=ch";
olcRootPW.path = config.secrets.files.openldap-root-password-file.path;
olcAccess = [
"{0}to attrs=userPassword
''{0}to attrs=userPassword
by anonymous auth
by dn.base="cn=dovecot,dc=mail,dc=kittywit,dc=ch" read
by self write
by * none"
"{1}to *
by dn.children=\"ou=users,dc=kittywit,dc=ch\" write
by self read by * none"
"{2}to dn.subtree=\"dc=kittywit,dc=ch\"
by dn.exact=\"cn=root,dc=kittywit,dc=ch\" manage"
by * none''
''{1}to dn.subtree="dc=kittywit,dc=ch"
by dn.exact="cn=root,dc=kittywit,dc=ch" manage
by dn.base="cn=dovecot,dc=mail,dc=kittywit,dc=ch" read''
''{2}to dn.subtree="ou=users,dc=kittywit,dc=ch"
by dn.base="cn=dovecot,dc=mail,dc=kittywit,dc=ch" read
by dn.subtree="ou=users,dc=kittywit,dc=ch" read
by * none''
''{3}to * by * read''
];
};
};
"cn={2}postfix,cn=schema".attrs = {
cn = "{2}postfix";
objectClass = "olcSchemaConfig";
olcAttributeTypes = [
''( 1.3.6.1.4.1.4203.666.1.200 NAME 'mailAcceptingGeneralId'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )''
''(1.3.6.1.4.1.12461.1.1.1 NAME 'postfixTransport'
DESC 'A string directing postfix which transport to use'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{20} SINGLE-VALUE)''
''(1.3.6.1.4.1.12461.1.1.5 NAME 'mailbox'
DESC 'The absolute path to the mailbox for a mail account in a non-default location'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE)''
''(1.3.6.1.4.1.12461.1.1.6 NAME 'quota'
DESC 'A string that represents the quota on a mailbox'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE)''
''(1.3.6.1.4.1.12461.1.1.8 NAME 'maildrop'
DESC 'RFC822 Mailbox - mail alias'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256})''
];
olcObjectClasses = [
''(1.3.6.1.4.1.12461.1.2.1 NAME 'mailAccount'
SUP top AUXILIARY
DESC 'Mail account objects'
MUST ( mail $ userPassword )
MAY ( cn $ description $ quota))''
''(1.3.6.1.4.1.12461.1.2.2 NAME 'mailAlias'
SUP top STRUCTURAL
DESC 'Mail aliasing/forwarding entry'
MUST ( mail $ mailAcceptingGeneralId $ maildrop )
MAY ( cn $ description ))''
''(1.3.6.1.4.1.12461.1.2.3 NAME 'mailDomain'
SUP domain STRUCTURAL
DESC 'Virtual Domain entry to be used with postfix transport maps'
MUST ( dc )
MAY ( postfixTransport $ description ))''
''(1.3.6.1.4.1.12461.1.2.4 NAME 'mailPostmaster'
SUP top AUXILIARY
DESC 'Added to a mailAlias to create a postmaster entry'
MUST roleOccupant)''
];
};
};
};
};

View file

@ -0,0 +1,48 @@
dn: dc=mail,dc=kittywit,dc=ch
objectClass: dcObject
objectClass: organizationalUnit
objectClass: top
dc: mail
ou: mail
dn: cn=dovecot,dc=mail,dc=kittywit,dc=ch
objectClass: organizationalRole
objectClass: simpleSecurityObject
objectClass: top
cn: dovecot
userPassword: {SSHA}GenerateYourOwn
dn: dc=aliases,dc=mail,dc=kittywit,dc=ch
objectClass: dcObject
objectClass: organizationalUnit
objectClass: top
dc: aliases
ou: aliases
dn: dc=domains,dc=mail,dc=kittywit,dc=ch
objectClass: dcObject
objectClass: organizationalUnit
objectClass: top
dc: domains
ou: domains
dn: dc=kittywit.ch,dc=domains,dc=mail,dc=kittywit,dc=ch
objectClass: mailDomain
objectClass: top
dc: kittywit.ch
postfixTransport: kittywit.ch
dn: dc=dork.dev,dc=domains,dc=mail,dc=kittywit,dc=ch
objectClass: top
objectClass: mailDomain
dc: dork.dev
postfixTransport: virtual:
dn: mail=kat@kittywit.ch,dc=aliases,dc=mail,dc=kittywit,dc=ch
objectClass: top
objectClass: mailAlias
mailAcceptingGeneralId: kittywit.ch
mailAcceptingGeneralId: @kittywit.ch
maildrop: kat@kittywit.ch

View file

@ -7,27 +7,27 @@ let rinnosuke = config.network.nodes.rinnosuke; in
node_public_rinnosuke_v4 = {
inherit (rinnosuke.network.dns) zone;
domain = rinnosuke.networking.hostName;
a.address = rinnosuke.network.addresses.public.nixos.ipv4.address;
a.address = rinnosuke.network.addresses.public.tf.ipv4.address;
};
node_public_rinnosuke_v6 = {
inherit (rinnosuke.network.dns) zone;
domain = rinnosuke.networking.hostName;
aaaa.address = rinnosuke.network.addresses.public.nixos.ipv6.address;
aaaa.address = rinnosuke.network.addresses.public.tf.ipv6.address;
};
node_wireguard_rinnosuke_v4 = {
inherit (rinnosuke.network.dns) zone;
domain = rinnosuke.network.addresses.wireguard.subdomain;
a.address = rinnosuke.network.addresses.wireguard.nixos.ipv4.address;
a.address = rinnosuke.network.addresses.wireguard.tf.ipv4.address;
};
node_wireguard_rinnosuke_v6 = {
inherit (rinnosuke.network.dns) zone;
domain = rinnosuke.network.addresses.wireguard.subdomain;
aaaa.address = rinnosuke.network.addresses.wireguard.nixos.ipv6.address;
aaaa.address = rinnosuke.network.addresses.wireguard.tf.ipv6.address;
};
node_yggdrasil_rinnosuke_v6 = {
inherit (rinnosuke.network.dns) zone;
domain = rinnosuke.network.addresses.yggdrasil.subdomain;
aaaa.address = rinnosuke.network.addresses.yggdrasil.nixos.ipv6.address;
aaaa.address = rinnosuke.network.addresses.yggdrasil.tf.ipv6.address;
};
};
};

View file

@ -34,9 +34,9 @@
boxes = [ "Inbox" ];
onNotifyPost = "${pkgs.notmuch}/bin/notmuch new && ${pkgs.libnotify}/bin/notify-send 'New mail arrived'";
};
imap.host = "kyouko.kittywit.ch";
smtp.host = "kyouko.kittywit.ch";
passwordCommand = "bitw get services/email/kittywitch -f password";
imap.host = "daiyousei.kittywit.ch";
smtp.host = "daiyousei.kittywit.ch";
passwordCommand = "bitw get services/kittywitch -f password";
gpg = {
signByDefault = true;
key = "01F50A29D4AA91175A11BDB17248991EFA8EFBEE";