Stalwart: Difference between revisions

Onny (talk | contribs)
Setup add admin user
Onny (talk | contribs)
Sending from subaddresses: Add note on planned feature
 
(22 intermediate revisions by 3 users not shown)
Line 2: Line 2:


== Setup ==
== Setup ==
The following example enables the Stalwart mail server for the domain ''example.org'', listening on mail delivery SMTP/Submission (<code>25, 465</code>), IMAPS (<code>993</code>) and JMAP ports (8080/443) for mail clients to connect to. Mailboxes for the accounts <code>postmaster@example.org</code> and <code>user1@example.org</code> get created if they don't exist yet.{{Note|Parts of this module are not yet stable will be available with the upcoming NixOS release 24.11.}}{{file|/etc/nixos/configuration.nix|nix|3=environment.etc = {
The following example enables the Stalwart mail server for the domain ''example.org'', listening on mail delivery SMTP/Submission (<code>25, 465</code>), IMAPS (<code>993</code>) and JMAP ports (8080/443) for mail clients to connect to. Mailboxes for the accounts <code>postmaster@example.org</code> and <code>user1@example.org</code> get created if they don't exist yet.
 
{{file|/etc/nixos/configuration.nix|nix|3=environment.etc = {
   "stalwart/mail-pw1".text = "foobar";
   "stalwart/mail-pw1".text = "foobar";
   "stalwart/mail-pw2".text = "foobar";
   "stalwart/mail-pw2".text = "foobar";
Line 11: Line 13:
services.stalwart-mail = {
services.stalwart-mail = {
   enable = true;
   enable = true;
  # Required before < NixOS 24.11 to use the latest Stalwart release
  package = pkgs.stalwart-mail;
   openFirewall = true;
   openFirewall = true;
   settings = {
   settings = {
     server = {
     server = {
       hostname = "example.org";
       hostname = "mx1.example.org";
       tls = {
       tls = {
         enable = true;
         enable = true;
Line 29: Line 29:
           bind = "[::]:465";
           bind = "[::]:465";
           protocol = "smtp";
           protocol = "smtp";
          tls.implicit = true
         };
         };
         imaps = {
         imaps = {
           bind = "[::]:993";
           bind = "[::]:993";
           protocol = "imap";
           protocol = "imap";
          tls.implicit = true
         };
         };
         jmap = {
         jmap = {
           bind = "[::]:8080";
           bind = "[::]:8080";
           url = "https://mail.example.org";
           url = "https://mail.example.org";
           protocol = "jmap";
           protocol = "http";
         };
         };
         management = {
         management = {
Line 53: Line 55:
       challenge = "dns-01";
       challenge = "dns-01";
       contact = "user1@example.org";
       contact = "user1@example.org";
       domains = [ "example.org" ];
       domains = [ "example.org" "mx1.example.org" ];
       provider = "cloudflare";
       provider = "cloudflare";
       secret = "%{file:/etc/stalwart/acme-secret}%";
       secret = "%{file:/etc/stalwart/acme-secret}%";
Line 63: Line 65:
     storage.directory = "in-memory";
     storage.directory = "in-memory";
     session.rcpt.directory = "'in-memory'";
     session.rcpt.directory = "'in-memory'";
    queue.outbound.next-hop = "'local'";
     directory."imap".lookup.domains = [ "example.org" ];
     directory."imap".lookup.domains = [ "example.org" ];
     directory."in-memory" = {
     directory."in-memory" = {
Line 82: Line 83:
       ];
       ];
     };
     };
     authentication.fallback-admin = {
     authentication.fallback-admin = {
       user = "admin";
       user = "admin";
Line 95: Line 95:
     "webadmin.example.org" = {
     "webadmin.example.org" = {
       extraConfig = ''
       extraConfig = ''
         reverse_proxy http://127.0.01:8080
         reverse_proxy http://127.0.0.1:8080
       '';
       '';
       serverAliases = [
       serverAliases = [
Line 105: Line 105:
     };
     };
   };
   };
};}}TLS key generation is done using DNS-01 challenge through Cloudflare domain provider, see dns-update library for [https://github.com/stalwartlabs/dns-update further providers] or configure [https://stalw.art/docs/server/tls/certificates manual certificates].
};}}


== Configuration ==
TLS key generation is done using DNS-01 challenge through Cloudflare domain provider, see dns-update library for [https://github.com/stalwartlabs/dns-update further providers] or configure [https://stalw.art/docs/server/tls/certificates manual certificates].


=== DNS records ===
=== DNS records ===
Before adding required records to the example domain <code>example.org</code>, we need to register the domain on the Stalwart server.<syntaxhighlight lang="shell">
Before adding required records to the example domain <code>example.org</code>, we need to register the domain on the Stalwart server.<syntaxhighlight lang="shell">
stalwart-cli --url https://webadmin.example.org domain example.org
stalwart-cli --url https://webadmin.example.org domain create example.org
</syntaxhighlight>
</syntaxhighlight>Authenticate using the fallback-admin password.
 
Review the list of which DNS records are required including their values for the mail server to work at https://webadmin.example.org/manage/directory/domains/tuxtux.com.co/view. Especially following records are essential:
 
{| class="wikitable"
! Record Type
! Name
! Value / Target
! Notes
|-
| A
| example.org
| ''IPv4 address of the mail server''
| Required
|-
| AAAA
| example.org
| ''IPv6 address of the mail server''
| Required
|-
| CNAME
| autoconfig
| example.org
| Mail client autoconfiguration
|-
| CNAME
| autodiscover
| example.org
| Outlook / Exchange compatibility
|-
| CNAME
| mail
| example.org
| Mail host
|-
| CNAME
| mta-sts
| example.org
| MTA-STS
|-
| CNAME
| webadmin
| example.org
| Stalwart web administration interface
|-
| MX
| example.org
| mx1.example.org
| Mail delivery
|-
| SRV
| _imaps._tcp
| ''See Web Admin for exact values''
| IMAPS service
|-
| SRV
| _submissions._tcp
| ''See Web Admin for exact values''
| SMTP Submission service
|-
| TLSA
| _25._tcp.example.org.
| 3 1 1 …
| Only the record starting with <code>3 1 1</code> is required
|-
| TLSA
| _25._tcp.mx1.example.org.
| 3 1 1 …
| Only the record starting with <code>3 1 1</code> is required
|-
| TXT
| 202409e._domainkey
| ''DKIM public key''
| DKIM
|-
| TXT
| 202409r._domainkey
| ''DKIM public key''
| DKIM
|-
| TXT
| _dmarc
| ''DMARC policy''
| DMARC
|-
| TXT
| mx1
| ''SPF or server information''
| Depends on configuration
|-
| TXT
| _smtp._tls
| ''MTA-STS policy''
| SMTP TLS reporting
|-
| TXT
| example.org
| ''SPF record''
| SPF
|}


=== DNSSEC ===
=== DNSSEC ===
Line 122: Line 221:
  _25._tcp.mx1.example.org. 10800 IN TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96 e3498238
  _25._tcp.mx1.example.org. 10800 IN TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96 e3498238
  _25._tcp.mx1.example.org. 10800 IN RRSIG TLSA 13 5 10800 20230601000000 20230511000000 39688 example.org. He9VYZ35xTC3fNo8GJa6swPrZodSnjjIWPG6Th2YbsOEKTV1E8eGtJ2A +eyBd9jgG+B3cA/jw8EJHmpvy/buCw==
  _25._tcp.mx1.example.org. 10800 IN RRSIG TLSA 13 5 10800 20230601000000 20230511000000 39688 example.org. He9VYZ35xTC3fNo8GJa6swPrZodSnjjIWPG6Th2YbsOEKTV1E8eGtJ2A +eyBd9jgG+B3cA/jw8EJHmpvy/buCw==
== Configuration ==
=== Mail aliases ===
Considering the configuration above, we could add a mail alias for <code>user1@example.org</code> by simply adding further addresses to the <code>email</code>-array such as <code>user1real@example.org</code>
{{file|/etc/nixos/configuration.nix|nix|3=services.stalwart-mail = {
  settings = {
    [...]
    directory."in-memory" = {
      type = "memory";
      principals = [
        {
          class = "individual";
          name = "User 1";
          secret = "%{file:/etc/stalwart/mail-pw1}%";
          email = [ "user1@example.org" "user1real@example.org ];
        }
      ];
    };
  };
};}}


== Tips and tricks ==
== Tips and tricks ==


=== Auto update TLSA records ===
Stalwart [https://github.com/stalwartlabs/stalwart/issues/1664 does not yet] automatically update the TLSA record if your ACME certificate changes.
Following script is a possible workaounrd. It extracts the ACME cert every five minute, calculates the TLSA hash and compares it with the upstream record. If it doesn't match, it uses [https://github.com/Stenstromen/gotlsaflare gotlsaflare] to update the TLSA record on Cloudflare.
<syntaxhighlight lang="nixos">
systemd.services.tlsa-cloudflare-update = {
  description = "Check and update TLSA/DANE record for mx1 from Stalwart ACME Cert";
 
  after = [
    "network-online.target"
    "stalwart-mail.service"
  ];
  wants = [
    "network-online.target"
    "stalwart-mail.service"
  ];
 
  serviceConfig = {
    Type = "oneshot";
    User = "stalwart-mail";
    Group = "stalwart-mail";
    EnvironmentFile = config.age.secrets.gotlsaflare-cloudflare-token.path;
    RuntimeDirectory = "stalwart-tlsa";
  };
  environment = {
    DOMAIN = "example.org";
    SUBDOMAIN = "mail";
    PORT = "25";
    ACME_PROVIDER_ID = "cloudflare";
  };
  path = with pkgs; [
    bash
    coreutils
    openssl
    dnsutils
    gotlsaflare
    rocksdb.tools
    gawk
  ];
  script = ''
    set -eu
    TLSA_RECORD="_$PORT._tcp.$SUBDOMAIN.$DOMAIN"
    DB_PATH="/var/lib/stalwart-mail/db"
    TEMP_RAW="/run/stalwart-tlsa/cert.bundle"
    TEMP_CRT="/run/stalwart-tlsa/cert.crt"
    echo "Starting TLSA update process for $DOMAIN"
    ldb --db="$DB_PATH" --column_family=s get "acme.$ACME_PROVIDER_ID.cert" | base64 -d > "$TEMP_RAW"
    if [ ! -s "$TEMP_RAW" ]; then
      echo "ERROR: ACME certificate extraction failed"
      exit 1
    fi
    openssl x509 -in "$TEMP_RAW" -out "$TEMP_CRT"
    LOCAL_HASH=$(openssl x509 -in "$TEMP_CRT" -pubkey -noout | openssl pkey -pubin -outform DER | openssl sha256 | awk '{print tolower($2)}')
    echo "Local hash: $LOCAL_HASH"
    UPSTREAM_HASH=$(dig +nosplit +short TLSA "$TLSA_RECORD" | awk '{print tolower($4)}' | head -n1)
    echo "Upstream hash: $UPSTREAM_HASH"
    if [ "$LOCAL_HASH" = "$UPSTREAM_HASH" ]; then
      echo "Hashes match. DNS is up to date."
      exit 0
    fi
    echo "Hashes differ! Updating Cloudflare..."
    gotlsaflare update \
      --url "$DOMAIN" \
      --subdomain "$SUBDOMAIN" \
      --tcp"$PORT" \
      --cert "$TEMP_CRT"
    echo "TLSA update completed successfully."
  '';
};
systemd.timers.tlsa-cloudflare-update = {
  description = "Run TLSA check and update every 5 minutes";
  wantedBy = [ "timers.target" ];
  timerConfig = {
    OnBootSec = "2m";
    OnUnitActiveSec = "5m";
    Unit = "tlsa-cloudflare-update.service";
  };
};
</syntaxhighlight>
Adapt the variables <code>DOMAIN</code>, <code>SUBDOMAIN</code>, and <code>PORT</code> according to your needs. The variable <code>ACME_PROVIDER_ID</code> corresponds to the ACME profile name you've setup in the Stalwart webadmin interface. <code>EnvironmentFile</code> points to a file containing the secret Cloudflare api token in the format: TOKEN=12345678[...].
=== Sending from subaddresses ===
Receiving mails to subaddresses like <code>john+secondary@example.org</code> is enabled by default. Sending from subaddresses will fail with "You are not allowed to send from this address" as long as they are not an configured alias address. You can disable this check but it will allow any authenticated user to send from any other address.
{{file|/etc/nixos/configuration.nix|nix|3=services.stalwart-mail = {
  settings = {
    [...]
    session.auth.must-match-sender = false;
  };
};}}
A configuration option to customize the pattern of authorized sender addresses is a [https://github.com/stalwartlabs/stalwart/issues/394#issuecomment-3705990056 planned feature].
=== Test mail server ===
=== Test mail server ===
You can use several online tools to test your mail server configuration:
You can use several online tools to test your mail server configuration:
Line 135: Line 366:
The following minimal configuration example is unsecure and for testing purpose only. It will run the Stalwart mail server on <code>localhost</code>, listening on port <code>143</code> (IMAP) and <code>587</code> (Submission). Users <code>alice</code> and <code>bob</code> are configured with the password <code>foobar</code>.{{file|/etc/nixos/configuration.nix|nix|3=services.stalwart-mail = {
The following minimal configuration example is unsecure and for testing purpose only. It will run the Stalwart mail server on <code>localhost</code>, listening on port <code>143</code> (IMAP) and <code>587</code> (Submission). Users <code>alice</code> and <code>bob</code> are configured with the password <code>foobar</code>.{{file|/etc/nixos/configuration.nix|nix|3=services.stalwart-mail = {
   enable = true;
   enable = true;
  # Use newer, latest version in NixOS 24.05
  package = pkgs.stalwart-mail;
   settings = {
   settings = {
     server = {
     server = {