nixos/utils: Add support for LoadCredential= with genJqSecretsReplacementSnippet

This commit is contained in:
oddlama
2025-10-18 12:29:18 +02:00
parent 8bbaa38321
commit b615f1825d

View File

@@ -229,7 +229,7 @@ let
listToAttrs (flatten (recurse "." item)); listToAttrs (flatten (recurse "." item));
/* /*
Takes an attrset and a file path and generates a bash snippet that Takes some options, an attrset and a file path and generates a bash snippet that
outputs a JSON file at the file path with all instances of outputs a JSON file at the file path with all instances of
{ _secret = "/path/to/secret" } { _secret = "/path/to/secret" }
@@ -237,6 +237,28 @@ let
in the attrset replaced with the contents of the file in the attrset replaced with the contents of the file
"/path/to/secret" in the output JSON. "/path/to/secret" in the output JSON.
The first argument exposes the following options:
- attr: The name of the secret attribute that will be processed, defaults to "_secret"
- loadCredential: A boolean determining whether the script should load secrets directly (false)
or load them from $CREDENTIALS_DIRECTORY (true). In the latter case the output attribute set
will contain a .credentials attribute with the necessary credential list that can be passed
to systemd's `LoadCredential=` option.
The output of this utility is an attribute set containing the main script and optionally
a list of credentials:
{
# The main script
script = "...";
# If the loadCredential option was set:
credentials = [
"secret1:/path/to/secret1"
#...
];
}
When a configuration option accepts an attrset that is finally When a configuration option accepts an attrset that is finally
converted to JSON, this makes it possible to let the user define converted to JSON, this makes it possible to let the user define
arbitrary secret values. arbitrary secret values.
@@ -245,7 +267,7 @@ let
If the file "/path/to/secret" contains the string If the file "/path/to/secret" contains the string
"topsecretpassword1234", "topsecretpassword1234",
genJqSecretsReplacementSnippet { genJqSecretsReplacement { } {
example = [ example = [
{ {
irrelevant = "not interesting"; irrelevant = "not interesting";
@@ -293,7 +315,7 @@ let
{ "b": "topsecretpassword5678" } { "b": "topsecretpassword5678" }
] ]
genJqSecretsReplacementSnippet { genJqSecretsReplacement { } {
example = [ example = [
{ {
irrelevant = "not interesting"; irrelevant = "not interesting";
@@ -330,12 +352,12 @@ let
] ]
} }
*/ */
genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret"; genJqSecretsReplacement =
{
# Like genJqSecretsReplacementSnippet, but allows the name of the attr ? "_secret",
# attr which identifies the secret to be changed. loadCredential ? false,
genJqSecretsReplacementSnippet' = }:
attr: set: output: set: output:
let let
secretsRaw = recursiveGetAttrsetWithJqPrefix set attr; secretsRaw = recursiveGetAttrsetWithJqPrefix set attr;
# Set default option values # Set default option values
@@ -347,38 +369,115 @@ let
// set // set
) secretsRaw; ) secretsRaw;
stringOrDefault = str: def: if str == "" then def else str; stringOrDefault = str: def: if str == "" then def else str;
in
''
if [[ -h '${output}' ]]; then
rm '${output}'
fi
inherit_errexit_enabled=0 # Sanitize path to create a valid credential tag (same as in genLoadCredentialForJqSecretsReplacementSnippet)
shopt -pq inherit_errexit && inherit_errexit_enabled=1 sanitizePath =
shopt -s inherit_errexit path: lib.stringAsChars (c: if builtins.match "[a-zA-Z0-9_.#=!-]" c != null then c else "_") path;
''
+ concatStringsSep "\n" ( # Generate credential tag for a given index and path
imap1 (index: name: '' credentialTag = index: path: "${toString index}_${sanitizePath (secrets.${path}.${attr})}";
secret${toString index}=$(<'${secrets.${name}.${attr}}')
export secret${toString index} credentialPath =
'') (attrNames secrets) index: name:
) if loadCredential then
+ "\n" ''"$CREDENTIALS_DIRECTORY/${credentialTag index name}"''
+ "${pkgs.jq}/bin/jq >'${output}' " else
+ escapeShellArg ( "'${secrets.${name}.${attr}}'";
stringOrDefault (concatStringsSep " | " ( in
{
script = ''
if [[ -h '${output}' ]]; then
rm '${output}'
fi
inherit_errexit_enabled=0
shopt -pq inherit_errexit && inherit_errexit_enabled=1
shopt -s inherit_errexit
''
+ concatStringsSep "\n" (
imap1 ( imap1 (
index: name: index: name:
''${name} = ($ENV.secret${toString index}${optionalString (!secrets.${name}.quote) " | fromjson"})'' # We keep variable assignment and export separated to avoid masking the return code of the file access.
) (attrNames secrets) # With `set -e` this will now fail if a file doesn't exist.
)) "." ''
) secret${toString index}=$(<${credentialPath index name})
+ '' export secret${toString index}
<<'EOF' '') (attrNames secrets)
${toJSON set} )
EOF + "\n"
(( ! inherit_errexit_enabled )) && shopt -u inherit_errexit + "${pkgs.jq}/bin/jq >'${output}' "
''; + escapeShellArg (
stringOrDefault (concatStringsSep " | " (
imap1 (
index: name:
''${name} = ($ENV.secret${toString index}${optionalString (!secrets.${name}.quote) " | fromjson"})''
) (attrNames secrets)
)) "."
)
+ ''
<<'EOF'
${toJSON set}
EOF
(( ! inherit_errexit_enabled )) && shopt -u inherit_errexit
'';
/*
Generates a list of systemd LoadCredential entries if loadCredential was set,
otherwise returns null.
The tag is sanitized to only contain characters a-zA-Z0-9_-.#=! and prefixed
with an index to ensure uniqueness.
Example:
genLoadCredentialForJqSecretsReplacementSnippet { } {
example = {
secret1 = { _secret = "/path/to/secret"; };
secret2 = { _secret = "/another/secret"; };
};
}
-> [ "0_path_to_secret:/path/to/secret" "1_another_secret:/another/secret" ]
*/
credentials =
if loadCredential then
imap1 (
index: path:
"${toString index}_${sanitizePath (secretsRaw.${path}.${attr})}:${secretsRaw.${path}.${attr}}"
) (attrNames secretsRaw)
else
null;
};
/*
A convenience function around `genJqSecretsReplacement` without any additional
settings that returns just the script that does the secret replacing. Make sure
to have a look at `genJqSecretsReplacement` first to decide whether you need
the additional functionality.
Example:
If the file "/path/to/secret" contains the string
"topsecretpassword1234",
genJqSecretsReplacementSnippet {
example = [
{
irrelevant = "not interesting";
}
{
ignored = "ignored attr";
relevant = {
secret = {
_secret = "/path/to/secret";
};
};
}
];
} "/path/to/output.json"
will return a set of bash commands that replaces the secret values
in the given attrset with values from the respective files and saves the result
as a JSON file.
*/
genJqSecretsReplacementSnippet = set: output: (genJqSecretsReplacement { } set output).script;
/* /*
Remove packages of packagesToRemove from packages, based on their names. Remove packages of packagesToRemove from packages, based on their names.