Welcome to NixOS Mailserver’s documentation!

SNM Logo

Setup Guide

Mail servers can be a tricky thing to set up. This guide is supposed to run you through the most important steps to achieve a 10/10 score on https://mail-tester.com.

What you need is:

  • a server running NixOS with a public IP
  • a domain name.

Note

In the following, we consider a server with the public IP 1.2.3.4 and the domain example.com.

First, we will set the minimum DNS configuration to be able to deploy an up and running mail server. Once the server is deployed, we could then set all DNS entries required to send and receive mails on this server.

Setup DNS A record for server

Add a DNS record to the domain example.com with the following entries

Name (Subdomain) TTL Type Value
mail.example.com 10800 A 1.2.3.4

You can check this with

$ ping mail.example.com
64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms
...

Note that it can take a while until a DNS entry is propagated. This DNS entry is required for the Let’s Encrypt certificate generation (which is used in the below configuration example).

Setup the server

The following describes a server setup that is fairly complete. Even though there are more possible options (see the default.nix file), these should be the most common ones.

{ config, pkgs, ... }:
{
  imports = [
    (builtins.fetchTarball {
      # Pick a commit from the branch you are interested in
      url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
      # And set its hash
      sha256 = "0000000000000000000000000000000000000000000000000000";
    })
  ];

  mailserver = {
    enable = true;
    fqdn = "mail.example.com";
    domains = [ "example.com" ];

    # A list of all login accounts. To create the password hashes, use
    # nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
    loginAccounts = {
        "user1@example.com" = {
            hashedPasswordFile = "/a/file/containing/a/hashed/password";
            aliases = ["postmaster@example.com"];
        };
        "user2@example.com" = { ... };
    };

    # Use Let's Encrypt certificates. Note that this needs to set up a stripped
    # down nginx and opens port 80.
    certificateScheme = 3;
  };
}

After a nixos-rebuild switch your server should be running all mail components.

Setup all other DNS requirements

Set rDNS (reverse DNS) entry for server

Wherever you have rented your server, you should be able to set reverse DNS entries for the IP’s you own. Add an entry resolving 1.2.3.4 to mail.example.com

You can check this with

$ nix-shell -p bind --command "host 1.2.3.4"
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.

Note that it can take a while until a DNS entry is propagated.

Set a MX record

Add a MX record to the domain example.com.

Name (Subdomain) Type Priority Value
example.com MX 10 mail.example.com

You can check this with

$ nix-shell -p bind --command "host -t mx example.com"
example.com mail is handled by 10 mail.example.com.

Note that it can take a while until a DNS entry is propagated.

Set a SPF record

Add a SPF record to the domain example.com.

Name (Subdomain) TTL Type Value
example.com 10800 TXT v=spf1 a:mail.example.com -all

You can check this with

$ nix-shell -p bind --command "host -t TXT example.com"
example.com descriptive text "v=spf1 a:mail.example.com -all"

Note that it can take a while until a DNS entry is propagated.

Set DKIM signature

On your server, the opendkim systemd service generated a file containing your DKIM public key in the file /var/dkim/example.com.mail.txt. The content of this file looks like

mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=<really-long-key>" ; ----- DKIM mail for domain.tld

where really-long-key is your public key.

Based on the content of this file, we can add a DKIM record to the domain example.com.

Name (Subdomain) TTL Type Value
mail._domainkey.example.com 10800 TXT v=DKIM1; p=<really-long-key>

You can check this with

$ nix-shell -p bind --command "host -t txt mail._domainkey.example.com"
mail._domainkey.example.com descriptive text "v=DKIM1;p=<really-long-key>"

Note that it can take a while until a DNS entry is propagated.

Set a DMARC record

Add a DMARC record to the domain example.com.

Name (Subdomain) TTL Type Value
_dmarc.example.com 10800 TXT v=DMARC1; p=none

You can check this with

$ nix-shell -p bind --command "host -t TXT _dmarc.example.com"
_dmarc.example.com descriptive text "v=DMARC1; p=none"

Note that it can take a while until a DNS entry is propagated.

Test your Setup

Write an email to your aunt (who has been waiting for your reply far too long), and sign up for some of the finest newsletters the Internet has. Maybe you want to sign up for the SNM Announcement List?

Besides that, you can send an email to mail-tester.com and see how you score, and let mxtoolbox.com take a look at your setup, but if you followed the steps closely then everything should be awesome!

Contribute or troubleshoot

To report an issue, please go to https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues.

You can also chat with us on the Libera IRC channel #nixos-mailserver.

Run NixOS tests

You can run the testsuite via

$ nix-build tests -A external.nixpkgs_20_03
$ nix-build tests -A internal.nixpkgs_unstable
...

Contributing to the documentation

The documentation is written in RST, build with Sphinx and published by Read the Docs.

For the syntax, see RST/Sphinx Cheatsheet.

The shell.nix provides all the tooling required to build the documentation:

$ nix-shell
$ cd docs
$ make html
$ firefox ./_build/html/index.html

Nixops

You can test the setup via nixops. After installation, do

$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
$ nixops deploy -d mail
$ nixops info -d mail

You can then test the server via e.g. telnet. To log into it, use

$ nixops ssh -d mail mailserver

Imap

To test imap manually use

$ openssl s_client -host mail.example.com -port 143 -starttls imap

FAQ

catchAll users can’t send email as user other than themself

To allow a catchAll user to send mail with the address used as recipient, the option aliases has to be used instead of catchAll.

For instance, to allow user@example.com to catch all mails to the domain example.com and send mails with any address of this domain:

mailserver.loginAccounts = {
    "user@example.com" = {
        aliases = [ "@example.com" ];
    };
};

