Merge branch 'dkim-key-management' into 'master'
Add support for DKIM key management Closes #341 See merge request simple-nixos-mailserver/nixos-mailserver!484
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
|
||||||
|
|||||||
+142
-28
@@ -917,31 +917,33 @@ 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;
|
|
||||||
default = "mail";
|
|
||||||
description = ''
|
|
||||||
The DKIM selector.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
dkimKeyDirectory = mkOption {
|
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/var/dkim";
|
default = "/var/dkim";
|
||||||
description = ''
|
description = ''
|
||||||
The DKIM directory.
|
The path where DKIM siging keys are stored.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dkimKeyType = mkOption {
|
defaults = {
|
||||||
|
selector = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail";
|
||||||
|
description = ''
|
||||||
|
The default selector used to reference and lookup DKIM keys.
|
||||||
|
|
||||||
|
This value should most likely not be changed. Instead manage
|
||||||
|
{option}`mailserver.dkim.domains.<name>.selectors` to sign with one
|
||||||
|
or multiple DKIM key pairs and manage migrations.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
keyType = mkOption {
|
||||||
type = types.enum [
|
type = types.enum [
|
||||||
"rsa"
|
"rsa"
|
||||||
"ed25519"
|
"ed25519"
|
||||||
@@ -952,29 +954,134 @@ in
|
|||||||
introduced in RFC6376 (2018).
|
introduced in RFC6376 (2018).
|
||||||
|
|
||||||
:::{warning}
|
:::{warning}
|
||||||
Ed25519 DKIM keys are currently not recommended for primary use, as
|
Ed25519 DKIM keys are currently not recommended for sole use, as
|
||||||
various DKIM validators out there lack support and consider the keypair invalid.
|
various DKIM validators out there lack support and consider the
|
||||||
|
keypair invalid.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
If you have already deployed a key with a different type than specified
|
This value should most likely not be changed. Once DKIM keys for
|
||||||
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
domain and selector are generated changing this value will not
|
||||||
this package to generate a key with the new type, you will either have to
|
regenerate the keypair. Instead create a new selector and configure
|
||||||
change the selector or delete the old key file.
|
{option}`mailserver.dkim.domains.<name>.selectors.<name>.keyType`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dkimKeyBits = mkOption {
|
keyLength = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 2048;
|
default = 2048;
|
||||||
description = ''
|
description = ''
|
||||||
How many bits in generated DKIM keys. RFC8301 suggests a minimum RSA key length of 2048 bit.
|
The default key length used for generating new DKIM keys.
|
||||||
|
|
||||||
If you have already deployed a key with a different number of bits than specified
|
Only applies for RSA keys, Ed25519 keys use a fixed key length.
|
||||||
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
|
||||||
this package to generate a key with the new number of bits, you will either have to
|
Per [RFC8301 3.2] the minimum RSA key length should be at least
|
||||||
change the selector or delete the old key file.
|
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 = {
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
@@ -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