nixos/parsedmarc: Improve secret handling

Make secret replacement more robust and futureproof:

- Allow any attribute in `services.parsedmarc.settings` to be a
  secret if set to `{ _secret = "/path/to/secret"; }`.

- Hash secret file paths before using them as a placeholders in the
  config file to minimize the risk of conflicting file paths being
  replaced instead.
This commit is contained in:
talyz 2022-03-31 15:58:26 +02:00
parent 32e057881a
commit 858a0c3fa6
No known key found for this signature in database
GPG key ID: 2DED2151F4671A2B

View file

@ -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
<filename>parsedmarc.ini</filename>. For a full list of
available parameters, see
<link xlink:href="https://domainaware.github.io/parsedmarc/#configuration-file" />.
Settings containing secret data should be set to an attribute
set containing the attribute <literal>_secret</literal> - 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 <filename>parsedmarc.ini</filename>
file, the <literal>splunk_hec.token</literal> key will be set
to the contents of the
<filename>/run/keys/splunk_token</filename> 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 <literal>{ _secret = ...; }</literal>
attrset or not (refer to <xref
linkend="opt-services.parsedmarc.settings" /> 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 <literal>{ _secret = ...; }</literal>
attrset or not (refer to <xref
linkend="opt-services.parsedmarc.settings" /> 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 <literal>{ _secret = ...; }</literal>
attrset or not (refer to <xref
linkend="opt-services.parsedmarc.settings" /> 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