garuda-mail (Netcup VPS)

General

This system mainly consists of the simple-nixos-mailserver. Its only purpose is providing a mail service to team members. The current config looks like this. In case of issues, the documentation can be consulted.

Mail server setup

The mail server details are as follows:

  • host: mail.garudalinux.net
  • username: full email address
  • password: given password
  • incoming: IMAP via 993 (SSL)
  • outgoing: SMTP via 587/465 (STARTTLS/SSL)

Additionally, it is possible to make use of the Roundcube-powered web interface.

Roundcube

Roundcube is used to provide a web interface for our mail accounts. It features a few plugins to enhance the general user experience.

Plugins

  • attachment_reminder - reminds about forgotten attachments
  • authres_status - checks for whether SPF/DKIM/DMARC match the sending domain
  • carddav - allows adding a CardDAV contact book as source (eg. Nextcloud)
  • contextmenu - adds a right click context menu to the most pages
  • custom_from - allows customizing from address
  • managesieve - allows managing Sieve rules, which automatically sort incoming mails based on rules
  • newmail_notifier - new mail notifier for desktops
  • persistent_login - allows storing a persistent login cookie for no more login prompts
  • thunderbird_labels - shows Thunderbird labels
  • zipdownload - allows downloading all attachments at once

Backups

Backups are happening daily via Borg. A Hetzner storage box is used to store multiple generations of backups.

Creating a new user

A new user can be created be adding a new loginAccounts value and supplying the password via secrets. We make use of hashedPasswordFile, therefore, new hashes can be generated by running nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'. Add it to the secrets, then execute deploy and apply. Remember to commit both changes.

Issues and their solution

Local DNS resolver failing to start

Simple NixOS mail server runs a local DNS server to prevent the log filling up with junk (source). There can be cases of the persisted files need to be deleted in order for the service to recover from dumping core. See this issue for reference.

Nix expression

{ config
, lib
, pkgs
, ...
}:
let
  authres_status = pkgs.roundcubePlugins.roundcubePlugin rec {
    pname = "authres_status";
    version = "0.6.3";
    src = pkgs.fetchzip {
      url = "https://github.com/pimlie/authres_status/archive/refs/tags/${version}.zip";
      hash = "sha256-WebJiN0vRkvc0AKvMm+inK3FY37R04q3y/0rFoiUW6A=";
    };
  };
in
{
  imports = [
    ../modules
    ./garuda-mail/hardware-configuration.nix
  ];

  # Base configuration
  networking.interfaces.ens3.ipv4.addresses = [{
    address = "94.16.112.218";
    prefixLength = 22;
  }];
  networking.hostName = "garuda-mail";
  networking.defaultGateway = "94.16.112.3";

  # GRUB
  boot.loader.grub.devices = [ "/dev/vda" ];

  # Backup configurations to Hetzner storage box
  programs.ssh.macs = [ "hmac-sha2-512" ];
  services.borgbackup.jobs = {
    backupToHetzner = {
      compression = "auto,zstd";
      doInit = true;
      encryption = {
        mode = "repokey-blake2";
        passCommand = "cat /var/garuda/secrets/backup/repo_key";
      };
      environment = {
        BORG_RSH = "ssh -i /var/garuda/secrets/backup/ssh_garuda-mail -p 23";
      };
      paths = [ config.mailserver.mailDirectory "/var/dkim" ];
      prune.keep = {
        within = "1d";
        daily = 5;
        weekly = 2;
        monthly = 1;
      };
      repo = "[email protected]:./garuda-mail";
      startAt = "daily";
    };
  };

  # NixOS Mailserver
  mailserver = {
    certificateScheme = "acme-nginx";
    dmarcReporting = {
      domain = "garudalinux.org";
      enable = true;
      organizationName = "Garuda Linux";
    };
    domains = [ "garudalinux.org" "chaotic.cx" "dr460nf1r3.org" ];
    enable = true;
    enableManageSieve = true;
    # Forwards (mostly chaotic.cx only)
    forwards =
      {
        "[email protected]" = "[email protected]";
        "[email protected]" = "[email protected]";
        "[email protected]" = "[email protected]";
        "[email protected]" = "[email protected]";
      };
    fqdn = "mail.garudalinux.net";
    fullTextSearch = {
      enable = true;
      enforced = "body";
      indexAttachments = true;
      memoryLimit = 512;
    };
    # To create the password hashes, use nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
    loginAccounts = {
      # garudalinux.org
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/cloudatgl";
        sendOnly = true;
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/complaintsatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/dr460nf1r3atgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/filoatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/gitlabatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/mastodonatgl";
        sendOnly = true;
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/namanatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/noreplyatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/rohitatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/securityatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/sgsatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/spam-reportsatgl";
      };
      "[email protected]" = {
        aliases = [
          "[email protected]"
          "[email protected]"
          "[email protected]"
          "[email protected]"
        ];
        hashedPasswordFile = "/var/garuda/secrets/mail/teamatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/tneatgl";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/yorperatgl";
      };
      # chaotic.cx
      "[email protected]" = {
        aliases = [ "[email protected]" ];
        hashedPasswordFile = "/var/garuda/secrets/mail/actionsatcx";
      };
      "[email protected]" = {
        aliases = [
          "[email protected]"
          "[email protected]"
          "[email protected]"
        ];
        hashedPasswordFile = "/var/garuda/secrets/mail/nicoatcx";
      };
      # dr460nf1r3.org
      "[email protected]" = {
        aliases = [ "@dr460nf1r3.org" ];
        catchAll = [ "dr460nf1r3.org" ];
        hashedPasswordFile = "/var/garuda/secrets/mail/nicoatdf";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/testatdf";
      };
      "[email protected]" = {
        hashedPasswordFile = "/var/garuda/secrets/mail/noreplyatdf";
      };
    };
    indexDir = "/var/lib/dovecot/indices";
    monitoring = {
      alertAddress = "[email protected]";
      enable = true;
    };
    rebootAfterKernelUpgrade.enable = true;
  };

  # Fix dovecot errors caused by failed scudo allocations
  environment.memoryAllocator.provider = lib.mkForce "libc";

  # Postmaster alias
  services.postfix.postmasterAlias = "[email protected]";

  # Web UI
  services.roundcube = {
    enable = true;
    # this is the url of the vhost, not necessarily the same as the fqdn of
    # the mailserver
    hostName = "mail.garudalinux.net";
    extraConfig = ''
      # starttls needed for authentication, so the fqdn required to match
      # the certificate
      $config['smtp_server'] = "tls://${config.mailserver.fqdn}";
      $config['smtp_user'] = "%u";
      $config['smtp_pass'] = "%p";
    '';
    package = pkgs.roundcube.withPlugins (
      plugins: [
        authres_status
        plugins.carddav
        plugins.contextmenu
        plugins.custom_from
        plugins.persistent_login
        plugins.thunderbird_labels
      ]
    );
    plugins = [
      "attachment_reminder" # Roundcube internal plugin
      "authres_status"
      "carddav"
      "contextmenu"
      "custom_from"
      "managesieve" # Roundcube internal plugin
      "newmail_notifier" # Roundcube internal plugin
      "persistent_login"
      "thunderbird_labels"
      "zipdownload" # Roundcube internal plugin
    ];
  };

  # At least try to prevent the insane spam of login attempts
  services.openssh.ports = [ 1022 ];

  # This mostly sends annoying notifications because SSH port is non-default
  services.monit.enable = lib.mkForce false;

  system.stateVersion = "22.05";
}