0a6e6cf7e6
this converts meta.doc into an md pointer, not an xml pointer. since we no longer need xml for manual chapters we can also remove support for manual chapters from md-to-db.sh since pandoc converts smart quotes to docbook quote elements and our nixos-render-docs does not we lose this distinction in the rendered output. that's probably not that bad, our stylesheet didn't make use of this anyway (and pre-23.05 versions of the chapters didn't use quote elements either). also updates the nixpkgs manual to clarify that option docs support all extensions (although it doesn't support headings at all, so heading anchors don't work by extension).
544 lines
19 KiB
Nix
544 lines
19 KiB
Nix
{ config, lib, options, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.parsedmarc;
|
|
opt = options.services.parsedmarc;
|
|
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
|
|
ini = pkgs.formats.ini {
|
|
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec {
|
|
mkValueString = v:
|
|
if isInt v then toString v
|
|
else if isString v then v
|
|
else if true == v then "True"
|
|
else if false == v then "False"
|
|
else if isSecret v then hashString "sha256" v._secret
|
|
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
|
|
};
|
|
};
|
|
inherit (builtins) elem isAttrs isString isInt isList typeOf hashString;
|
|
in
|
|
{
|
|
options.services.parsedmarc = {
|
|
|
|
enable = lib.mkEnableOption (lib.mdDoc ''
|
|
parsedmarc, a DMARC report monitoring service
|
|
'');
|
|
|
|
provision = {
|
|
localMail = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = lib.mdDoc ''
|
|
Whether Postfix and Dovecot should be set up to receive
|
|
mail locally. parsedmarc will be configured to watch the
|
|
local inbox as the automatically created user specified in
|
|
[](#opt-services.parsedmarc.provision.localMail.recipientName)
|
|
'';
|
|
};
|
|
|
|
recipientName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "dmarc";
|
|
description = lib.mdDoc ''
|
|
The DMARC mail recipient name, i.e. the name part of the
|
|
email address which receives DMARC reports.
|
|
|
|
A local user with this name will be set up and assigned a
|
|
randomized password on service start.
|
|
'';
|
|
};
|
|
|
|
hostname = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = config.networking.fqdn;
|
|
defaultText = lib.literalExpression "config.networking.fqdn";
|
|
example = "monitoring.example.com";
|
|
description = lib.mdDoc ''
|
|
The hostname to use when configuring Postfix.
|
|
|
|
Should correspond to the host's fully qualified domain
|
|
name and the domain part of the email address which
|
|
receives DMARC reports. You also have to set up an MX record
|
|
pointing to this domain name.
|
|
'';
|
|
};
|
|
};
|
|
|
|
geoIp = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = lib.mdDoc ''
|
|
Whether to enable and configure the [geoipupdate](#opt-services.geoipupdate.enable)
|
|
service to automatically fetch GeoIP databases. Not crucial,
|
|
but recommended for full functionality.
|
|
|
|
To finish the setup, you need to manually set the [](#opt-services.geoipupdate.settings.AccountID) and
|
|
[](#opt-services.geoipupdate.settings.LicenseKey)
|
|
options.
|
|
'';
|
|
};
|
|
|
|
elasticsearch = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = lib.mdDoc ''
|
|
Whether to set up and use a local instance of Elasticsearch.
|
|
'';
|
|
};
|
|
|
|
grafana = {
|
|
datasource = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = cfg.provision.elasticsearch && config.services.grafana.enable;
|
|
defaultText = lib.literalExpression ''
|
|
config.${opt.provision.elasticsearch} && config.${options.services.grafana.enable}
|
|
'';
|
|
apply = x: x && cfg.provision.elasticsearch;
|
|
description = lib.mdDoc ''
|
|
Whether the automatically provisioned Elasticsearch
|
|
instance should be added as a grafana datasource. Has no
|
|
effect unless
|
|
[](#opt-services.parsedmarc.provision.elasticsearch)
|
|
is also enabled.
|
|
'';
|
|
};
|
|
|
|
dashboard = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = config.services.grafana.enable;
|
|
defaultText = lib.literalExpression "config.services.grafana.enable";
|
|
description = lib.mdDoc ''
|
|
Whether the official parsedmarc grafana dashboard should
|
|
be provisioned to the local grafana instance.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
settings = lib.mkOption {
|
|
example = lib.literalExpression ''
|
|
{
|
|
imap = {
|
|
host = "imap.example.com";
|
|
user = "alice@example.com";
|
|
password = { _secret = "/run/keys/imap_password" };
|
|
};
|
|
mailbox = {
|
|
watch = true;
|
|
batch_size = 30;
|
|
};
|
|
splunk_hec = {
|
|
url = "https://splunkhec.example.com";
|
|
token = { _secret = "/run/keys/splunk_token" };
|
|
index = "email";
|
|
};
|
|
}
|
|
'';
|
|
description = lib.mdDoc ''
|
|
Configuration parameters to set in
|
|
{file}`parsedmarc.ini`. For a full list of
|
|
available parameters, see
|
|
<https://domainaware.github.io/parsedmarc/#configuration-file>.
|
|
|
|
Settings containing secret data should be set to an attribute
|
|
set containing the attribute `_secret` - a
|
|
string pointing to a file containing the value the option
|
|
should be set to. See the example to get a better picture of
|
|
this: in the resulting {file}`parsedmarc.ini`
|
|
file, the `splunk_hec.token` key will be set
|
|
to the contents of the
|
|
{file}`/run/keys/splunk_token` file.
|
|
'';
|
|
|
|
type = lib.types.submodule {
|
|
freeformType = ini.type;
|
|
|
|
options = {
|
|
general = {
|
|
save_aggregate = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = lib.mdDoc ''
|
|
Save aggregate report data to Elasticsearch and/or Splunk.
|
|
'';
|
|
};
|
|
|
|
save_forensic = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = lib.mdDoc ''
|
|
Save forensic report data to Elasticsearch and/or Splunk.
|
|
'';
|
|
};
|
|
};
|
|
|
|
mailbox = {
|
|
watch = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = lib.mdDoc ''
|
|
Use the IMAP IDLE command to process messages as they arrive.
|
|
'';
|
|
};
|
|
|
|
delete = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = lib.mdDoc ''
|
|
Delete messages after processing them, instead of archiving them.
|
|
'';
|
|
};
|
|
};
|
|
|
|
imap = {
|
|
host = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "localhost";
|
|
description = lib.mdDoc ''
|
|
The IMAP server hostname or IP address.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 993;
|
|
description = lib.mdDoc ''
|
|
The IMAP server port.
|
|
'';
|
|
};
|
|
|
|
ssl = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = lib.mdDoc ''
|
|
Use an encrypted SSL/TLS connection.
|
|
'';
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = with lib.types; nullOr str;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The IMAP server username.
|
|
'';
|
|
};
|
|
|
|
password = lib.mkOption {
|
|
type = with lib.types; nullOr (either path (attrsOf path));
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The IMAP server password.
|
|
|
|
Always handled as a secret whether the value is
|
|
wrapped in a `{ _secret = ...; }`
|
|
attrset or not (refer to [](#opt-services.parsedmarc.settings) for
|
|
details).
|
|
'';
|
|
apply = x: if isAttrs x || x == null then x else { _secret = x; };
|
|
};
|
|
};
|
|
|
|
smtp = {
|
|
host = lib.mkOption {
|
|
type = with lib.types; nullOr str;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The SMTP server hostname or IP address.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = with lib.types; nullOr port;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The SMTP server port.
|
|
'';
|
|
};
|
|
|
|
ssl = lib.mkOption {
|
|
type = with lib.types; nullOr bool;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
Use an encrypted SSL/TLS connection.
|
|
'';
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = with lib.types; nullOr str;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The SMTP server username.
|
|
'';
|
|
};
|
|
|
|
password = lib.mkOption {
|
|
type = with lib.types; nullOr (either path (attrsOf path));
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The SMTP server password.
|
|
|
|
Always handled as a secret whether the value is
|
|
wrapped in a `{ _secret = ...; }`
|
|
attrset or not (refer to [](#opt-services.parsedmarc.settings) for
|
|
details).
|
|
'';
|
|
apply = x: if isAttrs x || x == null then x else { _secret = x; };
|
|
};
|
|
|
|
from = lib.mkOption {
|
|
type = with lib.types; nullOr str;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The `From` address to use for the
|
|
outgoing mail.
|
|
'';
|
|
};
|
|
|
|
to = lib.mkOption {
|
|
type = with lib.types; nullOr (listOf str);
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The addresses to send outgoing mail to.
|
|
'';
|
|
};
|
|
};
|
|
|
|
elasticsearch = {
|
|
hosts = lib.mkOption {
|
|
default = [];
|
|
type = with lib.types; listOf str;
|
|
apply = x: if x == [] then null else lib.concatStringsSep "," x;
|
|
description = lib.mdDoc ''
|
|
A list of Elasticsearch hosts to push parsed reports
|
|
to.
|
|
'';
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = with lib.types; nullOr str;
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
Username to use when connecting to Elasticsearch, if
|
|
required.
|
|
'';
|
|
};
|
|
|
|
password = lib.mkOption {
|
|
type = with lib.types; nullOr (either path (attrsOf path));
|
|
default = null;
|
|
description = lib.mdDoc ''
|
|
The password to use when connecting to Elasticsearch,
|
|
if required.
|
|
|
|
Always handled as a secret whether the value is
|
|
wrapped in a `{ _secret = ...; }`
|
|
attrset or not (refer to [](#opt-services.parsedmarc.settings) for
|
|
details).
|
|
'';
|
|
apply = x: if isAttrs x || x == null then x else { _secret = x; };
|
|
};
|
|
|
|
ssl = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = lib.mdDoc ''
|
|
Whether to use an encrypted SSL/TLS connection.
|
|
'';
|
|
};
|
|
|
|
cert_path = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/etc/ssl/certs/ca-certificates.crt";
|
|
description = lib.mdDoc ''
|
|
The path to a TLS certificate bundle used to verify
|
|
the server's certificate.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
};
|
|
};
|
|
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
|
|
warnings = let
|
|
deprecationWarning = optname: "Starting in 8.0.0, the `${optname}` option has been moved from the `services.parsedmarc.settings.imap`"
|
|
+ "configuration section to the `services.parsedmarc.settings.mailbox` configuration section.";
|
|
hasImapOpt = lib.flip builtins.hasAttr cfg.settings.imap;
|
|
movedOptions = [ "reports_folder" "archive_folder" "watch" "delete" "test" "batch_size" ];
|
|
in builtins.map deprecationWarning (builtins.filter hasImapOpt movedOptions);
|
|
|
|
services.elasticsearch.enable = lib.mkDefault cfg.provision.elasticsearch;
|
|
|
|
services.geoipupdate = lib.mkIf cfg.provision.geoIp {
|
|
enable = true;
|
|
settings = {
|
|
EditionIDs = [
|
|
"GeoLite2-ASN"
|
|
"GeoLite2-City"
|
|
"GeoLite2-Country"
|
|
];
|
|
DatabaseDirectory = "/var/lib/GeoIP";
|
|
};
|
|
};
|
|
|
|
services.dovecot2 = lib.mkIf cfg.provision.localMail.enable {
|
|
enable = true;
|
|
protocols = [ "imap" ];
|
|
};
|
|
|
|
services.postfix = lib.mkIf cfg.provision.localMail.enable {
|
|
enable = true;
|
|
origin = cfg.provision.localMail.hostname;
|
|
config = {
|
|
myhostname = cfg.provision.localMail.hostname;
|
|
mydestination = cfg.provision.localMail.hostname;
|
|
};
|
|
};
|
|
|
|
services.grafana = {
|
|
declarativePlugins = with pkgs.grafanaPlugins;
|
|
lib.mkIf cfg.provision.grafana.dashboard [
|
|
grafana-worldmap-panel
|
|
grafana-piechart-panel
|
|
];
|
|
|
|
provision = {
|
|
enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
|
|
datasources =
|
|
let
|
|
esVersion = lib.getVersion config.services.elasticsearch.package;
|
|
in
|
|
lib.mkIf cfg.provision.grafana.datasource [
|
|
{
|
|
name = "dmarc-ag";
|
|
type = "elasticsearch";
|
|
access = "proxy";
|
|
url = "http://localhost:9200";
|
|
jsonData = {
|
|
timeField = "date_range";
|
|
inherit esVersion;
|
|
};
|
|
}
|
|
{
|
|
name = "dmarc-fo";
|
|
type = "elasticsearch";
|
|
access = "proxy";
|
|
url = "http://localhost:9200";
|
|
jsonData = {
|
|
timeField = "date_range";
|
|
inherit esVersion;
|
|
};
|
|
}
|
|
];
|
|
dashboards = lib.mkIf cfg.provision.grafana.dashboard [{
|
|
name = "parsedmarc";
|
|
options.path = "${pkgs.python3Packages.parsedmarc.dashboard}";
|
|
}];
|
|
};
|
|
};
|
|
|
|
services.parsedmarc.settings = lib.mkMerge [
|
|
(lib.mkIf cfg.provision.elasticsearch {
|
|
elasticsearch = {
|
|
hosts = [ "localhost:9200" ];
|
|
ssl = false;
|
|
};
|
|
})
|
|
(lib.mkIf cfg.provision.localMail.enable {
|
|
imap = {
|
|
host = "localhost";
|
|
port = 143;
|
|
ssl = false;
|
|
user = cfg.provision.localMail.recipientName;
|
|
password = "${pkgs.writeText "imap-password" "@imap-password@"}";
|
|
};
|
|
mailbox = {
|
|
watch = true;
|
|
};
|
|
})
|
|
];
|
|
|
|
systemd.services.parsedmarc =
|
|
let
|
|
# Remove any empty attributes from the config, i.e. empty
|
|
# lists, empty attrsets and null. This makes it possible to
|
|
# list interesting options in `settings` without them always
|
|
# ending up in the resulting config.
|
|
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null [] {} ])) cfg.settings;
|
|
|
|
# Extract secrets (attributes set to an attrset with a
|
|
# "_secret" key) from the settings and generate the commands
|
|
# to run to perform the secret replacements.
|
|
secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig);
|
|
parsedmarcConfig = ini.generate "parsedmarc.ini" filteredConfig;
|
|
mkSecretReplacement = file: ''
|
|
replace-secret ${lib.escapeShellArgs [ (hashString "sha256" file) file "/run/parsedmarc/parsedmarc.ini" ]}
|
|
'';
|
|
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
|
in
|
|
{
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "postfix.service" "dovecot2.service" "elasticsearch.service" ];
|
|
path = with pkgs; [ replace-secret openssl shadow ];
|
|
serviceConfig = {
|
|
ExecStartPre = let
|
|
startPreFullPrivileges = ''
|
|
set -o errexit -o pipefail -o nounset -o errtrace
|
|
shopt -s inherit_errexit
|
|
|
|
umask u=rwx,g=,o=
|
|
cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini
|
|
chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini
|
|
${secretReplacements}
|
|
'' + lib.optionalString cfg.provision.localMail.enable ''
|
|
openssl rand -hex 64 >/run/parsedmarc/dmarc_user_passwd
|
|
replace-secret '@imap-password@' '/run/parsedmarc/dmarc_user_passwd' /run/parsedmarc/parsedmarc.ini
|
|
echo "Setting new randomized password for user '${cfg.provision.localMail.recipientName}'."
|
|
cat <(echo -n "${cfg.provision.localMail.recipientName}:") /run/parsedmarc/dmarc_user_passwd | chpasswd
|
|
'';
|
|
in
|
|
"+${pkgs.writeShellScript "parsedmarc-start-pre-full-privileges" startPreFullPrivileges}";
|
|
Type = "simple";
|
|
User = "parsedmarc";
|
|
Group = "parsedmarc";
|
|
DynamicUser = true;
|
|
RuntimeDirectory = "parsedmarc";
|
|
RuntimeDirectoryMode = "0700";
|
|
CapabilityBoundingSet = "";
|
|
PrivateDevices = true;
|
|
PrivateMounts = true;
|
|
PrivateUsers = true;
|
|
ProtectClock = true;
|
|
ProtectControlGroups = true;
|
|
ProtectHome = true;
|
|
ProtectHostname = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectProc = "invisible";
|
|
ProcSubset = "pid";
|
|
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
|
RestrictRealtime = true;
|
|
RestrictNamespaces = true;
|
|
MemoryDenyWriteExecute = true;
|
|
LockPersonality = true;
|
|
SystemCallArchitectures = "native";
|
|
ExecStart = "${pkgs.python3Packages.parsedmarc}/bin/parsedmarc -c /run/parsedmarc/parsedmarc.ini";
|
|
};
|
|
};
|
|
|
|
users.users.${cfg.provision.localMail.recipientName} = lib.mkIf cfg.provision.localMail.enable {
|
|
isNormalUser = true;
|
|
description = "DMARC mail recipient";
|
|
};
|
|
};
|
|
|
|
meta.doc = ./parsedmarc.md;
|
|
meta.maintainers = [ lib.maintainers.talyz ];
|
|
}
|