Add support for DKIM key management
After bumping the generation of new DKIM keys to RSA 2048 in NixOS 25.11 key rotation for existing users could not be done safely. To resolve this situation we now support multiple generations of selectors per domain to enable proper DKIM key transitions as described in RFC6376 3.1. The added documentation introduces and motivates DKIM and guides the user through a DKIM key rotation. Additionally, DKIM key material can now also be treated as a managed secrets when autogenerated state on the mail server host is undesirable. This change is fully backwards compatible in behavior and will continue to use the previously generated DKIM key without any additional configuration up until the point when DKIM selectors are configured explicitly.
This commit is contained in:
@@ -46,6 +46,8 @@ SNM branch corresponding to your NixOS version.
|
|||||||
* [x] Via ClamAV
|
* [x] Via ClamAV
|
||||||
* DKIM Signing
|
* DKIM Signing
|
||||||
* [x] Via Rspamd
|
* [x] Via Rspamd
|
||||||
|
* [x] Automatic key generation
|
||||||
|
* [x] Multiple selectors per Domain
|
||||||
* User Management
|
* User Management
|
||||||
* [x] Declarative user management
|
* [x] Declarative user management
|
||||||
* [x] Declarative password management
|
* [x] Declarative password management
|
||||||
@@ -66,9 +68,6 @@ SNM branch corresponding to your NixOS version.
|
|||||||
* [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)
|
* [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)
|
||||||
* [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019)
|
* [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019)
|
||||||
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
|
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
|
||||||
* DKIM Signing
|
|
||||||
* [ ] Allow per domain selectors
|
|
||||||
* [ ] 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)
|
||||||
* User management
|
* User management
|
||||||
|
|||||||
+164
-50
@@ -917,63 +917,170 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dkimSigning = mkOption {
|
dkim = {
|
||||||
type = types.bool;
|
enable = mkEnableOption "DKIM signing" // {
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
};
|
||||||
Whether to activate dkim signing.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
dkimSelector = mkOption {
|
keyDirectory = mkOption {
|
||||||
type = types.str;
|
type = types.path;
|
||||||
default = "mail";
|
default = "/var/dkim";
|
||||||
description = ''
|
description = ''
|
||||||
The DKIM selector.
|
The path where DKIM siging keys are stored.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dkimKeyDirectory = mkOption {
|
defaults = {
|
||||||
type = types.path;
|
selector = mkOption {
|
||||||
default = "/var/dkim";
|
type = types.str;
|
||||||
description = ''
|
default = "mail";
|
||||||
The DKIM directory.
|
description = ''
|
||||||
'';
|
The default selector used to reference and lookup DKIM keys.
|
||||||
};
|
|
||||||
|
|
||||||
dkimKeyType = mkOption {
|
This value should most likely not be changed. Instead manage
|
||||||
type = types.enum [
|
{option}`mailserver.dkim.domains.<name>.selectors` to sign with one
|
||||||
"rsa"
|
or multiple DKIM key pairs and manage migrations.
|
||||||
"ed25519"
|
'';
|
||||||
];
|
};
|
||||||
default = "rsa";
|
|
||||||
description = ''
|
|
||||||
The key type used for generating DKIM keys. Ed25519 support was
|
|
||||||
introduced in RFC6376 (2018).
|
|
||||||
|
|
||||||
:::{warning}
|
keyType = mkOption {
|
||||||
Ed25519 DKIM keys are currently not recommended for primary use, as
|
type = types.enum [
|
||||||
various DKIM validators out there lack support and consider the keypair invalid.
|
"rsa"
|
||||||
:::
|
"ed25519"
|
||||||
|
];
|
||||||
|
default = "rsa";
|
||||||
|
description = ''
|
||||||
|
The key type used for generating DKIM keys. Ed25519 support was
|
||||||
|
introduced in RFC6376 (2018).
|
||||||
|
|
||||||
If you have already deployed a key with a different type than specified
|
:::{warning}
|
||||||
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
Ed25519 DKIM keys are currently not recommended for sole use, as
|
||||||
this package to generate a key with the new type, you will either have to
|
various DKIM validators out there lack support and consider the
|
||||||
change the selector or delete the old key file.
|
keypair invalid.
|
||||||
'';
|
:::
|
||||||
};
|
|
||||||
|
|
||||||
dkimKeyBits = mkOption {
|
This value should most likely not be changed. Once DKIM keys for
|
||||||
type = types.int;
|
domain and selector are generated changing this value will not
|
||||||
default = 2048;
|
regenerate the keypair. Instead create a new selector and configure
|
||||||
description = ''
|
{option}`mailserver.dkim.domains.<name>.selectors.<name>.keyType`.
|
||||||
How many bits in generated DKIM keys. RFC8301 suggests a minimum RSA key length of 2048 bit.
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
If you have already deployed a key with a different number of bits than specified
|
keyLength = mkOption {
|
||||||
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
type = types.int;
|
||||||
this package to generate a key with the new number of bits, you will either have to
|
default = 2048;
|
||||||
change the selector or delete the old key file.
|
description = ''
|
||||||
'';
|
The default key length used for generating new DKIM keys.
|
||||||
|
|
||||||
|
Only applies for RSA keys, Ed25519 keys use a fixed key length.
|
||||||
|
|
||||||
|
Per [RFC8301 3.2] the minimum RSA key length should be at least
|
||||||
|
2048 bit.
|
||||||
|
|
||||||
|
This value should most likely not be changed. Once DKIM keys for
|
||||||
|
domain and selector are generated changing this value will not
|
||||||
|
regenerate the keypair. Instead create a new selector and configure
|
||||||
|
{option}`mailserver.dkim.domains.<name>.selectors.<name>.keyLength`.
|
||||||
|
|
||||||
|
[RFC8301 3.2]: https://datatracker.ietf.org/doc/html/rfc8301#section-3.2
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
domains = mkOption {
|
||||||
|
description = "DKIM configuration per domain.";
|
||||||
|
type = types.attrsOf (
|
||||||
|
types.submodule ({
|
||||||
|
options = {
|
||||||
|
selectors = mkOption {
|
||||||
|
description = ''
|
||||||
|
DKIM selectors used for signing outgoing mail for this domain.
|
||||||
|
|
||||||
|
When no selector is configured a default selector will be
|
||||||
|
created with settings inherited from {option}`mailserver.defaults.dkim <mailserver.dkim.defaults.keyLength>`.
|
||||||
|
'';
|
||||||
|
type = types.attrsOf (
|
||||||
|
types.submodule ({
|
||||||
|
options = {
|
||||||
|
keyType = mkOption {
|
||||||
|
type =
|
||||||
|
with types;
|
||||||
|
nullOr (enum [
|
||||||
|
"rsa"
|
||||||
|
"ed25519"
|
||||||
|
]);
|
||||||
|
default = null;
|
||||||
|
example = "rsa";
|
||||||
|
description = ''
|
||||||
|
The key type used for generating this DKIM keypair.
|
||||||
|
|
||||||
|
:::{warning}
|
||||||
|
Ed25519 DKIM keys are currently not recommended for sole use, as
|
||||||
|
various DKIM validators out there lack support and consider the
|
||||||
|
keypair invalid.
|
||||||
|
:::
|
||||||
|
|
||||||
|
This option is mutually exclusive with `keyFile`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
keyLength = mkOption {
|
||||||
|
type = with types; nullOr int;
|
||||||
|
default = null;
|
||||||
|
example = 2048;
|
||||||
|
description = ''
|
||||||
|
The key length used for generating this DKIM key.
|
||||||
|
|
||||||
|
Only applies for RSA keys, Ed25519 keys use a fixed key size.
|
||||||
|
|
||||||
|
This option is mutually exclusive with `keyFile`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
keyFile = mkOption {
|
||||||
|
type =
|
||||||
|
with types;
|
||||||
|
nullOr (pathWith {
|
||||||
|
inStore = false;
|
||||||
|
});
|
||||||
|
default = null;
|
||||||
|
example = "/run/keys/example.com-dkim-rsa-2026-03.key";
|
||||||
|
description = ''
|
||||||
|
Path to an existing DKIM private key file.
|
||||||
|
|
||||||
|
DKIM keys can be generated using `rspamadm dkim_keygen`.
|
||||||
|
|
||||||
|
This option is mutually exclusive with `keyType` and `keyLength`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
"mail" = {
|
||||||
|
# inherit defaults from mailserver.dkim.defaults
|
||||||
|
};
|
||||||
|
"rsa-2026-03".keyFile = "/run/keys/example.com-dkim-rsa-2026-03.key";
|
||||||
|
};
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
"example.com".selectors = {
|
||||||
|
"mail" = {
|
||||||
|
# inherit defaults from mailserver.dkim.defaults
|
||||||
|
};
|
||||||
|
"rsa-2026-03".keyFile = "/run/keys/example.com-dkim-rsa-2026-03.key";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
dmarcReporting = {
|
dmarcReporting = {
|
||||||
@@ -1468,6 +1575,13 @@ in
|
|||||||
(mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] ''
|
(mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] ''
|
||||||
The workaround for the SMTP Smuggling attack is default enabled in Postfix >3.9. Use `services.postfix.config.smtpd_forbid_bare_newline` if you need to deviate from its default.
|
The workaround for the SMTP Smuggling attack is default enabled in Postfix >3.9. Use `services.postfix.config.smtpd_forbid_bare_newline` if you need to deviate from its default.
|
||||||
'')
|
'')
|
||||||
|
(mkRenamedOptionModule [ "mailserver" "dkimSigning" ] [ "mailserver" "dkim" "enable" ])
|
||||||
|
(mkRenamedOptionModule [ "mailserver" "dkimKeyDirectory" ] [ "mailserver" "dkim" "keyDirectory" ])
|
||||||
|
(mkRenamedOptionModule
|
||||||
|
[ "mailserver" "dkimSelector" ]
|
||||||
|
[ "mailserver" "dkim" "defaults" "selector" ]
|
||||||
|
)
|
||||||
|
(mkRenamedOptionModule [ "mailserver" "dkimKeyType" ] [ "mailserver" "dkim" "defaults" "keyType" ])
|
||||||
(mkRenamedOptionModule [ "mailserver" "dmarcReporting" "domain" ] [ "mailserver" "systemDomain" ])
|
(mkRenamedOptionModule [ "mailserver" "dmarcReporting" "domain" ] [ "mailserver" "systemDomain" ])
|
||||||
(mkRenamedOptionModule
|
(mkRenamedOptionModule
|
||||||
[ "mailserver" "dmarcReporting" "organizationName" ]
|
[ "mailserver" "dmarcReporting" "organizationName" ]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Maildir.
|
|||||||
|
|
||||||
To backup spam and ham training data, backup ``/var/lib/redis-rspamd``.
|
To backup spam and ham training data, backup ``/var/lib/redis-rspamd``.
|
||||||
|
|
||||||
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever you
|
||||||
you specified as ``dkimKeyDirectory``). If you should lose those don’t
|
specified as :option:`mailserver.dkim.keyDirectory`). If you should lose those
|
||||||
worry, new ones will be created on the fly. But you will need to repeat
|
don’t worry, new ones will be created on the fly. But you will need to update
|
||||||
step ``B)5`` and correct all the ``dkim`` keys.
|
the DKIM TXT records to reflect the new key material.
|
||||||
|
|||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
DKIM Signing
|
||||||
|
============
|
||||||
|
|
||||||
|
DKIM (DomainKeys Identified Mail) is an email authentication mechanism that
|
||||||
|
allows a mailserver to digitally sign outgoing emails for a domain. Receiving
|
||||||
|
mail servers can verify this signature using a public key published in DNS to
|
||||||
|
confirm the message was authorized by the domain and was not modified during
|
||||||
|
transit.
|
||||||
|
|
||||||
|
How DKIM works in practice
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
1. ``bob@bar.example`` sends an email to ``alice@foo.example``. The sending
|
||||||
|
mail server for ``bar.example`` selects one or multiple DKIM keys using a
|
||||||
|
**selector** (e.g., ``mail``) and creates one or multiple cryptographic
|
||||||
|
signature for selected headers and the message body, adding a ``DKIM-Signature``
|
||||||
|
header that references the selector.
|
||||||
|
|
||||||
|
2. The message arrives at ``foo.example``. The receiving mail server reads the
|
||||||
|
``DKIM-Signature`` headers, looks up the public keys for ``bar.example`` for
|
||||||
|
each of the specified selectors (e.g., ``mail._domainkey.bar.example``), and
|
||||||
|
verifies that at least one signature matches the message content.
|
||||||
|
|
||||||
|
3. With a valid signature, the receiver knows the message was authorized by
|
||||||
|
``bar.example`` and that the signed headers and body were not modified in
|
||||||
|
transit. If the content or signed headers were changed, the DKIM verification
|
||||||
|
fails. The use of selectors allows ``bar.example`` to rotate or migrate keys
|
||||||
|
without disrupting verification for previously sent messages.
|
||||||
|
|
||||||
|
Enabling DKIM Signing
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Because DKIM signing is crucial for reliable mail delivery it is enabled by
|
||||||
|
default and without further configuration a DKIM keypair will be generated for
|
||||||
|
each :option:`mailserver.domains` (including :option:`mailserver.srs.domain`,
|
||||||
|
if set) based on :option:`mailserver.dkim.defaults
|
||||||
|
<mailserver.dkim.defaults.keyLength>`.
|
||||||
|
|
||||||
|
.. code:: nix
|
||||||
|
|
||||||
|
{
|
||||||
|
mailserver = {
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
dkim.enable = true; # enabled by default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If you set up NixOS Mailserver before the `25.11 release`_ your DKIM keys were
|
||||||
|
generated with 1024 bit RSA and we recommend replacing them with 2048 bit
|
||||||
|
RSA key material per `RFC8301 3.2`_.
|
||||||
|
|
||||||
|
.. _25.11 release: release-notes.html#nixos-25-11
|
||||||
|
.. _RFC8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2
|
||||||
|
|
||||||
|
DKIM Key Rotation
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
DKIM key rotation replaces a domain's signing keys to maintain
|
||||||
|
strong email authentication and support algorithm upgrades. Rotation is
|
||||||
|
essential for migrating away from weaker or deprecated algorithms.
|
||||||
|
|
||||||
|
Selectors allow multiple keys to coexist during the transition: a new key can
|
||||||
|
be deployed under a different selector while the old key remains valid for a
|
||||||
|
limited period to verify messages still in transit. Once all messages signed
|
||||||
|
with the old key have been delivered, the key can be safely retired, ensuring a
|
||||||
|
reliable migration without breaking verification.
|
||||||
|
|
||||||
|
|
||||||
|
1. Make the automatically generated key explicit
|
||||||
|
************************************************
|
||||||
|
|
||||||
|
First we need to make sure we keep the current DKIM key configured. If you were
|
||||||
|
relying on automatically generated keys before, you now need to start explicitly
|
||||||
|
defining that key, because explicit selector configuration takes precedence.
|
||||||
|
|
||||||
|
.. code:: nix
|
||||||
|
|
||||||
|
{
|
||||||
|
mailserver = {
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
dkim.domains = {
|
||||||
|
"example.com".selectors = {
|
||||||
|
"${config.mailserver.dkim.defaults.selector}" = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2. Create the new DKIM keypair
|
||||||
|
******************************
|
||||||
|
|
||||||
|
Next we need to create a new DKIM key with a unique selector, you can
|
||||||
|
for example choose the current date. Without any settings passed a new
|
||||||
|
key will be generated from the current :option:`mailserver.dkim.defaults
|
||||||
|
<mailserver.dkim.defaults.keyLength>`, which should be sufficient.
|
||||||
|
|
||||||
|
.. code:: nix
|
||||||
|
|
||||||
|
{
|
||||||
|
mailserver = {
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
dkim = {
|
||||||
|
enable = true;
|
||||||
|
domains."example.com".selectors = {
|
||||||
|
"${config.mailserver.dkim.defaults.selector}" = { };
|
||||||
|
"rsa-2026-03" = {
|
||||||
|
keyType = "rsa";
|
||||||
|
keyLength = 2048;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
While DKIM does support Ed25519 keys (`RFC8463`_), many validators still lack
|
||||||
|
proper support and may treat Ed25519 key material as invalid. As a result,
|
||||||
|
mail signed only with Ed25519 DKIM keys may fail verification at some
|
||||||
|
receivers.
|
||||||
|
|
||||||
|
.. _RFC8463: https://datatracker.ietf.org/doc/html/rfc8463
|
||||||
|
|
||||||
|
Once this configuration is applied the new keypair will be generated below
|
||||||
|
:option:`mailserver.dkim.keyDirectory`, which defaults to ``/var/dkim``. The
|
||||||
|
mailserver then begins signing outgoing mail with this key, so that it is now
|
||||||
|
signing with two DKIM keys simultaneously.
|
||||||
|
|
||||||
|
To allow receiving servers to verify the new DKIM signature its
|
||||||
|
public key needs to be published into DNS. Look up the public key from
|
||||||
|
``/var/dkim/example.com.rsa-2026-03.txt`` and create the following DNS record by
|
||||||
|
substituting selector and public key.
|
||||||
|
|
||||||
|
.. csv-table::
|
||||||
|
:header: "Name", "TTL", "Type", "Value"
|
||||||
|
:widths: 30, 10, 10, 50
|
||||||
|
|
||||||
|
rsa-2026-03._domainkey.example.com., 86400, TXT, v=DKIM1; k=rsa; p=<public key>
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If you created an Ed25519 key, make sure to set the correct key type: ``k=ed25519``
|
||||||
|
|
||||||
|
Now wait for a few minutes and then check DNS propagation to show the value specified.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ nix-shell -p dig --command "dig @ns1.example.org TXT rsa-2026-03._domainkey.example.com"
|
||||||
|
|
||||||
|
You can use https://www.mail-tester.com to test the new DKIM signature passes
|
||||||
|
validation. They allow you to view the message source where you can check that
|
||||||
|
the correct number of ``DKIM-Signature`` keys are present in the mail header.
|
||||||
|
|
||||||
|
|
||||||
|
3. Stop signing with the old DKIM keypair
|
||||||
|
*****************************************
|
||||||
|
|
||||||
|
Once validation passes we need to stop signing with the old DKIM keypair, so
|
||||||
|
mail in transit eventually stops depending on the key material we want to rotate
|
||||||
|
out. Removing the selector will not remove the key material from disk, but it
|
||||||
|
will stop using it to sign outgoing mail.
|
||||||
|
|
||||||
|
.. code:: nix
|
||||||
|
|
||||||
|
{
|
||||||
|
mailserver = {
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
dkim = {
|
||||||
|
enable = true;
|
||||||
|
domains."example.com".selectors = {
|
||||||
|
"rsa-2026-03" = {
|
||||||
|
keyType = "rsa";
|
||||||
|
keyLength = 2048;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Apply the configuration.
|
||||||
|
|
||||||
|
|
||||||
|
4. Remove the old DKIM selector from DNS
|
||||||
|
****************************************
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
Do not remove the DNS records for the old selector immediately. Keeping them
|
||||||
|
in place is essential to ensure that messages still in transit can be verified
|
||||||
|
and delivered successfully.
|
||||||
|
|
||||||
|
Mail delivery is not always instantaneous. In the worst case, multiple retries
|
||||||
|
over several days may be required. According to `RFC5321 4.5.4.1`_ delivery
|
||||||
|
should be retried for at least 4-5 days.
|
||||||
|
|
||||||
|
This means messages signed only with the old DKIM key could still be in transit
|
||||||
|
and rely on the old selector to verify their signatures. To ensure reliable
|
||||||
|
delivery, we recommend waiting **at least five days** before removing the old
|
||||||
|
DKIM selector from DNS.
|
||||||
|
|
||||||
|
.. _RFC5321 4.5.4.1: https://www.rfc-editor.org/rfc/rfc5321#section-4.5.4.1
|
||||||
@@ -25,6 +25,7 @@ Welcome to NixOS Mailserver's documentation!
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
:caption: Features
|
:caption: Features
|
||||||
|
|
||||||
|
dkim
|
||||||
fts
|
fts
|
||||||
ldap
|
ldap
|
||||||
srs
|
srs
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ NixOS 26.05
|
|||||||
:option:`mailserver.x509.privateKeyFile` instead. Support for automatic
|
:option:`mailserver.x509.privateKeyFile` instead. Support for automatic
|
||||||
creation of self-signed certificates has been removed.
|
creation of self-signed certificates has been removed.
|
||||||
Check the updated `setup guide`_ for a basic ACME HTTP-01 example.
|
Check the updated `setup guide`_ for a basic ACME HTTP-01 example.
|
||||||
|
- `DKIM key management`_ is now available with multiple concurrent selectors per
|
||||||
|
domain enabling proper DKIM key rotation. While we still generate a default
|
||||||
|
key for backwards compatibility we now also support passing pre-created
|
||||||
|
key material. If your DKIM keys were automatically created before the 25.11
|
||||||
|
release they are 1024 bit RSA keys and should be rotated out.
|
||||||
|
See :option:`mailserver.dkim.domains` for further relevant options.
|
||||||
- Cleartext password files can now be configured for login accounts. This
|
- Cleartext password files can now be configured for login accounts. This
|
||||||
is an alternative to hashed passwords that integrates well with workflows
|
is an alternative to hashed passwords that integrates well with workflows
|
||||||
established by `agenix`_/`sops-nix`_ that instead rely on encryption. This
|
established by `agenix`_/`sops-nix`_ that instead rely on encryption. This
|
||||||
@@ -18,6 +24,7 @@ NixOS 26.05
|
|||||||
See :option:`mailserver.loginAccounts.<name>.passwordFile`.
|
See :option:`mailserver.loginAccounts.<name>.passwordFile`.
|
||||||
|
|
||||||
.. _setup guide: setup-guide.html#setup-the-server
|
.. _setup guide: setup-guide.html#setup-the-server
|
||||||
|
.. _DKIM key management: dkim.html
|
||||||
.. _agenix: https://github.com/ryantm/agenix
|
.. _agenix: https://github.com/ryantm/agenix
|
||||||
.. _sops-nix: https://github.com/Mic92/sops-nix
|
.. _sops-nix: https://github.com/Mic92/sops-nix
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,20 @@ in
|
|||||||
message = "Configure either an ACME certificate (`mailserver.x509.useACMEHost`) or pass an existing certificate (`mailserver.x509.certificateFile`, `mailserver.x509.privateKeyFile`).";
|
message = "Configure either an ACME certificate (`mailserver.x509.useACMEHost`) or pass an existing certificate (`mailserver.x509.certificateFile`, `mailserver.x509.privateKeyFile`).";
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
++ lib.optionals config.mailserver.dkim.enable (
|
||||||
|
lib.flatten (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
domain: domainAttrs:
|
||||||
|
lib.mapAttrsToList (selector: selectorAttrs: [
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
selectorAttrs.keyFile != null -> (selectorAttrs.keyType == null && selectorAttrs.keyLength == null);
|
||||||
|
message = "${domain} DKIM selector ${selector} can only use either `keyType`, `keyLength` OR `keyFile` not both.";
|
||||||
|
}
|
||||||
|
]) domainAttrs.selectors
|
||||||
|
) config.mailserver.dkim.domains
|
||||||
|
)
|
||||||
|
)
|
||||||
++ lib.optionals config.mailserver.ldap.enable [
|
++ lib.optionals config.mailserver.ldap.enable [
|
||||||
{
|
{
|
||||||
assertion = config.mailserver.loginAccounts == { };
|
assertion = config.mailserver.loginAccounts == { };
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ in
|
|||||||
smtpd_tls_loglevel = "1";
|
smtpd_tls_loglevel = "1";
|
||||||
|
|
||||||
smtpd_milters = smtpdMilters;
|
smtpd_milters = smtpdMilters;
|
||||||
non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
non_smtpd_milters = lib.mkIf cfg.dkim.enable [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||||
milter_protocol = "6";
|
milter_protocol = "6";
|
||||||
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
|
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
|
||||||
};
|
};
|
||||||
|
|||||||
+93
-22
@@ -31,27 +31,80 @@ let
|
|||||||
rspamdGroup = config.services.rspamd.group;
|
rspamdGroup = config.services.rspamd.group;
|
||||||
|
|
||||||
createDkimKeypair =
|
createDkimKeypair =
|
||||||
domain:
|
{
|
||||||
|
domain,
|
||||||
|
selector,
|
||||||
|
type,
|
||||||
|
bits,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
|
privkey = "${cfg.dkim.keyDirectory}/${domain}.${selector}.key";
|
||||||
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
|
pubkey = "${cfg.dkim.keyDirectory}/${domain}.${selector}.txt";
|
||||||
in
|
in
|
||||||
pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
pkgs.writeShellScript "dkim-keygen-${domain}-${selector}" ''
|
||||||
if [ ! -f "${privateKey}" ]
|
if [ ! -f "${privkey}" ]
|
||||||
then
|
then
|
||||||
export PATH=${lib.makeBinPath [ pkgs.openssl ]}
|
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen ${
|
||||||
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
|
lib.cli.toCommandLineShellGNU { } {
|
||||||
--domain "${domain}" \
|
inherit
|
||||||
--selector "${cfg.dkimSelector}" \
|
domain
|
||||||
--type "${cfg.dkimKeyType}" \
|
selector
|
||||||
--bits ${toString cfg.dkimKeyBits} \
|
type
|
||||||
--privkey "${privateKey}" > "${publicKey}"
|
bits
|
||||||
chmod 0644 "${publicKey}"
|
privkey
|
||||||
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
|
;
|
||||||
|
}
|
||||||
|
} > "${pubkey}"
|
||||||
|
chmod 0644 "${pubkey}"
|
||||||
|
echo "Generated key for domain ${domain} and selector ${selector}"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
dkimDomains = lib.unique (cfg.domains ++ (lib.optionals cfg.srs.enable [ cfg.srs.domain ]));
|
mailDomains = lib.unique (
|
||||||
|
# primary mailserver domains
|
||||||
|
config.mailserver.domains
|
||||||
|
# all dkim domains, even extra domains specified
|
||||||
|
++ lib.attrNames cfg.dkim.domains
|
||||||
|
# and the srs domain, if one is configured
|
||||||
|
++ lib.optionals (cfg.srs.domain != null) [ cfg.srs.domain ]
|
||||||
|
);
|
||||||
|
|
||||||
|
dkimKeys = lib.concatMap (
|
||||||
|
domain:
|
||||||
|
let
|
||||||
|
configuredSelectors = config.mailserver.dkim.domains.${domain}.selectors or { };
|
||||||
|
|
||||||
|
finalSelectors =
|
||||||
|
if configuredSelectors == { } then
|
||||||
|
# synthesize default dkim key, if none configured
|
||||||
|
{
|
||||||
|
"${config.mailserver.dkim.defaults.selector}" = {
|
||||||
|
keyType = null;
|
||||||
|
keyLength = null;
|
||||||
|
keyFile = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
configuredSelectors;
|
||||||
|
in
|
||||||
|
lib.mapAttrsToList (selector: settings: rec {
|
||||||
|
inherit domain selector;
|
||||||
|
keyFile = settings.keyFile;
|
||||||
|
keyPath = if keyFile != null then keyFile else "${cfg.dkim.keyDirectory}/${domain}.${selector}.key";
|
||||||
|
bits =
|
||||||
|
if settings.keyLength != null then
|
||||||
|
settings.keyLength
|
||||||
|
else
|
||||||
|
config.mailserver.dkim.defaults.keyLength;
|
||||||
|
type =
|
||||||
|
if settings.keyType != null then settings.keyType else config.mailserver.dkim.defaults.keyType;
|
||||||
|
}) finalSelectors
|
||||||
|
) mailDomains;
|
||||||
|
|
||||||
|
dkimKeysToGenerate = lib.filter (key: key.keyFile == null) dkimKeys;
|
||||||
|
|
||||||
|
dkimKeysByDomain = lib.groupBy (item: item.domain) dkimKeys;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -110,13 +163,31 @@ in
|
|||||||
};
|
};
|
||||||
"dkim_signing.conf" = {
|
"dkim_signing.conf" = {
|
||||||
text = ''
|
text = ''
|
||||||
enabled = ${lib.boolToString cfg.dkimSigning};
|
enabled = ${lib.boolToString cfg.dkim.enable};
|
||||||
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
|
# Only sign explicitly configured domains
|
||||||
selector = "${cfg.dkimSelector}";
|
try_fallback = false;
|
||||||
# Allow for usernames w/o domain part
|
# Allow for usernames w/o domain part
|
||||||
allow_username_mismatch = true;
|
allow_username_mismatch = true;
|
||||||
# Don't normalize DKIM key selection for subdomains
|
# Don't normalize DKIM key selection for subdomains
|
||||||
use_esld = false;
|
use_esld = false;
|
||||||
|
|
||||||
|
domain {
|
||||||
|
${lib.concatStringsSep "\n\n" (
|
||||||
|
map (domain: ''
|
||||||
|
${domain} {
|
||||||
|
selectors [
|
||||||
|
${lib.concatStringsSep ",\n" (
|
||||||
|
map (selector: ''
|
||||||
|
{
|
||||||
|
path: "${selector.keyPath}";
|
||||||
|
selector: "${selector.selector}";
|
||||||
|
}'') dkimKeysByDomain.${domain}
|
||||||
|
)}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
'') (lib.attrNames dkimKeysByDomain)
|
||||||
|
)}
|
||||||
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
"dmarc.conf" = {
|
"dmarc.conf" = {
|
||||||
@@ -183,7 +254,7 @@ in
|
|||||||
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
|
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
|
||||||
|
|
||||||
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
||||||
"${cfg.dkimKeyDirectory}" = {
|
"${cfg.dkim.keyDirectory}" = {
|
||||||
d = {
|
d = {
|
||||||
# Create /var/dkim owned by rspamd user/group
|
# Create /var/dkim owned by rspamd user/group
|
||||||
user = rspamdUser;
|
user = rspamdUser;
|
||||||
@@ -204,9 +275,9 @@ in
|
|||||||
{
|
{
|
||||||
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
||||||
}
|
}
|
||||||
(lib.optionalAttrs cfg.dkimSigning {
|
(lib.optionalAttrs cfg.dkim.enable {
|
||||||
ExecStartPre = map createDkimKeypair dkimDomains;
|
ExecStartPre = map createDkimKeypair dkimKeysToGenerate;
|
||||||
ReadWritePaths = [ cfg.dkimKeyDirectory ];
|
ReadWritePaths = [ cfg.dkim.keyDirectory ];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ in
|
|||||||
after = [
|
after = [
|
||||||
"dovecot.service"
|
"dovecot.service"
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.dkimSigning "rspamd.service"
|
++ lib.optional cfg.dkim.enable "rspamd.service"
|
||||||
++ certificateDeps;
|
++ certificateDeps;
|
||||||
requires = [ "dovecot.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
|
requires = [ "dovecot.service" ] ++ lib.optional cfg.dkim.enable "rspamd.service";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-4
@@ -49,7 +49,21 @@
|
|||||||
"example2.com"
|
"example2.com"
|
||||||
];
|
];
|
||||||
rewriteMessageId = true;
|
rewriteMessageId = true;
|
||||||
dkimKeyBits = 1535;
|
dkim = {
|
||||||
|
defaults.keyLength = 1535;
|
||||||
|
domains."example2.com".selectors = {
|
||||||
|
"dkim-rsa" = {
|
||||||
|
# rsa 1535 bits via defaults
|
||||||
|
};
|
||||||
|
"dkim-ed25519" = {
|
||||||
|
keyType = "ed25519";
|
||||||
|
keyLength = null;
|
||||||
|
};
|
||||||
|
"dkim-file" = {
|
||||||
|
keyFile = "/run/rspamd/dkim-test.key";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
dmarcReporting.enable = true;
|
dmarcReporting.enable = true;
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
@@ -369,6 +383,9 @@
|
|||||||
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
server.succeed("rspamadm dkim_keygen > /run/rspamd/dkim-test.key")
|
||||||
|
server.succeed("chown rspamd: /run/rspamd/dkim-test.key")
|
||||||
|
|
||||||
client.execute("cp -p /etc/root/.* ~/")
|
client.execute("cp -p /etc/root/.* ~/")
|
||||||
client.succeed("mkdir -p ~/mail")
|
client.succeed("mkdir -p ~/mail")
|
||||||
client.succeed("ls -la ~/ >&2")
|
client.succeed("ls -la ~/ >&2")
|
||||||
@@ -404,10 +421,10 @@
|
|||||||
|
|
||||||
with subtest("dkim has user-specified size"):
|
with subtest("dkim has user-specified size"):
|
||||||
server.succeed(
|
server.succeed(
|
||||||
"openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'"
|
"openssl rsa -in /var/dkim/example2.com.dkim-rsa.key -text -noout | grep 'Private-Key: (1535 bit'"
|
||||||
)
|
)
|
||||||
|
|
||||||
with subtest("dkim singing, multiple domains"):
|
with subtest("dkim signing, multiple domains"):
|
||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from user2 to user1
|
# send email from user2 to user1
|
||||||
client.succeed(
|
client.succeed(
|
||||||
@@ -418,7 +435,9 @@
|
|||||||
client.succeed("fetchmail --nosslcertck -v")
|
client.succeed("fetchmail --nosslcertck -v")
|
||||||
client.succeed("cat ~/mail/* >&2")
|
client.succeed("cat ~/mail/* >&2")
|
||||||
# make sure it is dkim signed
|
# make sure it is dkim signed
|
||||||
client.succeed("grep DKIM-Signature: ~/mail/*")
|
client.succeed("grep 's=dkim-rsa' ~/mail/*")
|
||||||
|
client.succeed("grep 's=dkim-ed25519' ~/mail/*")
|
||||||
|
client.succeed("grep 's=dkim-file' ~/mail/*")
|
||||||
|
|
||||||
with subtest("aliases"):
|
with subtest("aliases"):
|
||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
|
|||||||
Reference in New Issue
Block a user