diff --git a/hosts/hetzner-vm/containers/mail/mail.nix b/hosts/hetzner-vm/containers/mail/mail.nix new file mode 100644 index 0000000..ce4792e --- /dev/null +++ b/hosts/hetzner-vm/containers/mail/mail.nix @@ -0,0 +1,144 @@ +{ + tree, + lib, + inputs, + config, + pkgs, + ... +}: let + container-addresses = import ../../data/container-addresses.nix {}; + hostIP = container-addresses.host; + containerIP = container-addresses.containers.mail; + + ports = [ + # SMTP + 25 + # Submission + 587 + # Submission w/ SSL + 465 + # IMAP + 143 + # IMAP w/ SSL + 993 + # Sieve + 4190 + ]; + + # Using secrets from Host + secrets = config.services.secrets.secrets; + secrets_list = [ + "mail_restic_password" + "mail_restic_env" + "private_mail_aliases" + "chaos_mail_passwd" + "system_mail_passwd" + ]; + shared_files = [ + "/var/lib/acme/mail.owo.monster/fullchain.pem" + "/var/lib/acme/mail.owo.monster/key.pem" + ]; +in { + containers.mail = { + autoStart = true; + privateNetwork = true; + hostAddress = hostIP; + localAddress = containerIP; + bindMounts = lib.mkMerge [ + (lib.mkMerge (lib.forEach secrets_list (secret_name: let + path = "${secrets.${secret_name}.path}"; + in { + "${path}" = { + hostPath = "${path}"; + }; + }))) + (lib.mkMerge (lib.forEach shared_files (file: { + "${file}" = { + hostPath = "${file}"; + }; + }))) + ]; + + config = { + config, + pkgs, + ... + }: { + _module.args = { + inherit inputs; + inherit tree; + host_secrets = secrets; + }; + + imports = with tree; + [ + profiles.base + inputs.home-manager-unstable.nixosModules.home-manager + hosts.hetzner-vm.modules.mailserver + + profiles.sshd + + modules.nixos.secrets + + users.root + ] + ++ (with hosts.hetzner-vm.containers.mail; [ + profiles.mailserver + profiles.restic + ]); + + # For Shared Secrets + systemd.tmpfiles.rules = [ + "d ${config.services.secrets.secretsDir} - root root" + "d /var/lib/acme - root root" + "d /var/lib/acme/mail.owo.monster - root root" + ]; + + networking.firewall = { + enable = true; + allowedTCPPorts = [22] ++ ports; + }; + + home-manager.users.root = { + imports = with tree; [home.base home.dev.small]; + + home.stateVersion = "23.05"; + }; + + # Manually configure nameserver. Using resolved inside the container seems to fail + # currently + environment.etc."resolv.conf".text = "nameserver 8.8.8.8"; + system.stateVersion = "23.05"; + }; + }; + + # users for secrets + users.users."dovecot2" = { + uid = config.ids.uids.dovecot2; + group = "dovecot2"; + }; + users.groups."dovecot2".gid = config.ids.gids.dovecot2; + + # ssl for mail + services.nginx = { + enable = true; + virtualHosts."mail.owo.monster" = { + serverName = "mail.owo.monster"; + serverAliases = ["owo.monster"]; + forceSSL = true; + enableACME = true; + acmeRoot = "/var/lib/acme/acme-challenge"; + # also being used for webmail + locations."/" = { + proxyPass = "http://unix:/var/lib/nixos-containers/mail/var/sockets/roundcube.sock"; + }; + }; + }; + + networking.nat.forwardPorts = lib.forEach ports ( + port: { + sourcePort = port; + destination = "${containerIP}\:${toString port}"; + } + ); +} diff --git a/hosts/hetzner-vm/containers/mail/profiles/mailserver.nix b/hosts/hetzner-vm/containers/mail/profiles/mailserver.nix new file mode 100644 index 0000000..b0d96e7 --- /dev/null +++ b/hosts/hetzner-vm/containers/mail/profiles/mailserver.nix @@ -0,0 +1,61 @@ +{host_secrets, ...}: let + secrets = host_secrets; +in { + config.mailserver = { + enable = true; + fqdn = "mail.owo.monster"; + domains = ["owo.monster"]; + + ssl_config = { + useACME = false; + cert = "/var/lib/acme/mail.owo.monster/fullchain.pem"; + key = "/var/lib/acme/mail.owo.monster/key.pem"; + }; + + enable_roundcube = true; + force_roundcube_ssl = false; + force_roundcube_acme = false; + + debug_mode = true; + + extra_roundcube_config = '' + $config['session_lifetime'] = (60 * 24 * 7 * 2); # 2 Weeks + $config['product_name'] = 'Chaos Mail'; + $config['username_domain'] = "owo.monster"; + $config['username_domain_forced'] = true; + ''; + + extra_aliases_file = "${secrets.private_mail_aliases.path}"; + + accounts = { + "chaos@owo.monster" = { + name = "chaos@owo.monster"; + passwordFile = "${secrets.chaos_mail_passwd.path}"; + aliases = [ + "all@owo.monster" + "chaoticryptidz@owo.monster" + ]; + sieveScript = null; + }; + + "system@owo.monster" = { + name = "system@owo.monster"; + passwordFile = "${secrets.system_mail_passwd.path}"; + aliases = []; + sieveScript = null; + }; + }; + }; + + config.systemd.tmpfiles.rules = [ + "d /var/sockets - nginx nginx" + ]; + + config.systemd.services.nginx.serviceConfig.ReadWritePaths = [ + "/var/sockets" + ]; + + config.services.nginx.virtualHosts."mail.owo.monster" = { + extraConfig = "listen unix:/var/sockets/roundcube.sock;"; + }; +} diff --git a/hosts/hetzner-vm/profiles/restic.nix b/hosts/hetzner-vm/containers/mail/profiles/restic.nix similarity index 53% rename from hosts/hetzner-vm/profiles/restic.nix rename to hosts/hetzner-vm/containers/mail/profiles/restic.nix index 5356892..18ac0ef 100644 --- a/hosts/hetzner-vm/profiles/restic.nix +++ b/hosts/hetzner-vm/containers/mail/profiles/restic.nix @@ -1,43 +1,33 @@ { - lib, - config, pkgs, + config, + lib, + host_secrets, ... }: let - secrets = config.services.secrets.secrets; + secrets = host_secrets; mail_config = config.mailserver; - backupPrepareCommand = "${ (pkgs.writeShellScriptBin "backupPrepareCommand" '' - systemctl start ${ - lib.concatStringsSep " " - (lib.forEach config.services.postgresqlBackup.databases - (db: "postgresqlBackup-${db}")) - } --wait + systemctl start postgresqlBackup-roundcube --wait '') }/bin/backupPrepareCommand"; in { environment.systemPackages = with pkgs; [ restic - - (pkgs.writeShellScriptBin "restic-hetzner-vm" '' + (pkgs.writeShellScriptBin "restic-mail" '' env \ - RESTIC_PASSWORD_FILE=${secrets.restic_password.path} \ - $(cat ${secrets.restic_env.path}) \ + RESTIC_PASSWORD_FILE=${secrets.mail_restic_password.path} \ + $(cat ${secrets.mail_restic_env.path}) \ ${pkgs.restic}/bin/restic $@ '') ]; - services.restic.backups.hetzner-vm = { + services.restic.backups.mail = { user = "root"; paths = [ - "/var/lib/acme" - - # Quassel & Invidious "/var/backup/postgresql" - "/home/quassel/.config/quassel-irc.org" - # mail mail_config.vmail_config.directory mail_config.sieve_directory mail_config.dkim_directory @@ -46,12 +36,12 @@ in { # repository is overrided in environmentFile to contain auth # make sure to keep up to date when changing repository - repository = "rest:https://storage-restic.owo.monster/HetznerVM"; - passwordFile = "${secrets.restic_password.path}"; - environmentFile = "${secrets.restic_env.path}"; + repository = "rest:https://storage-restic.owo.monster/Mail"; + passwordFile = "${secrets.mail_restic_password.path}"; + environmentFile = "${secrets.mail_restic_env.path}"; pruneOpts = [ - "--keep-last 20" + "--keep-last 5" ]; timerConfig = { @@ -62,10 +52,11 @@ in { inherit backupPrepareCommand; }; + services.postgresql.enable = true; services.postgresqlBackup = { enable = true; backupAll = false; - databases = ["postgres" "quassel" "roundcube"]; + databases = ["roundcube"]; compression = "zstd"; }; } diff --git a/hosts/hetzner-vm/containers/storage/data/ports.nix b/hosts/hetzner-vm/containers/storage/data/ports.nix index 41ef77b..6f292f2 100644 --- a/hosts/hetzner-vm/containers/storage/data/ports.nix +++ b/hosts/hetzner-vm/containers/storage/data/ports.nix @@ -9,6 +9,7 @@ rclone_serve_restic_social = 4213; rclone_serve_restic_quassel = 4214; rclone_serve_restic_piped = 4215; + rclone_serve_restic_mail = 4216; rclone_serve_http_music = 4220; rclone_serve_http_public = 4221; diff --git a/hosts/hetzner-vm/containers/storage/profiles/rclone-serve.nix b/hosts/hetzner-vm/containers/storage/profiles/rclone-serve.nix index cf6969c..8dcbb43 100644 --- a/hosts/hetzner-vm/containers/storage/profiles/rclone-serve.nix +++ b/hosts/hetzner-vm/containers/storage/profiles/rclone-serve.nix @@ -143,6 +143,17 @@ in { ]; inherit serviceConfig; } + { + user = "storage"; + remote = "StorageBox:Backups/Restic/Mail"; + type = "restic"; + extraArgs = [ + "--addr=0.0.0.0:${toString ports.rclone_serve_restic_mail}" + "--htpasswd=${secrets.restic_mail_htpasswd.path}" + "--baseurl=/Mail/" + ]; + inherit serviceConfig; + } ]; }; } diff --git a/hosts/hetzner-vm/containers/storage/profiles/secrets.nix b/hosts/hetzner-vm/containers/storage/profiles/secrets.nix index b238b00..9bd1612 100644 --- a/hosts/hetzner-vm/containers/storage/profiles/secrets.nix +++ b/hosts/hetzner-vm/containers/storage/profiles/secrets.nix @@ -101,6 +101,16 @@ ''; }; + restic_mail_htpasswd = { + user = "storage"; + group = "storage"; + fetchScript = '' + username=$(simple_get "/api-keys/storage/restic/Mail" .username) + password=$(simple_get "/api-keys/storage/restic/Mail" .password) + htpasswd -bc "$secretFile" "$username" "$password" 2>/dev/null + ''; + }; + webdav_main_htpasswd = { user = "storage"; group = "storage"; diff --git a/hosts/hetzner-vm/containers/storage/storage.nix b/hosts/hetzner-vm/containers/storage/storage.nix index 87b17b7..56f131d 100644 --- a/hosts/hetzner-vm/containers/storage/storage.nix +++ b/hosts/hetzner-vm/containers/storage/storage.nix @@ -101,6 +101,7 @@ in { "/Social/".proxyPass = "http://${containerIP}:${toString ports.rclone_serve_restic_social}"; "/Quassel/".proxyPass = "http://${containerIP}:${toString ports.rclone_serve_restic_quassel}"; "/Piped/".proxyPass = "http://${containerIP}:${toString ports.rclone_serve_restic_piped}"; + "/Mail/".proxyPass = "http://${containerIP}:${toString ports.rclone_serve_restic_mail}"; }; }; } diff --git a/hosts/hetzner-vm/data/container-addresses.nix b/hosts/hetzner-vm/data/container-addresses.nix index fb8b33d..896c9d9 100644 --- a/hosts/hetzner-vm/data/container-addresses.nix +++ b/hosts/hetzner-vm/data/container-addresses.nix @@ -6,5 +6,6 @@ music = "192.168.100.13"; quassel = "192.168.100.14"; piped = "192.168.100.15"; + mail = "192.168.100.16"; }; } diff --git a/hosts/hetzner-vm/hetzner-vm.nix b/hosts/hetzner-vm/hetzner-vm.nix index 5c9a0e9..56b95a1 100644 --- a/hosts/hetzner-vm/hetzner-vm.nix +++ b/hosts/hetzner-vm/hetzner-vm.nix @@ -20,9 +20,10 @@ ./containers/music/music.nix ./containers/quassel/quassel.nix ./containers/piped/piped.nix + ./containers/mail/mail.nix - hosts.hetzner-vm.profiles.restic - hosts.hetzner-vm.profiles.mailserver + #hosts.hetzner-vm.profiles.restic + #hosts.hetzner-vm.profiles.mailserver hosts.hetzner-vm.profiles.gitlab-static-sites hosts.hetzner-vm.profiles.wireguard hosts.hetzner-vm.profiles.nginx-misc diff --git a/hosts/hetzner-vm/modules/mailserver/default.nix b/hosts/hetzner-vm/modules/mailserver/default.nix index 975e6a4..a840ad0 100644 --- a/hosts/hetzner-vm/modules/mailserver/default.nix +++ b/hosts/hetzner-vm/modules/mailserver/default.nix @@ -48,6 +48,16 @@ in { default = "${cfg.fqdn}"; }; + force_roundcube_ssl = mkOption { + type = types.bool; + default = true; + }; + + force_roundcube_acme = mkOption { + type = types.bool; + default = true; + }; + accounts = mkOption { # where name = email for login type = types.attrsOf (types.submodule ({name, ...}: { diff --git a/hosts/hetzner-vm/modules/mailserver/webmail.nix b/hosts/hetzner-vm/modules/mailserver/webmail.nix index f3f545e..8230c64 100644 --- a/hosts/hetzner-vm/modules/mailserver/webmail.nix +++ b/hosts/hetzner-vm/modules/mailserver/webmail.nix @@ -18,5 +18,10 @@ in { ${mail_config.extra_roundcube_config} ''; }; + + services.nginx.virtualHosts."${mail_config.roundcube_url}" = { + forceSSL = mail_config.force_roundcube_ssl; + enableACME = mail_config.force_roundcube_acme; + }; }; } diff --git a/hosts/hetzner-vm/secrets.nix b/hosts/hetzner-vm/secrets.nix index f51a4b2..7a5722f 100644 --- a/hosts/hetzner-vm/secrets.nix +++ b/hosts/hetzner-vm/secrets.nix @@ -114,6 +114,19 @@ ''; }; + mail_restic_password = { + fetchScript = '' + simple_get "/private-public-keys/restic/Mail" .password > $secretFile + ''; + }; + mail_restic_env = { + fetchScript = '' + RESTIC_USERNAME=$(simple_get "/api-keys/storage/restic/Mail" .username) + RESTIC_PASSWORD=$(simple_get "/api-keys/storage/restic/Mail" .password) + echo "RESTIC_REPOSITORY=rest:https://$RESTIC_USERNAME:$RESTIC_PASSWORD@storage-restic.owo.monster/Mail" > $secretFile + ''; + }; + restic_password = { fetchScript = '' simple_get "/private-public-keys/restic/HetznerVM" .password > $secretFile diff --git a/hosts/lappy-t495/lappy-t495.nix b/hosts/lappy-t495/lappy-t495.nix index 473ff5d..5171b59 100644 --- a/hosts/lappy-t495/lappy-t495.nix +++ b/hosts/lappy-t495/lappy-t495.nix @@ -37,7 +37,7 @@ networking.firewall.allowedTCPPorts = [8088]; networking.hostName = "lappy-t495"; - time.timeZone = "Europe/Germany"; + time.timeZone = "Europe/London"; system.stateVersion = "23.05"; }