diff --git a/default.nix b/default.nix
index 7c9118e..8584af2 100644
--- a/default.nix
+++ b/default.nix
@@ -627,6 +627,63 @@ in
'';
};
+ dmarcReporting = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to send out aggregated, daily DMARC reports in response to incoming
+ mail, when the sender domain defines a DMARC policy including the RUA tag.
+
+ This is helpful for the mail ecosystem, because it allows third parties to
+ get notified about SPF/DKIM violations originating from their sender domains.
+
+ See https://rspamd.com/doc/modules/dmarc.html#reporting
+ '';
+ };
+
+ localpart = mkOption {
+ type = types.str;
+ default = "dmarc-noreply";
+ example = "dmarc-report";
+ description = ''
+ The local part of the email address used for outgoing DMARC reports.
+ '';
+ };
+
+ domain = mkOption {
+ type = types.enum (cfg.domains);
+ example = "example.com";
+ description = ''
+ The domain from which outgoing DMARC reports are served.
+ '';
+ };
+
+ email = mkOption {
+ type = types.str;
+ default = with cfg.dmarcReporting; "${localpart}@${domain}";
+ example = "dmarc-noreply@example.com";
+ readOnly = true;
+ };
+
+ organizationName = mkOption {
+ type = types.str;
+ example = "ACME Corp.";
+ description = ''
+ The name of your organization used in the org_name attribute in
+ DMARC reports.
+ '';
+ };
+
+ fromName = mkOption {
+ type = types.str;
+ default = cfg.dmarcReporting.organizationName;
+ description = ''
+ The sender name for DMARC reports. Defaults to the organization name.
+ '';
+ };
+ };
+
debug = mkOption {
type = types.bool;
default = false;
diff --git a/docs/options.rst b/docs/options.rst
index 1e0af9a..0e3f7ab 100644
--- a/docs/options.rst
+++ b/docs/options.rst
@@ -19,6 +19,72 @@ to enable this unless you're hacking on nixos-mailserver.
- Default: ``False``
+mailserver.dmarcReporting.domain
+--------------------------------
+
+The domain from which outgoing DMARC reports are served.
+
+
+- Type: ``value "example.com" (singular enum)``
+
+
+
+mailserver.dmarcReporting.email
+-------------------------------
+
+None
+
+- Type: ``string``
+- Default: ``dmarc-noreply@example.com``
+
+
+mailserver.dmarcReporting.enable
+--------------------------------
+
+Whether to send out aggregated, daily DMARC reports in response to incoming
+mail, when the sender domain defines a DMARC policy including the RUA tag.
+
+This is helpful for the mail ecosystem, because it allows third parties to
+get notified about SPF/DKIM violations originating from their sender domains.
+
+See https://rspamd.com/doc/modules/dmarc.html#reporting
+
+
+- Type: ``boolean``
+- Default: ``False``
+
+
+mailserver.dmarcReporting.fromName
+----------------------------------
+
+The sender name for DMARC reports. Defaults to the organization name.
+
+
+- Type: ``string``
+- Default: ``Example Corp``
+
+
+mailserver.dmarcReporting.localpart
+-----------------------------------
+
+The local part of the email address used for outgoing DMARC reports.
+
+
+- Type: ``string``
+- Default: ``dmarc-noreply``
+
+
+mailserver.dmarcReporting.organizationName
+------------------------------------------
+
+The name of your organization used in the org_name attribute in
+DMARC reports.
+
+
+- Type: ``string``
+
+
+
mailserver.domains
------------------
@@ -489,7 +555,7 @@ For which domains should this account act as a catch all?
Note: Does not allow sending from all addresses of these domains.
-- Type: ``list of impossible (empty enum)s``
+- Type: ``list of value "example.com" (singular enum)s``
- Default: ``[]``
diff --git a/flake.nix b/flake.nix
index e5b85ad..3897e8c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -59,7 +59,16 @@
# don't care about this package but it is part of the
# NixOS module evaluation)
nixpkgs.config.allowBroken = true;
- mailserver.fqdn = "mx.example.com";
+ mailserver = {
+ fqdn = "mx.example.com";
+ domains = [
+ "example.com"
+ ];
+ dmarcReporting = {
+ organizationName = "Example Corp";
+ domain = "example.com";
+ };
+ };
}
];
diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix
index efe3abc..a506904 100644
--- a/mail-server/rspamd.nix
+++ b/mail-server/rspamd.nix
@@ -56,6 +56,17 @@ in
# Disable outbound email signing, we use opendkim for this
enabled = false;
''; };
+ "dmarc.conf" = { text = ''
+ ${lib.optionalString cfg.dmarcReporting.enable ''
+ reporting {
+ enabled = true;
+ email = "${cfg.dmarcReporting.email}";
+ domain = "${cfg.dmarcReporting.domain}";
+ org_name = "${cfg.dmarcReporting.organizationName}";
+ from_name = "${cfg.dmarcReporting.fromName}";
+ msgid_from = "dmarc-rua";
+ }''}
+ ''; };
};
overrides = {
@@ -108,6 +119,64 @@ in
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
};
+ systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
+ # Explicitly select yesterday's date to work around broken
+ # default behaviour when called without a date.
+ # https://github.com/rspamd/rspamd/issues/4062
+ script = ''
+ ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
+ '';
+ serviceConfig = {
+ User = "${config.services.rspamd.user}";
+ Group = "${config.services.rspamd.group}";
+
+ AmbientCapabilities = [];
+ CapabilityBoundingSet = "";
+ DevicePolicy = "closed";
+ IPAddressAllow = "localhost";
+ LockPersonality = true;
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectProc = "invisible";
+ ProcSubset = "pid";
+ ProtectSystem = "strict";
+ RemoveIPC = true;
+ RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged"
+ ];
+ UMask = "0077";
+ };
+ };
+
+ systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
+ description = "Daily delivery of aggregated DMARC reports";
+ wantedBy = [
+ "timers.target"
+ ];
+ timerConfig = {
+ OnCalendar = "daily";
+ Persistent = true;
+ RandomizedDelaySec = 86400;
+ FixedRandomDelay = true;
+ };
+ };
+
systemd.services.postfix = {
after = [ rspamdSocket ];
requires = [ rspamdSocket ];
diff --git a/tests/external.nix b/tests/external.nix
index f30a1e5..c14a345 100644
--- a/tests/external.nix
+++ b/tests/external.nix
@@ -43,6 +43,11 @@ pkgs.nixosTest {
domains = [ "example.com" "example2.com" ];
rewriteMessageId = true;
dkimKeyBits = 1535;
+ dmarcReporting = {
+ enable = true;
+ domain = "example.com";
+ organizationName = "ACME Corp";
+ };
loginAccounts = {
"user1@example.com" = {
@@ -494,6 +499,10 @@ pkgs.nixosTest {
# check that Junk is not indexed
server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2")
+ with subtest("dmarc reporting"):
+ server.systemctl("start rspamd-dmarc-reporter.service")
+ server.wait_until_succeeds("journalctl -eu rspamd-dmarc-reporter.service -o cat | grep -q 'No reports for '")
+
with subtest("no warnings or errors"):
server.fail("journalctl -u postfix | grep -i error >&2")
server.fail("journalctl -u postfix | grep -i warning >&2")