diff --git a/config/hosts/daiyousei.nix b/config/hosts/daiyousei.nix index 791df59e..fd3d5b06 100644 --- a/config/hosts/daiyousei.nix +++ b/config/hosts/daiyousei.nix @@ -5,7 +5,9 @@ profiles.network services.nginx services.keycloak + services.roundcube services.openldap + services.mail services.hedgedoc services.dnscrypt-proxy ]; diff --git a/config/hosts/kyouko.nix b/config/hosts/kyouko.nix index 295780b1..0deaee7e 100644 --- a/config/hosts/kyouko.nix +++ b/config/hosts/kyouko.nix @@ -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 diff --git a/config/modules/nixos/network.nix b/config/modules/nixos/network.nix index 72a34dd8..2924c33a 100644 --- a/config/modules/nixos/network.nix +++ b/config/modules/nixos/network.nix @@ -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); }; diff --git a/config/profiles/hardware/oracle/common.nix b/config/profiles/hardware/oracle/common.nix index 57990c45..b12dcd61 100644 --- a/config/profiles/hardware/oracle/common.nix +++ b/config/profiles/hardware/oracle/common.nix @@ -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"; }; }; diff --git a/config/services/mail/autoconfig.nix b/config/services/mail/autoconfig.nix new file mode 100644 index 00000000..571a012b --- /dev/null +++ b/config/services/mail/autoconfig.nix @@ -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" '' + + + + kittywit.ch + kittywit.ch Mail + em0lar + + ${config.network.addresses.public.domain} + 993 + SSL + password-cleartext + %EMAILADDRESS% + + + ${config.network.addresses.public.domain} + 465 + SSL + password-cleartext + %EMAILADDRESS% + + + + ''; + }; + }; + }; + }; +} diff --git a/config/services/mail/default.nix b/config/services/mail/default.nix index e2f68758..f0498d43 100644 --- a/config/services/mail/default.nix +++ b/config/services/mail/default.nix @@ -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 + ]; } diff --git a/config/services/mail/dns.nix b/config/services/mail/dns.nix new file mode 100644 index 00000000..c5b17b07 --- /dev/null +++ b/config/services/mail/dns.nix @@ -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); +} diff --git a/config/services/mail/dovecot.nix b/config/services/mail/dovecot.nix new file mode 100644 index 00000000..c1e455bb --- /dev/null +++ b/config/services/mail/dovecot.nix @@ -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 = /run/dovecot2/ldap.conf + ''; + + networking.firewall.allowedTCPPorts = [ + 143 # imap + 993 # imaps + 4190 # sieve + ]; +} diff --git a/config/services/mail/opendkim.nix b/config/services/mail/opendkim.nix new file mode 100644 index 00000000..125cc407 --- /dev/null +++ b/config/services/mail/opendkim.nix @@ -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} - -" + ]; + }; +} diff --git a/config/services/mail/postfix.nix b/config/services/mail/postfix.nix new file mode 100644 index 00000000..d094670c --- /dev/null +++ b/config/services/mail/postfix.nix @@ -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 + ]; +} diff --git a/config/services/mail/rspamd.nix b/config/services/mail/rspamd.nix new file mode 100644 index 00000000..a05b3876 --- /dev/null +++ b/config/services/mail/rspamd.nix @@ -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 ]; + }; +} + diff --git a/config/services/nixos-mailserver/default.nix b/config/services/nixos-mailserver/default.nix new file mode 100644 index 00000000..e2f68758 --- /dev/null +++ b/config/services/nixos-mailserver/default.nix @@ -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" ]; + }; + } + ]; + }; +} diff --git a/config/services/openldap/default.nix b/config/services/openldap/default.nix index 2e1a04e8..3db95905 100644 --- a/config/services/openldap/default.nix +++ b/config/services/openldap/default.nix @@ -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)'' + ]; + }; }; }; }; diff --git a/config/services/openldap/mail.ldif b/config/services/openldap/mail.ldif new file mode 100644 index 00000000..685ebe0d --- /dev/null +++ b/config/services/openldap/mail.ldif @@ -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 + + diff --git a/config/targets/rinnosuke-domains.nix b/config/targets/rinnosuke-domains.nix index 2f12f728..ef976639 100644 --- a/config/targets/rinnosuke-domains.nix +++ b/config/targets/rinnosuke-domains.nix @@ -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; }; }; }; diff --git a/config/users/kat/personal/email.nix b/config/users/kat/personal/email.nix index 22783eaf..f2d461d5 100644 --- a/config/users/kat/personal/email.nix +++ b/config/users/kat/personal/email.nix @@ -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";