See also this discussion for details.

Release Notes

NixOS 21.05

  • New fullTextSearch option to search in messages (based on Xapian) (Merge Request)
  • Flake support (Merge Request)
  • New openFirewall option defaulting to true
  • We moved from Freenode to Libera Chat

NixOS 20.09

  • IMAP and Submission with TLS wrapped-mode are now enabled by default on ports 993 and 465 respectively
  • OpenDKIM is now sandboxed with Systemd
  • New forwards option to forwards emails to external addresses (Merge Request)
  • New sendingFqdn option to specify the fqdn of the machine sending email (Merge Request)
  • Move the Gitlab wiki to ReadTheDocs

Backup Guide

First off you should have a backup of your configuration.nix file where you have the server config (but that is already in a git repository right?)

Next you need to backup /var/vmail or whatever you have specified for the option mailDirectory. This is where all the mails reside. Good options are a cron job with rsync or scp. But really anything works, as it is simply a folder with plenty of files in it. If your backup solution does not preserve the owner of the files don’t forget to chown them to virtualMail:virtualMail if you copy them back (or whatever you specified as vmailUserName, and vmailGoupName).

Finally you can (optionally) make a backup of /var/dkim (or whatever you specified as dkimKeyDirectory). If you should lose those don’t worry, new ones will be created on the fly. But you will need to repeat step B)5 and correct all the dkim keys.

Add Radicale

Configuration by @dotlambda

Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional crypt passwords, as generated by mkpasswd, are no longer supported. Instead bcrypt passwords have to be used which can be generated using htpasswd.

{ config, pkgs, lib, ... }:

with lib;

let
  mailAccounts = config.mailserver.loginAccounts;
  htpasswd = pkgs.writeText "radicale.users" (concatStrings
    (flip mapAttrsToList mailAccounts (mail: user:
      mail + ":" + user.hashedPassword + "\n"
    ))
  );

in {
  services.radicale = {
    enable = true;
    config = ''
      [auth]
      type = htpasswd
      htpasswd_filename = ${htpasswd}
      htpasswd_encryption = bcrypt
    '';
  };

  services.nginx = {
    enable = true;
    virtualHosts = {
      "cal.example.com" = {
        forceSSL = true;
        enableACME = true;
        locations."/" = {
          proxyPass = "http://localhost:5232/";
          extraConfig = ''
            proxy_set_header  X-Script-Name /;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass_header Authorization;
          '';
        };
      };
    };
  };

  networking.firewall.allowedTCPPorts = [ 80 443 ];
}

Tune spam filtering

SNM comes with the rspamd spam filtering system enabled by default. Although its out-of-the-box performance is good, you can increase its efficiency by tuning its behaviour.

Auto-learning

Moving spam email to the Junk folder (and false-positives out of it) will trigger an automatic training of the Bayesian filters, improving filtering of future emails.

Train from existing folders

If you kept previous spam, you can train the filter from it. Note that the rspamd FAQ indicates that you should always learn both classes with almost equal amount of messages to increase performance of the statistical engine.

You can run the training in a root shell as follows:

# Path to the controller socket
export RSOCK="/var/run/rspamd/worker-controller.sock"

# Learn the Junk folder as spam
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/

# Learn the INBOX as ham
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/

# Check that training was successful
rspamc -h $RSOCK stat | grep learned

Tune symbol weight

The X-Spamd-Result header is automatically added to your emails, detailing the scoring decisions. The modules documentation details the meaning of each symbol. You can tune the weight if a symbol if needed.

services.rspamd.locals = {
  "groups.conf".text = ''
    symbols {
      "FORGED_RECIPIENTS" { weight = 0; }
    }'';
};

Tune action thresholds

After scoring the message, rspamd decides on an action based on configurable thresholds. By default, rspamd will tell postfix to reject any message with a score higher than 15. If you experience issues in scoring or want to stay on the safe side, you can disable this behaviour by tuning the configuration. For example:

services.rspamd.extraConfig = ''
  actions {
    reject = null; # Disable rejects, default is 15
    add_header = 6; # Add header when reaching this score
    greylist = 4; # Apply greylisting when reaching this score
  }
'';

Access the rspamd web UI

Rspamd comes with a web interface that displays statistics and history of past scans. We do NOT recommend using it to change the configuration as doing so will override values from the configuration set in the previous sections.

The UI is served on the /var/run/rspamd/worker-controller.sock Unix socket. Here are two ways to access it from your browser.

With ssh forwarding

For occasional access, the simplest way is to forward the socket to localhost and open http://localhost:3333 in your browser.

ssh -L 3333:/run/rspamd/worker-controller.sock $HOSTNAME

With an nginx reverse-proxy

If you have a secured nginx reverse proxy set on the host, you can use it to expose the socket. Keep in mind the UI is unsecured by default, you need to setup an authentication scheme, for exemple with basic auth:

services.nginx.virtualHosts.rspamd = {
  forceSSL = true;
  enableACME = true;
  basicAuthFile = "/basic/auth/hashes/file";
  serverName = "rspamd.example.com";
  locations = {
    "/" = {
      proxyPass = "http://unix:/run/rspamd/worker-controller.sock:/";
    };
  };
};

Nix Flakes

If you’re using flakes, you can use the following minimal flake.nix as an example:

{
  description = "NixOS configuration";

  inputs.simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-20.09";

  outputs = { self, nixpkgs, simple-nixos-mailserver }: {
    nixosConfigurations = {
      hostname = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          simple-nixos-mailserver.nixosModule
          {
            mailserver = {
              enable = true;
              # ...
            };
          }
        ];
      };
    };
  };
}

Indices and tables