···
1
+
{ config, lib, pkgs, ... }:
4
+
cfg = config.services.parsedmarc;
5
+
ini = pkgs.formats.ini {};
8
+
options.services.parsedmarc = {
10
+
enable = lib.mkEnableOption ''
11
+
parsedmarc, a DMARC report monitoring service
16
+
enable = lib.mkOption {
17
+
type = lib.types.bool;
20
+
Whether Postfix and Dovecot should be set up to receive
21
+
mail locally. parsedmarc will be configured to watch the
22
+
local inbox as the automatically created user specified in
23
+
<xref linkend="opt-services.parsedmarc.provision.localMail.recipientName" />
27
+
recipientName = lib.mkOption {
28
+
type = lib.types.str;
31
+
The DMARC mail recipient name, i.e. the name part of the
32
+
email address which receives DMARC reports.
34
+
A local user with this name will be set up and assigned a
35
+
randomized password on service start.
39
+
hostname = lib.mkOption {
40
+
type = lib.types.str;
41
+
default = config.networking.fqdn;
42
+
defaultText = "config.networking.fqdn";
43
+
example = "monitoring.example.com";
45
+
The hostname to use when configuring Postfix.
47
+
Should correspond to the host's fully qualified domain
48
+
name and the domain part of the email address which
49
+
receives DMARC reports. You also have to set up an MX record
50
+
pointing to this domain name.
55
+
geoIp = lib.mkOption {
56
+
type = lib.types.bool;
59
+
Whether to enable and configure the <link
60
+
linkend="opt-services.geoipupdate.enable">geoipupdate</link>
61
+
service to automatically fetch GeoIP databases. Not crucial,
62
+
but recommended for full functionality.
64
+
To finish the setup, you need to manually set the <xref
65
+
linkend="opt-services.geoipupdate.settings.AccountID" /> and
66
+
<xref linkend="opt-services.geoipupdate.settings.LicenseKey" />
71
+
elasticsearch = lib.mkOption {
72
+
type = lib.types.bool;
75
+
Whether to set up and use a local instance of Elasticsearch.
80
+
datasource = lib.mkOption {
81
+
type = lib.types.bool;
82
+
default = cfg.provision.elasticsearch && config.services.grafana.enable;
83
+
apply = x: x && cfg.provision.elasticsearch;
85
+
Whether the automatically provisioned Elasticsearch
86
+
instance should be added as a grafana datasource. Has no
88
+
<xref linkend="opt-services.parsedmarc.provision.elasticsearch" />
93
+
dashboard = lib.mkOption {
94
+
type = lib.types.bool;
95
+
default = config.services.grafana.enable;
97
+
Whether the official parsedmarc grafana dashboard should
98
+
be provisioned to the local grafana instance.
104
+
settings = lib.mkOption {
106
+
Configuration parameters to set in
107
+
<filename>parsedmarc.ini</filename>. For a full list of
108
+
available parameters, see
109
+
<link xlink:href="https://domainaware.github.io/parsedmarc/#configuration-file" />.
112
+
type = lib.types.submodule {
113
+
freeformType = ini.type;
117
+
save_aggregate = lib.mkOption {
118
+
type = lib.types.bool;
121
+
Save aggregate report data to Elasticsearch and/or Splunk.
125
+
save_forensic = lib.mkOption {
126
+
type = lib.types.bool;
129
+
Save forensic report data to Elasticsearch and/or Splunk.
135
+
host = lib.mkOption {
136
+
type = lib.types.str;
137
+
default = "localhost";
139
+
The IMAP server hostname or IP address.
143
+
port = lib.mkOption {
144
+
type = lib.types.port;
147
+
The IMAP server port.
151
+
ssl = lib.mkOption {
152
+
type = lib.types.bool;
155
+
Use an encrypted SSL/TLS connection.
159
+
user = lib.mkOption {
160
+
type = with lib.types; nullOr str;
163
+
The IMAP server username.
167
+
password = lib.mkOption {
168
+
type = with lib.types; nullOr path;
171
+
The path to a file containing the IMAP server password.
175
+
watch = lib.mkOption {
176
+
type = lib.types.bool;
179
+
Use the IMAP IDLE command to process messages as they arrive.
183
+
delete = lib.mkOption {
184
+
type = lib.types.bool;
187
+
Delete messages after processing them, instead of archiving them.
193
+
host = lib.mkOption {
194
+
type = with lib.types; nullOr str;
197
+
The SMTP server hostname or IP address.
201
+
port = lib.mkOption {
202
+
type = with lib.types; nullOr port;
205
+
The SMTP server port.
209
+
ssl = lib.mkOption {
210
+
type = with lib.types; nullOr bool;
213
+
Use an encrypted SSL/TLS connection.
217
+
user = lib.mkOption {
218
+
type = with lib.types; nullOr str;
221
+
The SMTP server username.
225
+
password = lib.mkOption {
226
+
type = with lib.types; nullOr path;
229
+
The path to a file containing the SMTP server password.
233
+
from = lib.mkOption {
234
+
type = with lib.types; nullOr str;
237
+
The <literal>From</literal> address to use for the
242
+
to = lib.mkOption {
243
+
type = with lib.types; nullOr (listOf str);
246
+
The addresses to send outgoing mail to.
252
+
hosts = lib.mkOption {
254
+
type = with lib.types; listOf str;
255
+
apply = x: if x == [] then null else lib.concatStringsSep "," x;
257
+
A list of Elasticsearch hosts to push parsed reports
262
+
user = lib.mkOption {
263
+
type = with lib.types; nullOr str;
266
+
Username to use when connecting to Elasticsearch, if
271
+
password = lib.mkOption {
272
+
type = with lib.types; nullOr path;
275
+
The path to a file containing the password to use when
276
+
connecting to Elasticsearch, if required.
280
+
ssl = lib.mkOption {
281
+
type = lib.types.bool;
284
+
Whether to use an encrypted SSL/TLS connection.
288
+
cert_path = lib.mkOption {
289
+
type = lib.types.path;
290
+
default = "/etc/ssl/certs/ca-certificates.crt";
292
+
The path to a TLS certificate bundle used to verify
293
+
the server's certificate.
299
+
hosts = lib.mkOption {
301
+
type = with lib.types; listOf str;
302
+
apply = x: if x == [] then null else lib.concatStringsSep "," x;
304
+
A list of Apache Kafka hosts to publish parsed reports
309
+
user = lib.mkOption {
310
+
type = with lib.types; nullOr str;
313
+
Username to use when connecting to Kafka, if
318
+
password = lib.mkOption {
319
+
type = with lib.types; nullOr path;
322
+
The path to a file containing the password to use when
323
+
connecting to Kafka, if required.
327
+
ssl = lib.mkOption {
328
+
type = with lib.types; nullOr bool;
331
+
Whether to use an encrypted SSL/TLS connection.
335
+
aggregate_topic = lib.mkOption {
336
+
type = with lib.types; nullOr str;
338
+
example = "aggregate";
340
+
The Kafka topic to publish aggregate reports on.
344
+
forensic_topic = lib.mkOption {
345
+
type = with lib.types; nullOr str;
347
+
example = "forensic";
349
+
The Kafka topic to publish forensic reports on.
361
+
config = lib.mkIf cfg.enable {
363
+
services.elasticsearch.enable = lib.mkDefault cfg.provision.elasticsearch;
365
+
services.geoipupdate = lib.mkIf cfg.provision.geoIp {
373
+
DatabaseDirectory = "/var/lib/GeoIP";
377
+
services.dovecot2 = lib.mkIf cfg.provision.localMail.enable {
379
+
protocols = [ "imap" ];
382
+
services.postfix = lib.mkIf cfg.provision.localMail.enable {
384
+
origin = cfg.provision.localMail.hostname;
386
+
myhostname = cfg.provision.localMail.hostname;
387
+
mydestination = cfg.provision.localMail.hostname;
391
+
services.grafana = {
392
+
declarativePlugins = with pkgs.grafanaPlugins;
393
+
lib.mkIf cfg.provision.grafana.dashboard [
394
+
grafana-worldmap-panel
395
+
grafana-piechart-panel
399
+
enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
402
+
pkgVer = lib.getVersion config.services.elasticsearch.package;
404
+
if lib.versionOlder pkgVer "7" then
406
+
else if lib.versionOlder pkgVer "8" then
409
+
throw "When provisioning parsedmarc grafana datasources: unknown Elasticsearch version.";
411
+
lib.mkIf cfg.provision.grafana.datasource [
414
+
type = "elasticsearch";
416
+
url = "localhost:9200";
418
+
timeField = "date_range";
424
+
type = "elasticsearch";
426
+
url = "localhost:9200";
428
+
timeField = "date_range";
433
+
dashboards = lib.mkIf cfg.provision.grafana.dashboard [{
434
+
name = "parsedmarc";
435
+
options.path = "${pkgs.python3Packages.parsedmarc.dashboard}";
440
+
services.parsedmarc.settings = lib.mkMerge [
441
+
(lib.mkIf cfg.provision.elasticsearch {
443
+
hosts = [ "localhost:9200" ];
447
+
(lib.mkIf cfg.provision.localMail.enable {
449
+
host = "localhost";
452
+
user = cfg.provision.localMail.recipientName;
453
+
password = "${pkgs.writeText "imap-password" "@imap-password@"}";
459
+
systemd.services.parsedmarc =
461
+
# Remove any empty attributes from the config, i.e. empty
462
+
# lists, empty attrsets and null. This makes it possible to
463
+
# list interesting options in `settings` without them always
464
+
# ending up in the resulting config.
465
+
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! builtins.elem v [ null [] {} ])) cfg.settings;
466
+
parsedmarcConfig = ini.generate "parsedmarc.ini" filteredConfig;
467
+
mkSecretReplacement = file:
468
+
lib.optionalString (file != null) ''
469
+
replace-secret '${file}' '${file}' /run/parsedmarc/parsedmarc.ini
473
+
wantedBy = [ "multi-user.target" ];
474
+
after = [ "postfix.service" "dovecot2.service" "elasticsearch.service" ];
475
+
path = with pkgs; [ replace-secret openssl shadow ];
478
+
startPreFullPrivileges = ''
479
+
set -o errexit -o pipefail -o nounset -o errtrace
480
+
shopt -s inherit_errexit
483
+
cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini
484
+
chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini
485
+
${mkSecretReplacement cfg.settings.smtp.password}
486
+
${mkSecretReplacement cfg.settings.imap.password}
487
+
${mkSecretReplacement cfg.settings.elasticsearch.password}
488
+
${mkSecretReplacement cfg.settings.kafka.password}
489
+
'' + lib.optionalString cfg.provision.localMail.enable ''
490
+
openssl rand -hex 64 >/run/parsedmarc/dmarc_user_passwd
491
+
replace-secret '@imap-password@' '/run/parsedmarc/dmarc_user_passwd' /run/parsedmarc/parsedmarc.ini
492
+
echo "Setting new randomized password for user '${cfg.provision.localMail.recipientName}'."
493
+
cat <(echo -n "${cfg.provision.localMail.recipientName}:") /run/parsedmarc/dmarc_user_passwd | chpasswd
496
+
"+${pkgs.writeShellScript "parsedmarc-start-pre-full-privileges" startPreFullPrivileges}";
498
+
User = "parsedmarc";
499
+
Group = "parsedmarc";
500
+
DynamicUser = true;
501
+
RuntimeDirectory = "parsedmarc";
502
+
RuntimeDirectoryMode = 0700;
503
+
CapabilityBoundingSet = "";
504
+
PrivateDevices = true;
505
+
PrivateMounts = true;
506
+
PrivateUsers = true;
507
+
ProtectClock = true;
508
+
ProtectControlGroups = true;
509
+
ProtectHome = true;
510
+
ProtectHostname = true;
511
+
ProtectKernelLogs = true;
512
+
ProtectKernelModules = true;
513
+
ProtectKernelTunables = true;
514
+
ProtectProc = "invisible";
515
+
ProcSubset = "pid";
516
+
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
517
+
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
518
+
RestrictRealtime = true;
519
+
RestrictNamespaces = true;
520
+
MemoryDenyWriteExecute = true;
521
+
LockPersonality = true;
522
+
SystemCallArchitectures = "native";
523
+
ExecStart = "${pkgs.python3Packages.parsedmarc}/bin/parsedmarc -c /run/parsedmarc/parsedmarc.ini";
527
+
users.users.${cfg.provision.localMail.recipientName} = lib.mkIf cfg.provision.localMail.enable {
528
+
isNormalUser = true;
529
+
description = "DMARC mail recipient";