From fe36e7ae0db34862e5397544c7b01472b0febc3a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 3 Oct 2021 14:31:43 +0200 Subject: [PATCH] rspamd: allow configuring dmarc reporting Enabling collects DMARC results in Redis and sends out aggregated reports (RUA) on a daily basis. --- default.nix | 57 ++++++++++++++++++++++++++++++++++ docs/options.rst | 68 ++++++++++++++++++++++++++++++++++++++++- flake.nix | 11 ++++++- mail-server/rspamd.nix | 69 ++++++++++++++++++++++++++++++++++++++++++ tests/external.nix | 9 ++++++ 5 files changed, 212 insertions(+), 2 deletions(-) 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")