Merge branch 'srs' into 'master'

Add support for sender rewriting for forwards using postsrsd

See merge request simple-nixos-mailserver/nixos-mailserver!431
This commit is contained in:
Martin Weinelt
2025-11-16 14:00:07 +00:00
9 changed files with 163 additions and 5 deletions

View File

@@ -57,6 +57,8 @@ SNM branch corresponding to your NixOS version.
* User Aliases * User Aliases
* [x] Regular aliases * [x] Regular aliases
* [x] Catch all aliases * [x] Catch all aliases
* Improve the Forwarding Experience
* [x] [Sender Rewriting Scheme](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme)
### In the future ### In the future
@@ -69,7 +71,6 @@ SNM branch corresponding to your NixOS version.
* [ ] Allow passing DKIM signing keys * [ ] Allow passing DKIM signing keys
* Improve the Forwarding Experience * Improve the Forwarding Experience
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) * [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html)
* [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd)
* User management * User management
* [ ] Allow local and LDAP user to coexist * [ ] Allow local and LDAP user to coexist
* OpenID Connect * OpenID Connect

View File

@@ -1101,6 +1101,28 @@ in
''; '';
}; };
srs = {
enable = mkEnableOption "Sender Rewrite Scheme";
domain = mkOption {
type = with types; nullOr str;
default = config.mailserver.systemDomain;
defaultText = literalExpression "config.mailserver.systemDomain";
example = "srs.example.com";
description = ''
Mail domain used for ephemeral SRS envelope addresses.
:::{note}
This domain can only support relaxed SPF alignment.
:::
:::{important}
For privacy reasons you should use a dedicated domain when serving multiple unrelated domains.
:::
'';
};
};
redis = { redis = {
configureLocally = mkOption { configureLocally = mkOption {
type = types.bool; type = types.bool;

View File

@@ -23,16 +23,22 @@ Welcome to NixOS Mailserver's documentation!
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
:caption: Features
fts
ldap
srs
.. toctree::
:maxdepth: 0
:caption: How-to :caption: How-to
backup-guide backup-guide
add-radicale add-radicale
add-roundcube add-roundcube
rspamd-tuning rspamd-tuning
fts
flakes flakes
autodiscovery autodiscovery
ldap
Indices and tables Indices and tables
================== ==================

View File

@@ -7,6 +7,8 @@ NixOS 25.11
- The ``systemName`` and ``systemDomain`` options have been introduced to have - The ``systemName`` and ``systemDomain`` options have been introduced to have
reusable configurations for automated reports (DMARC, TLSRPT). They come with reusable configurations for automated reports (DMARC, TLSRPT). They come with
reasonable defaults, but it is suggested to check and change them as needed. reasonable defaults, but it is suggested to check and change them as needed.
- Support for the `Sender Rewriting Scheme`_ has been added, which allows
forwarding mail without breaking SPF by rewriting the envelope address.
- The default key length for new DKIM RSA keys was increased to 2048 bits as - The default key length for new DKIM RSA keys was increased to 2048 bits as
recommended in `RFC 8301 3.2`_. recommended in `RFC 8301 3.2`_.
We recommend rotating existing keys, as the RFC advises that signatures from We recommend rotating existing keys, as the RFC advises that signatures from
@@ -29,6 +31,7 @@ NixOS 25.11
`tlsrpt-reporter`_. They can be enabled with the ``mailserver.tlsrpt.enable`` `tlsrpt-reporter`_. They can be enabled with the ``mailserver.tlsrpt.enable``
option. option.
.. _Sender Rewriting Scheme: srs.html
.. _RFC 8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2 .. _RFC 8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2
.. _RFC 8314 3.3: https://www.rfc-editor.org/rfc/rfc8314#section-3.3 .. _RFC 8314 3.3: https://www.rfc-editor.org/rfc/rfc8314#section-3.3
.. _RFC 8314 4.1: https://www.rfc-editor.org/rfc/rfc8314#section-4.1 .. _RFC 8314 4.1: https://www.rfc-editor.org/rfc/rfc8314#section-4.1

102
docs/srs.rst Normal file
View File

@@ -0,0 +1,102 @@
Sender Rewriting Scheme
=======================
The Sender Rewriting Scheme (SRS) allows mail servers to forward emails without
breaking SPF checks. By rewriting the envelope sender to an address within the
forwarders domain, SRS ensures that forwarded messages pass SPF validation,
preventing them from being rejected as spoofed or unauthorized.
How SRS works in practice
~~~~~~~~~~~~~~~~~~~~~~~~~
1. ``alice@foo.example`` receives an E-Mail from ``bob@bar.example``. Both the
envelope sender as well as the ``From`` header show ``bob@bar.example``. This
results in strict SPF alignment, because ``bar.example`` is the domain used in
both the ``Return-Path`` and ``FROM`` headers.
2. ``alice@foo.example`` forwards the mail to ``charlie@moo.example`` and
uses SRS to rewrite the envelope sender to originate from the local SRS domain
(e.g. `SRS0=HHH=TT=bar.example=alice@foo.example`). The ``FROM`` header remains
unchanged. This ensures that the forwarded mail succeeds SPF checks.
3. The email reaches ``charlie@moo.example``. SPF passes because the sender
domain in the envelope has been rewritten. The mismatch between envelope sender
domain and ``FROM`` domain does however break strict SPF alignment.
Enabling SRS
~~~~~~~~~~~~
In a simple setup just enabling SRS will use your ``mailserver.systemDomain``
when rewriting the envelope sender domain.
.. code:: nix
{
mailserver = {
srs = {
enable = true;
#domain = "srs.example.com";
};
};
};
..
While you can reuse an existing email domain for SRS, it is recommended to
configure a dedicated SRS domain. This is particularly important under the
following conditions:
* Multiple unrelated mail domains are hosted on the mailserver
* The mail domain requires strict SPF alignment in its DMARC policy
Required DNS changes
~~~~~~~~~~~~~~~~~~~~
.. note::
In the following example we assume that you want to set up a dedicated SRS
domain. If that is not the case you already have SPF and DKIM set up for the
system domain. If you have a DMARC record on the system domain, make sure it
uses a relaxed SPF alignment policy (``aspf=r``).
First we set up an MX record. This is so that we can receive and route bounces
that can result from forwards.
======================== ===== ==== ======== =====================
Name (Subdomain) TTL Type Priority Value
======================== ===== ==== ======== =====================
srs.example.com 10800 MX 10 ``mail.example.com``
======================== ===== ==== ==============================
Next up is the SPF record on the SRS domain to allow SPF authentication.
======================== ===== ==== ===================
Name (Subdomain) TTL Type Value
======================== ===== ==== ===================
srs.example.com 10800 TXT ``v=spf1 mx -all``
======================== ===== ==== ===================
Then we deploy the DKIM record with the `p=<value>` taken from
``/var/dkim/srs.example.com.mail.txt``, that appears after deploying with SRS
enabled.
=============================== ===== ==== ========================================
Name (Subdomain) TTL Type Value
=============================== ===== ==== ========================================
mail._domainkey.srs.example.com 10800 TXT ``v=DKIM1; k=rsa; p=<really-long-key>``
=============================== ===== ==== ========================================
Finally we can tie this together in the DMARC record to require receivers to
verify the requested SPF/DKIM alignment.
.. note::
The SRS domain can only support relaxed SPF alignment due to the envelope
sender and ``FROM`` header mismatch.
======================== ===== ==== =========================================
Name (Subdomain) TTL Type Value
======================== ===== ==== =========================================
_dmarc.srs.example.com 10800 TXT ``v=DMARC1; p=reject; aspf=r; adkim=s;``
======================== ===== ==== =========================================
We can safely configure a ``reject`` policy on the SRS domain, to enforce the
SPF and DKIM alignment as configured above.

View File

@@ -84,6 +84,7 @@
_module.check = false; _module.check = false;
mailserver = { mailserver = {
fqdn = "mx.example.com"; fqdn = "mx.example.com";
systemDomain = "example.com";
domains = [ domains = [
"example.com" "example.com"
]; ];

View File

@@ -261,6 +261,24 @@ in
configurePostfix = true; configurePostfix = true;
}; };
# Sender Rewriting Scheme (https://www.libsrs2.net/srs/srs.pdf)
services.postsrsd = {
inherit (cfg.srs) enable;
configurePostfix = true;
settings = {
domains = lib.unique (
[
cfg.fqdn
cfg.sendingFqdn
cfg.systemDomain
]
++ cfg.domains
);
separator = "=";
srs-domain = cfg.srs.domain;
};
};
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = '' preStart = ''
${appendPwdInVirtualMailboxMap} ${appendPwdInVirtualMailboxMap}

View File

@@ -50,6 +50,8 @@ let
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}" echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
fi fi
''; '';
dkimDomains = lib.unique (cfg.domains ++ (lib.optionals cfg.srs.enable [ cfg.srs.domain ]));
in in
{ {
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@@ -202,7 +204,7 @@ in
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
} }
(lib.optionalAttrs cfg.dkimSigning { (lib.optionalAttrs cfg.dkimSigning {
ExecStartPre = map createDkimKeypair cfg.domains; ExecStartPre = map createDkimKeypair dkimDomains;
ReadWritePaths = [ cfg.dkimKeyDirectory ]; ReadWritePaths = [ cfg.dkimKeyDirectory ];
}) })
]; ];

View File

@@ -27,6 +27,7 @@ groups = [
"mailserver.loginAccounts", "mailserver.loginAccounts",
"mailserver.certificate", "mailserver.certificate",
"mailserver.dkim", "mailserver.dkim",
"mailserver.srs",
"mailserver.dmarcReporting", "mailserver.dmarcReporting",
"mailserver.fullTextSearch", "mailserver.fullTextSearch",
"mailserver.redis", "mailserver.redis",
@@ -90,7 +91,9 @@ def print_option(option):
key=option["name"], key=option["name"],
description=description or "", description=description or "",
type=f"- type: {md_literal(option['type'])}", type=f"- type: {md_literal(option['type'])}",
default=render_option_value(option, "default"), default=render_option_value(option, "defaultText")
if "defaultText" in option
else render_option_value(option, "default"),
example=render_option_value(option, "example"), example=render_option_value(option, "example"),
) )
) )