···239239 <link xlink:href="options.html#opt-programs.git.enable">programs.git</link>.
240240 </para>
241241 </listitem>
242242+ <listitem>
243243+ <para>
244244+ <link xlink:href="https://domainaware.github.io/parsedmarc/">parsedmarc</link>,
245245+ a service which parses incoming
246246+ <link xlink:href="https://dmarc.org/">DMARC</link> reports and
247247+ stores or sends them to a downstream service for further
248248+ analysis. Documented in
249249+ <link linkend="module-services-parsedmarc">its manual
250250+ entry</link>.
251251+ </para>
252252+ </listitem>
242253 </itemizedlist>
243254 </section>
244255 <section xml:id="sec-release-21.11-incompatibilities">
+5
nixos/doc/manual/release-notes/rl-2111.section.md
···73737474- [git](https://git-scm.com), a distributed version control system. Available as [programs.git](options.html#opt-programs.git.enable).
75757676+- [parsedmarc](https://domainaware.github.io/parsedmarc/), a service
7777+ which parses incoming [DMARC](https://dmarc.org/) reports and stores
7878+ or sends them to a downstream service for further analysis.
7979+ Documented in [its manual entry](#module-services-parsedmarc).
8080+7681## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
77827883
···11+# parsedmarc {#module-services-parsedmarc}
22+[parsedmarc](https://domainaware.github.io/parsedmarc/) is a service
33+which parses incoming [DMARC](https://dmarc.org/) reports and stores
44+or sends them to a downstream service for further analysis. In
55+combination with Elasticsearch, Grafana and the included Grafana
66+dashboard, it provides a handy overview of DMARC reports over time.
77+88+## Basic usage {#module-services-parsedmarc-basic-usage}
99+A very minimal setup which reads incoming reports from an external
1010+email address and saves them to a local Elasticsearch instance looks
1111+like this:
1212+1313+```nix
1414+services.parsedmarc = {
1515+ enable = true;
1616+ settings.imap = {
1717+ host = "imap.example.com";
1818+ user = "alice@example.com";
1919+ password = "/path/to/imap_password_file";
2020+ watch = true;
2121+ };
2222+ provision.geoIp = false; # Not recommended!
2323+};
2424+```
2525+2626+Note that GeoIP provisioning is disabled in the example for
2727+simplicity, but should be turned on for fully functional reports.
2828+2929+## Local mail
3030+Instead of watching an external inbox, a local inbox can be
3131+automatically provisioned. The recipient's name is by default set to
3232+`dmarc`, but can be configured in
3333+[services.parsedmarc.provision.localMail.recipientName](options.html#opt-services.parsedmarc.provision.localMail.recipientName). You
3434+need to add an MX record pointing to the host. More concretely: for
3535+the example to work, an MX record needs to be set up for
3636+`monitoring.example.com` and the complete email address that should be
3737+configured in the domain's dmarc policy is
3838+`dmarc@monitoring.example.com`.
3939+4040+```nix
4141+services.parsedmarc = {
4242+ enable = true;
4343+ provision = {
4444+ localMail = {
4545+ enable = true;
4646+ hostname = monitoring.example.com;
4747+ };
4848+ geoIp = false; # Not recommended!
4949+ };
5050+};
5151+```
5252+5353+## Grafana and GeoIP
5454+The reports can be visualized and summarized with parsedmarc's
5555+official Grafana dashboard. For all views to work, and for the data to
5656+be complete, GeoIP databases are also required. The following example
5757+shows a basic deployment where the provisioned Elasticsearch instance
5858+is automatically added as a Grafana datasource, and the dashboard is
5959+added to Grafana as well.
6060+6161+```nix
6262+services.parsedmarc = {
6363+ enable = true;
6464+ provision = {
6565+ localMail = {
6666+ enable = true;
6767+ hostname = url;
6868+ };
6969+ grafana = {
7070+ datasource = true;
7171+ dashboard = true;
7272+ };
7373+ };
7474+};
7575+7676+# Not required, but recommended for full functionality
7777+services.geoipupdate = {
7878+ settings = {
7979+ AccountID = 000000;
8080+ LicenseKey = "/path/to/license_key_file";
8181+ };
8282+};
8383+8484+services.grafana = {
8585+ enable = true;
8686+ addr = "0.0.0.0";
8787+ domain = url;
8888+ rootUrl = "https://" + url;
8989+ protocol = "socket";
9090+ security = {
9191+ adminUser = "admin";
9292+ adminPasswordFile = "/path/to/admin_password_file";
9393+ secretKeyFile = "/path/to/secret_key_file";
9494+ };
9595+};
9696+9797+services.nginx = {
9898+ enable = true;
9999+ recommendedTlsSettings = true;
100100+ recommendedOptimisation = true;
101101+ recommendedGzipSettings = true;
102102+ recommendedProxySettings = true;
103103+ upstreams.grafana.servers."unix:/${config.services.grafana.socket}" = {};
104104+ virtualHosts.${url} = {
105105+ root = config.services.grafana.staticRootPath;
106106+ enableACME = true;
107107+ forceSSL = true;
108108+ locations."/".tryFiles = "$uri @grafana";
109109+ locations."@grafana".proxyPass = "http://grafana";
110110+ };
111111+};
112112+users.users.nginx.extraGroups = [ "grafana" ];
113113+```
+537
nixos/modules/services/monitoring/parsedmarc.nix
···11+{ config, lib, pkgs, ... }:
22+33+let
44+ cfg = config.services.parsedmarc;
55+ ini = pkgs.formats.ini {};
66+in
77+{
88+ options.services.parsedmarc = {
99+1010+ enable = lib.mkEnableOption ''
1111+ parsedmarc, a DMARC report monitoring service
1212+ '';
1313+1414+ provision = {
1515+ localMail = {
1616+ enable = lib.mkOption {
1717+ type = lib.types.bool;
1818+ default = false;
1919+ description = ''
2020+ Whether Postfix and Dovecot should be set up to receive
2121+ mail locally. parsedmarc will be configured to watch the
2222+ local inbox as the automatically created user specified in
2323+ <xref linkend="opt-services.parsedmarc.provision.localMail.recipientName" />
2424+ '';
2525+ };
2626+2727+ recipientName = lib.mkOption {
2828+ type = lib.types.str;
2929+ default = "dmarc";
3030+ description = ''
3131+ The DMARC mail recipient name, i.e. the name part of the
3232+ email address which receives DMARC reports.
3333+3434+ A local user with this name will be set up and assigned a
3535+ randomized password on service start.
3636+ '';
3737+ };
3838+3939+ hostname = lib.mkOption {
4040+ type = lib.types.str;
4141+ default = config.networking.fqdn;
4242+ defaultText = "config.networking.fqdn";
4343+ example = "monitoring.example.com";
4444+ description = ''
4545+ The hostname to use when configuring Postfix.
4646+4747+ Should correspond to the host's fully qualified domain
4848+ name and the domain part of the email address which
4949+ receives DMARC reports. You also have to set up an MX record
5050+ pointing to this domain name.
5151+ '';
5252+ };
5353+ };
5454+5555+ geoIp = lib.mkOption {
5656+ type = lib.types.bool;
5757+ default = true;
5858+ description = ''
5959+ Whether to enable and configure the <link
6060+ linkend="opt-services.geoipupdate.enable">geoipupdate</link>
6161+ service to automatically fetch GeoIP databases. Not crucial,
6262+ but recommended for full functionality.
6363+6464+ To finish the setup, you need to manually set the <xref
6565+ linkend="opt-services.geoipupdate.settings.AccountID" /> and
6666+ <xref linkend="opt-services.geoipupdate.settings.LicenseKey" />
6767+ options.
6868+ '';
6969+ };
7070+7171+ elasticsearch = lib.mkOption {
7272+ type = lib.types.bool;
7373+ default = true;
7474+ description = ''
7575+ Whether to set up and use a local instance of Elasticsearch.
7676+ '';
7777+ };
7878+7979+ grafana = {
8080+ datasource = lib.mkOption {
8181+ type = lib.types.bool;
8282+ default = cfg.provision.elasticsearch && config.services.grafana.enable;
8383+ apply = x: x && cfg.provision.elasticsearch;
8484+ description = ''
8585+ Whether the automatically provisioned Elasticsearch
8686+ instance should be added as a grafana datasource. Has no
8787+ effect unless
8888+ <xref linkend="opt-services.parsedmarc.provision.elasticsearch" />
8989+ is also enabled.
9090+ '';
9191+ };
9292+9393+ dashboard = lib.mkOption {
9494+ type = lib.types.bool;
9595+ default = config.services.grafana.enable;
9696+ description = ''
9797+ Whether the official parsedmarc grafana dashboard should
9898+ be provisioned to the local grafana instance.
9999+ '';
100100+ };
101101+ };
102102+ };
103103+104104+ settings = lib.mkOption {
105105+ description = ''
106106+ Configuration parameters to set in
107107+ <filename>parsedmarc.ini</filename>. For a full list of
108108+ available parameters, see
109109+ <link xlink:href="https://domainaware.github.io/parsedmarc/#configuration-file" />.
110110+ '';
111111+112112+ type = lib.types.submodule {
113113+ freeformType = ini.type;
114114+115115+ options = {
116116+ general = {
117117+ save_aggregate = lib.mkOption {
118118+ type = lib.types.bool;
119119+ default = true;
120120+ description = ''
121121+ Save aggregate report data to Elasticsearch and/or Splunk.
122122+ '';
123123+ };
124124+125125+ save_forensic = lib.mkOption {
126126+ type = lib.types.bool;
127127+ default = true;
128128+ description = ''
129129+ Save forensic report data to Elasticsearch and/or Splunk.
130130+ '';
131131+ };
132132+ };
133133+134134+ imap = {
135135+ host = lib.mkOption {
136136+ type = lib.types.str;
137137+ default = "localhost";
138138+ description = ''
139139+ The IMAP server hostname or IP address.
140140+ '';
141141+ };
142142+143143+ port = lib.mkOption {
144144+ type = lib.types.port;
145145+ default = 993;
146146+ description = ''
147147+ The IMAP server port.
148148+ '';
149149+ };
150150+151151+ ssl = lib.mkOption {
152152+ type = lib.types.bool;
153153+ default = true;
154154+ description = ''
155155+ Use an encrypted SSL/TLS connection.
156156+ '';
157157+ };
158158+159159+ user = lib.mkOption {
160160+ type = with lib.types; nullOr str;
161161+ default = null;
162162+ description = ''
163163+ The IMAP server username.
164164+ '';
165165+ };
166166+167167+ password = lib.mkOption {
168168+ type = with lib.types; nullOr path;
169169+ default = null;
170170+ description = ''
171171+ The path to a file containing the IMAP server password.
172172+ '';
173173+ };
174174+175175+ watch = lib.mkOption {
176176+ type = lib.types.bool;
177177+ default = true;
178178+ description = ''
179179+ Use the IMAP IDLE command to process messages as they arrive.
180180+ '';
181181+ };
182182+183183+ delete = lib.mkOption {
184184+ type = lib.types.bool;
185185+ default = false;
186186+ description = ''
187187+ Delete messages after processing them, instead of archiving them.
188188+ '';
189189+ };
190190+ };
191191+192192+ smtp = {
193193+ host = lib.mkOption {
194194+ type = with lib.types; nullOr str;
195195+ default = null;
196196+ description = ''
197197+ The SMTP server hostname or IP address.
198198+ '';
199199+ };
200200+201201+ port = lib.mkOption {
202202+ type = with lib.types; nullOr port;
203203+ default = null;
204204+ description = ''
205205+ The SMTP server port.
206206+ '';
207207+ };
208208+209209+ ssl = lib.mkOption {
210210+ type = with lib.types; nullOr bool;
211211+ default = null;
212212+ description = ''
213213+ Use an encrypted SSL/TLS connection.
214214+ '';
215215+ };
216216+217217+ user = lib.mkOption {
218218+ type = with lib.types; nullOr str;
219219+ default = null;
220220+ description = ''
221221+ The SMTP server username.
222222+ '';
223223+ };
224224+225225+ password = lib.mkOption {
226226+ type = with lib.types; nullOr path;
227227+ default = null;
228228+ description = ''
229229+ The path to a file containing the SMTP server password.
230230+ '';
231231+ };
232232+233233+ from = lib.mkOption {
234234+ type = with lib.types; nullOr str;
235235+ default = null;
236236+ description = ''
237237+ The <literal>From</literal> address to use for the
238238+ outgoing mail.
239239+ '';
240240+ };
241241+242242+ to = lib.mkOption {
243243+ type = with lib.types; nullOr (listOf str);
244244+ default = null;
245245+ description = ''
246246+ The addresses to send outgoing mail to.
247247+ '';
248248+ };
249249+ };
250250+251251+ elasticsearch = {
252252+ hosts = lib.mkOption {
253253+ default = [];
254254+ type = with lib.types; listOf str;
255255+ apply = x: if x == [] then null else lib.concatStringsSep "," x;
256256+ description = ''
257257+ A list of Elasticsearch hosts to push parsed reports
258258+ to.
259259+ '';
260260+ };
261261+262262+ user = lib.mkOption {
263263+ type = with lib.types; nullOr str;
264264+ default = null;
265265+ description = ''
266266+ Username to use when connecting to Elasticsearch, if
267267+ required.
268268+ '';
269269+ };
270270+271271+ password = lib.mkOption {
272272+ type = with lib.types; nullOr path;
273273+ default = null;
274274+ description = ''
275275+ The path to a file containing the password to use when
276276+ connecting to Elasticsearch, if required.
277277+ '';
278278+ };
279279+280280+ ssl = lib.mkOption {
281281+ type = lib.types.bool;
282282+ default = false;
283283+ description = ''
284284+ Whether to use an encrypted SSL/TLS connection.
285285+ '';
286286+ };
287287+288288+ cert_path = lib.mkOption {
289289+ type = lib.types.path;
290290+ default = "/etc/ssl/certs/ca-certificates.crt";
291291+ description = ''
292292+ The path to a TLS certificate bundle used to verify
293293+ the server's certificate.
294294+ '';
295295+ };
296296+ };
297297+298298+ kafka = {
299299+ hosts = lib.mkOption {
300300+ default = [];
301301+ type = with lib.types; listOf str;
302302+ apply = x: if x == [] then null else lib.concatStringsSep "," x;
303303+ description = ''
304304+ A list of Apache Kafka hosts to publish parsed reports
305305+ to.
306306+ '';
307307+ };
308308+309309+ user = lib.mkOption {
310310+ type = with lib.types; nullOr str;
311311+ default = null;
312312+ description = ''
313313+ Username to use when connecting to Kafka, if
314314+ required.
315315+ '';
316316+ };
317317+318318+ password = lib.mkOption {
319319+ type = with lib.types; nullOr path;
320320+ default = null;
321321+ description = ''
322322+ The path to a file containing the password to use when
323323+ connecting to Kafka, if required.
324324+ '';
325325+ };
326326+327327+ ssl = lib.mkOption {
328328+ type = with lib.types; nullOr bool;
329329+ default = null;
330330+ description = ''
331331+ Whether to use an encrypted SSL/TLS connection.
332332+ '';
333333+ };
334334+335335+ aggregate_topic = lib.mkOption {
336336+ type = with lib.types; nullOr str;
337337+ default = null;
338338+ example = "aggregate";
339339+ description = ''
340340+ The Kafka topic to publish aggregate reports on.
341341+ '';
342342+ };
343343+344344+ forensic_topic = lib.mkOption {
345345+ type = with lib.types; nullOr str;
346346+ default = null;
347347+ example = "forensic";
348348+ description = ''
349349+ The Kafka topic to publish forensic reports on.
350350+ '';
351351+ };
352352+ };
353353+354354+ };
355355+356356+ };
357357+ };
358358+359359+ };
360360+361361+ config = lib.mkIf cfg.enable {
362362+363363+ services.elasticsearch.enable = lib.mkDefault cfg.provision.elasticsearch;
364364+365365+ services.geoipupdate = lib.mkIf cfg.provision.geoIp {
366366+ enable = true;
367367+ settings = {
368368+ EditionIDs = [
369369+ "GeoLite2-ASN"
370370+ "GeoLite2-City"
371371+ "GeoLite2-Country"
372372+ ];
373373+ DatabaseDirectory = "/var/lib/GeoIP";
374374+ };
375375+ };
376376+377377+ services.dovecot2 = lib.mkIf cfg.provision.localMail.enable {
378378+ enable = true;
379379+ protocols = [ "imap" ];
380380+ };
381381+382382+ services.postfix = lib.mkIf cfg.provision.localMail.enable {
383383+ enable = true;
384384+ origin = cfg.provision.localMail.hostname;
385385+ config = {
386386+ myhostname = cfg.provision.localMail.hostname;
387387+ mydestination = cfg.provision.localMail.hostname;
388388+ };
389389+ };
390390+391391+ services.grafana = {
392392+ declarativePlugins = with pkgs.grafanaPlugins;
393393+ lib.mkIf cfg.provision.grafana.dashboard [
394394+ grafana-worldmap-panel
395395+ grafana-piechart-panel
396396+ ];
397397+398398+ provision = {
399399+ enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
400400+ datasources =
401401+ let
402402+ pkgVer = lib.getVersion config.services.elasticsearch.package;
403403+ esVersion =
404404+ if lib.versionOlder pkgVer "7" then
405405+ "60"
406406+ else if lib.versionOlder pkgVer "8" then
407407+ "70"
408408+ else
409409+ throw "When provisioning parsedmarc grafana datasources: unknown Elasticsearch version.";
410410+ in
411411+ lib.mkIf cfg.provision.grafana.datasource [
412412+ {
413413+ name = "dmarc-ag";
414414+ type = "elasticsearch";
415415+ access = "proxy";
416416+ url = "localhost:9200";
417417+ jsonData = {
418418+ timeField = "date_range";
419419+ inherit esVersion;
420420+ };
421421+ }
422422+ {
423423+ name = "dmarc-fo";
424424+ type = "elasticsearch";
425425+ access = "proxy";
426426+ url = "localhost:9200";
427427+ jsonData = {
428428+ timeField = "date_range";
429429+ inherit esVersion;
430430+ };
431431+ }
432432+ ];
433433+ dashboards = lib.mkIf cfg.provision.grafana.dashboard [{
434434+ name = "parsedmarc";
435435+ options.path = "${pkgs.python3Packages.parsedmarc.dashboard}";
436436+ }];
437437+ };
438438+ };
439439+440440+ services.parsedmarc.settings = lib.mkMerge [
441441+ (lib.mkIf cfg.provision.elasticsearch {
442442+ elasticsearch = {
443443+ hosts = [ "localhost:9200" ];
444444+ ssl = false;
445445+ };
446446+ })
447447+ (lib.mkIf cfg.provision.localMail.enable {
448448+ imap = {
449449+ host = "localhost";
450450+ port = 143;
451451+ ssl = false;
452452+ user = cfg.provision.localMail.recipientName;
453453+ password = "${pkgs.writeText "imap-password" "@imap-password@"}";
454454+ watch = true;
455455+ };
456456+ })
457457+ ];
458458+459459+ systemd.services.parsedmarc =
460460+ let
461461+ # Remove any empty attributes from the config, i.e. empty
462462+ # lists, empty attrsets and null. This makes it possible to
463463+ # list interesting options in `settings` without them always
464464+ # ending up in the resulting config.
465465+ filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! builtins.elem v [ null [] {} ])) cfg.settings;
466466+ parsedmarcConfig = ini.generate "parsedmarc.ini" filteredConfig;
467467+ mkSecretReplacement = file:
468468+ lib.optionalString (file != null) ''
469469+ replace-secret '${file}' '${file}' /run/parsedmarc/parsedmarc.ini
470470+ '';
471471+ in
472472+ {
473473+ wantedBy = [ "multi-user.target" ];
474474+ after = [ "postfix.service" "dovecot2.service" "elasticsearch.service" ];
475475+ path = with pkgs; [ replace-secret openssl shadow ];
476476+ serviceConfig = {
477477+ ExecStartPre = let
478478+ startPreFullPrivileges = ''
479479+ set -o errexit -o pipefail -o nounset -o errtrace
480480+ shopt -s inherit_errexit
481481+482482+ umask u=rwx,g=,o=
483483+ cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini
484484+ chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini
485485+ ${mkSecretReplacement cfg.settings.smtp.password}
486486+ ${mkSecretReplacement cfg.settings.imap.password}
487487+ ${mkSecretReplacement cfg.settings.elasticsearch.password}
488488+ ${mkSecretReplacement cfg.settings.kafka.password}
489489+ '' + lib.optionalString cfg.provision.localMail.enable ''
490490+ openssl rand -hex 64 >/run/parsedmarc/dmarc_user_passwd
491491+ replace-secret '@imap-password@' '/run/parsedmarc/dmarc_user_passwd' /run/parsedmarc/parsedmarc.ini
492492+ echo "Setting new randomized password for user '${cfg.provision.localMail.recipientName}'."
493493+ cat <(echo -n "${cfg.provision.localMail.recipientName}:") /run/parsedmarc/dmarc_user_passwd | chpasswd
494494+ '';
495495+ in
496496+ "+${pkgs.writeShellScript "parsedmarc-start-pre-full-privileges" startPreFullPrivileges}";
497497+ Type = "simple";
498498+ User = "parsedmarc";
499499+ Group = "parsedmarc";
500500+ DynamicUser = true;
501501+ RuntimeDirectory = "parsedmarc";
502502+ RuntimeDirectoryMode = 0700;
503503+ CapabilityBoundingSet = "";
504504+ PrivateDevices = true;
505505+ PrivateMounts = true;
506506+ PrivateUsers = true;
507507+ ProtectClock = true;
508508+ ProtectControlGroups = true;
509509+ ProtectHome = true;
510510+ ProtectHostname = true;
511511+ ProtectKernelLogs = true;
512512+ ProtectKernelModules = true;
513513+ ProtectKernelTunables = true;
514514+ ProtectProc = "invisible";
515515+ ProcSubset = "pid";
516516+ SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
517517+ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
518518+ RestrictRealtime = true;
519519+ RestrictNamespaces = true;
520520+ MemoryDenyWriteExecute = true;
521521+ LockPersonality = true;
522522+ SystemCallArchitectures = "native";
523523+ ExecStart = "${pkgs.python3Packages.parsedmarc}/bin/parsedmarc -c /run/parsedmarc/parsedmarc.ini";
524524+ };
525525+ };
526526+527527+ users.users.${cfg.provision.localMail.recipientName} = lib.mkIf cfg.provision.localMail.enable {
528528+ isNormalUser = true;
529529+ description = "DMARC mail recipient";
530530+ };
531531+ };
532532+533533+ # Don't edit the docbook xml directly, edit the md and generate it:
534534+ # `pandoc parsedmarc.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > parsedmarc.xml`
535535+ meta.doc = ./parsedmarc.xml;
536536+ meta.maintainers = [ lib.maintainers.talyz ];
537537+}
+125
nixos/modules/services/monitoring/parsedmarc.xml
···11+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-parsedmarc">
22+ <title>parsedmarc</title>
33+ <para>
44+ <link xlink:href="https://domainaware.github.io/parsedmarc/">parsedmarc</link>
55+ is a service which parses incoming
66+ <link xlink:href="https://dmarc.org/">DMARC</link> reports and
77+ stores or sends them to a downstream service for further analysis.
88+ In combination with Elasticsearch, Grafana and the included Grafana
99+ dashboard, it provides a handy overview of DMARC reports over time.
1010+ </para>
1111+ <section xml:id="module-services-parsedmarc-basic-usage">
1212+ <title>Basic usage</title>
1313+ <para>
1414+ A very minimal setup which reads incoming reports from an external
1515+ email address and saves them to a local Elasticsearch instance
1616+ looks like this:
1717+ </para>
1818+ <programlisting language="bash">
1919+services.parsedmarc = {
2020+ enable = true;
2121+ settings.imap = {
2222+ host = "imap.example.com";
2323+ user = "alice@example.com";
2424+ password = "/path/to/imap_password_file";
2525+ watch = true;
2626+ };
2727+ provision.geoIp = false; # Not recommended!
2828+};
2929+</programlisting>
3030+ <para>
3131+ Note that GeoIP provisioning is disabled in the example for
3232+ simplicity, but should be turned on for fully functional reports.
3333+ </para>
3434+ </section>
3535+ <section xml:id="local-mail">
3636+ <title>Local mail</title>
3737+ <para>
3838+ Instead of watching an external inbox, a local inbox can be
3939+ automatically provisioned. The recipient’s name is by default set
4040+ to <literal>dmarc</literal>, but can be configured in
4141+ <link xlink:href="options.html#opt-services.parsedmarc.provision.localMail.recipientName">services.parsedmarc.provision.localMail.recipientName</link>.
4242+ You need to add an MX record pointing to the host. More
4343+ concretely: for the example to work, an MX record needs to be set
4444+ up for <literal>monitoring.example.com</literal> and the complete
4545+ email address that should be configured in the domain’s dmarc
4646+ policy is <literal>dmarc@monitoring.example.com</literal>.
4747+ </para>
4848+ <programlisting language="bash">
4949+services.parsedmarc = {
5050+ enable = true;
5151+ provision = {
5252+ localMail = {
5353+ enable = true;
5454+ hostname = monitoring.example.com;
5555+ };
5656+ geoIp = false; # Not recommended!
5757+ };
5858+};
5959+</programlisting>
6060+ </section>
6161+ <section xml:id="grafana-and-geoip">
6262+ <title>Grafana and GeoIP</title>
6363+ <para>
6464+ The reports can be visualized and summarized with parsedmarc’s
6565+ official Grafana dashboard. For all views to work, and for the
6666+ data to be complete, GeoIP databases are also required. The
6767+ following example shows a basic deployment where the provisioned
6868+ Elasticsearch instance is automatically added as a Grafana
6969+ datasource, and the dashboard is added to Grafana as well.
7070+ </para>
7171+ <programlisting language="bash">
7272+services.parsedmarc = {
7373+ enable = true;
7474+ provision = {
7575+ localMail = {
7676+ enable = true;
7777+ hostname = url;
7878+ };
7979+ grafana = {
8080+ datasource = true;
8181+ dashboard = true;
8282+ };
8383+ };
8484+};
8585+8686+# Not required, but recommended for full functionality
8787+services.geoipupdate = {
8888+ settings = {
8989+ AccountID = 000000;
9090+ LicenseKey = "/path/to/license_key_file";
9191+ };
9292+};
9393+9494+services.grafana = {
9595+ enable = true;
9696+ addr = "0.0.0.0";
9797+ domain = url;
9898+ rootUrl = "https://" + url;
9999+ protocol = "socket";
100100+ security = {
101101+ adminUser = "admin";
102102+ adminPasswordFile = "/path/to/admin_password_file";
103103+ secretKeyFile = "/path/to/secret_key_file";
104104+ };
105105+};
106106+107107+services.nginx = {
108108+ enable = true;
109109+ recommendedTlsSettings = true;
110110+ recommendedOptimisation = true;
111111+ recommendedGzipSettings = true;
112112+ recommendedProxySettings = true;
113113+ upstreams.grafana.servers."unix:/${config.services.grafana.socket}" = {};
114114+ virtualHosts.${url} = {
115115+ root = config.services.grafana.staticRootPath;
116116+ enableACME = true;
117117+ forceSSL = true;
118118+ locations."/".tryFiles = "$uri @grafana";
119119+ locations."@grafana".proxyPass = "http://grafana";
120120+ };
121121+};
122122+users.users.nginx.extraGroups = [ "grafana" ];
123123+</programlisting>
124124+ </section>
125125+</chapter>
+7
nixos/modules/services/search/elasticsearch.nix
···201201202202 if [ "$(id -u)" = 0 ]; then chown -R elasticsearch:elasticsearch ${cfg.dataDir}; fi
203203 '';
204204+ postStart = ''
205205+ # Make sure elasticsearch is up and running before dependents
206206+ # are started
207207+ while ! ${pkgs.curl}/bin/curl -sS -f http://localhost:${toString cfg.port} 2>/dev/null; do
208208+ sleep 1
209209+ done
210210+ '';
204211 };
205212206213 environment.systemPackages = [ cfg.package ];