diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
index ae1b2076ad0..efc7f69be7d 100644
--- a/nixos/modules/services/monitoring/parsedmarc.nix
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -3,7 +3,19 @@
let
cfg = config.services.parsedmarc;
opt = options.services.parsedmarc;
- ini = pkgs.formats.ini {};
+ 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 = {
@@ -107,11 +119,35 @@ in
};
settings = lib.mkOption {
+ example = lib.literalExpression ''
+ {
+ imap = {
+ host = "imap.example.com";
+ user = "alice@example.com";
+ password = { _secret = "/run/keys/imap_password" };
+ watch = true;
+ };
+ splunk_hec = {
+ url = "https://splunkhec.example.com";
+ token = { _secret = "/run/keys/splunk_token" };
+ index = "email";
+ };
+ }
+ '';
description = ''
Configuration parameters to set in
parsedmarc.ini. For a full list of
available parameters, see
.
+
+ 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 parsedmarc.ini
+ file, the splunk_hec.token key will be set
+ to the contents of the
+ /run/keys/splunk_token file.
'';
type = lib.types.submodule {
@@ -170,11 +206,18 @@ in
};
password = lib.mkOption {
- type = with lib.types; nullOr path;
+ type = with lib.types; nullOr (either path (attrsOf path));
default = null;
description = ''
- The path to a file containing the IMAP server password.
+ The IMAP server password.
+
+ Always handled as a secret whether the value is
+ wrapped in a { _secret = ...; }
+ attrset or not (refer to for
+ details).
'';
+ apply = x: if isAttrs x || x == null then x else { _secret = x; };
};
watch = lib.mkOption {
@@ -228,11 +271,18 @@ in
};
password = lib.mkOption {
- type = with lib.types; nullOr path;
+ type = with lib.types; nullOr (either path (attrsOf path));
default = null;
description = ''
- The path to a file containing the SMTP server password.
+ The SMTP server password.
+
+ Always handled as a secret whether the value is
+ wrapped in a { _secret = ...; }
+ attrset or not (refer to for
+ details).
'';
+ apply = x: if isAttrs x || x == null then x else { _secret = x; };
};
from = lib.mkOption {
@@ -274,12 +324,19 @@ in
};
password = lib.mkOption {
- type = with lib.types; nullOr path;
+ type = with lib.types; nullOr (either path (attrsOf path));
default = null;
description = ''
- The path to a file containing the password to use when
- connecting to Elasticsearch, if required.
+ 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 for
+ details).
'';
+ apply = x: if isAttrs x || x == null then x else { _secret = x; };
};
ssl = lib.mkOption {
@@ -403,12 +460,17 @@ in
# 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: ! builtins.elem v [ null [] {} ])) cfg.settings;
+ 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:
- lib.optionalString (file != null) ''
- replace-secret '${file}' '${file}' /run/parsedmarc/parsedmarc.ini
- '';
+ mkSecretReplacement = file: ''
+ replace-secret ${lib.escapeShellArgs [ (hashString "sha256" file) file "/run/parsedmarc/parsedmarc.ini" ]}
+ '';
+ secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
in
{
wantedBy = [ "multi-user.target" ];
@@ -423,9 +485,7 @@ in
umask u=rwx,g=,o=
cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini
chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini
- ${mkSecretReplacement cfg.settings.smtp.password}
- ${mkSecretReplacement cfg.settings.imap.password}
- ${mkSecretReplacement cfg.settings.elasticsearch.password}
+ ${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