Files
cnx-network-clan/modules/monitoring/parsedmarc.nix
T
Berwn 4bafc64b84 fix(monitoring): restore DMARC dashboard under Grafana 13
Grafana 13 externalised its core data sources; nixpkgs no longer bundles
the Elasticsearch datasource, so every DMARC panel failed with "Plugin not
registered". Load the official Grafana-signed elasticsearch plugin via
declarativePlugins, pin the dmarc-ag/dmarc-fo datasource UIDs (Grafana 11+
resolves datasource template variables by UID, not name), and clear stale
uid-less records left by an earlier deploy.
2026-06-26 11:34:08 +07:00

123 lines
4.4 KiB
Nix

# DMARC report analyzer, imported by control only. parsedmarc fetches the
# aggregate/forensic reports that land in the dmarc@cnx.email mailbox on mx1,
# parses the XML, and stores results in a local Elasticsearch; the official
# parsedmarc dashboard + an Elasticsearch datasource are auto-provisioned into
# the Grafana instance that server.nix already runs on this host.
#
# IMAP runs over the ZeroTier mesh, not the public net: we pin mx1.cnx.email to
# its mesh address in /etc/hosts so TLS still validates against the public
# Let's Encrypt cert (primary domain mx1.cnx.email) while the bytes stay on the
# overlay. The mailbox passphrase is the shared mail-dmarc-cred secret; parsedmarc
# reads it as root in its ExecStartPre, so root-owned (clan default) is fine.
{
config,
lib,
pkgs,
...
}:
let
mesh = import ../mesh-hosts.nix { inherit config lib; };
# Grafana 13 (since the externalisation of core data sources) no longer bundles
# the Elasticsearch data source, and nixpkgs has no grafanaPlugins entry for it,
# so a bare elasticsearch datasource reports "Plugin not registered" and every
# DMARC panel fails. Pull the official, Grafana-signed plugin from the catalog
# and load it declaratively.
elasticsearchPlugin = pkgs.grafanaPlugins.grafanaPlugin {
pname = "elasticsearch";
version = "12.6.4";
zipHash.x86_64-linux = "sha256-xyAUprdWyQM0IJSg/oBVZ0ltFAAffgPrphln9+IKcUY=";
};
in
{
imports = [ ../mail-dmarc-cred.nix ];
# Elasticsearch 7.x is under the (unfree) Elastic License; allow just this one
# package rather than opening allowUnfree globally.
nixpkgs.config.allowUnfreePredicate = pkg: lib.getName pkg == "elasticsearch";
services.grafana.declarativePlugins = [ elasticsearchPlugin ];
# Keep mx1's IMAP traffic on the mesh while presenting the public cert name.
networking.hosts.${mesh.hosts.mx1} = [ "mx1.cnx.email" ];
services.parsedmarc = {
enable = true;
provision = {
# Local Elasticsearch on 127.0.0.1:9200 (loopback; no firewall change).
# datasource + dashboard default to true once ES and Grafana are both on.
elasticsearch = true;
# GeoIP needs a MaxMind account/license key; skip it (reports still parse,
# just without source-IP geolocation).
geoIp = false;
grafana = {
# We provision the two Elasticsearch datasources ourselves (below) so we
# can pin their UIDs. Leaving this on would add a second, uid-less pair
# and collide on the dmarc-ag/dmarc-fo names.
datasource = false;
dashboard = true;
};
};
settings = {
imap = {
host = "mx1.cnx.email";
port = 993;
ssl = true;
user = "dmarc@cnx.email";
password = {
_secret = config.clan.core.vars.generators.mail-dmarc-cred.files."passphrase".path;
};
};
mailbox = {
watch = true; # IMAP IDLE: process reports as they arrive
delete = false; # archive processed reports, don't delete
};
general = {
save_aggregate = true;
save_forensic = true;
};
};
};
# The bundled parsedmarc dashboard selects its datasource through template
# variables ($datasourceag/$datasourcefo) whose stored value is the string
# "dmarc-ag"/"dmarc-fo". Grafana 11+ resolves a datasource template variable
# by UID, not by name, so without a matching UID every panel renders
# "datasource was not found". Pin the UIDs to those names so it resolves.
services.grafana.provision.datasources.settings = {
# These two were previously provisioned uid-less, so they exist in Grafana's
# DB with random UIDs. Provisioning a *new* UID onto an existing datasource
# makes Grafana abort with "data source not found" and crash-loop, so delete
# the stale records first; the entries below re-create them with pinned UIDs.
deleteDatasources = [
{
name = "dmarc-ag";
orgId = 1;
}
{
name = "dmarc-fo";
orgId = 1;
}
];
datasources =
let
esVersion = lib.getVersion config.services.elasticsearch.package;
es = name: {
inherit name;
uid = name;
type = "elasticsearch";
access = "proxy";
url = "http://localhost:9200";
jsonData = {
timeField = "date_range";
inherit esVersion;
};
};
in
[
(es "dmarc-ag")
(es "dmarc-fo")
];
};
}