Loading nixos/lib/utils.nix +137 −38 Original line number Diff line number Diff line Loading @@ -229,7 +229,7 @@ let 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 { _secret = "/path/to/secret" } Loading @@ -237,6 +237,28 @@ let in the attrset replaced with the contents of the file "/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 converted to JSON, this makes it possible to let the user define arbitrary secret values. Loading @@ -245,7 +267,7 @@ let If the file "/path/to/secret" contains the string "topsecretpassword1234", genJqSecretsReplacementSnippet { genJqSecretsReplacement { } { example = [ { irrelevant = "not interesting"; Loading Loading @@ -293,7 +315,7 @@ let { "b": "topsecretpassword5678" } ] genJqSecretsReplacementSnippet { genJqSecretsReplacement { } { example = [ { irrelevant = "not interesting"; Loading Loading @@ -330,12 +352,12 @@ let ] } */ genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret"; # Like genJqSecretsReplacementSnippet, but allows the name of the # attr which identifies the secret to be changed. genJqSecretsReplacementSnippet' = attr: set: output: genJqSecretsReplacement = { attr ? "_secret", loadCredential ? false, }: set: output: let secretsRaw = recursiveGetAttrsetWithJqPrefix set attr; # Set default option values Loading @@ -347,8 +369,23 @@ let // set ) secretsRaw; stringOrDefault = str: def: if str == "" then def else str; # Sanitize path to create a valid credential tag (same as in genLoadCredentialForJqSecretsReplacementSnippet) sanitizePath = path: lib.stringAsChars (c: if builtins.match "[a-zA-Z0-9_.#=!-]" c != null then c else "_") path; # Generate credential tag for a given index and path credentialTag = index: path: "${toString index}_${sanitizePath (secrets.${path}.${attr})}"; credentialPath = index: name: if loadCredential then ''"$CREDENTIALS_DIRECTORY/${credentialTag index name}"'' else "'${secrets.${name}.${attr}}'"; in '' { script = '' if [[ -h '${output}' ]]; then rm '${output}' fi Loading @@ -358,8 +395,12 @@ let shopt -s inherit_errexit '' + concatStringsSep "\n" ( imap1 (index: name: '' secret${toString index}=$(<'${secrets.${name}.${attr}}') imap1 ( index: name: # We keep variable assignment and export separated to avoid masking the return code of the file access. # With `set -e` this will now fail if a file doesn't exist. '' secret${toString index}=$(<${credentialPath index name}) export secret${toString index} '') (attrNames secrets) ) Loading @@ -380,6 +421,64 @@ let (( ! 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. Relies on package names and has quadratic complexity so use with caution! Loading nixos/modules/services/web-apps/immich.nix +23 −41 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ config, lib, pkgs, utils, ... }: let Loading @@ -9,10 +10,9 @@ let format = pkgs.formats.json { }; isPostgresUnixSocket = lib.hasPrefix "/" cfg.database.host; isRedisUnixSocket = lib.hasPrefix "/" cfg.redis.host; # convert a Nix attribute path to jq object identifier-index: # https://jqlang.org/manual/#object-identifier-index attrPathToIndex = attrPath: "." + lib.concatStringsSep "." attrPath; secretsReplacement = utils.genJqSecretsReplacement { loadCredential = true; } cfg.settings "/run/immich/config.json"; commonServiceConfig = { Type = "simple"; Loading Loading @@ -55,6 +55,22 @@ let if cfg.database.enable then config.services.postgresql.package else pkgs.postgresql; in { imports = [ (lib.mkRemovedOptionModule [ "services" "immich" "secretSettings" ] '' `secretSettings` has been deprecated as secrets can now be specified directly in `settings`. To do so, set `_secret` of the desired attribute to a file path, for example: `services.immich.settings.oauth.clientSecret._secret = "/path/to/secret/file";` '' ) ]; options.services.immich = { enable = mkEnableOption "Immich"; package = lib.mkPackageOption pkgs "immich" { }; Loading Loading @@ -128,6 +144,7 @@ in <https://my.immich.app/admin/system-settings> for options and defaults. Setting it to `null` allows configuring Immich in the web interface. You can load secret values from a file in this configuration by setting `somevalue._secret = "/path/to/file"` instead of setting `somevalue` directly. ''; type = types.nullOr ( types.submodule { Loading @@ -151,27 +168,6 @@ in ); }; secretSettings = mkOption { default = { }; description = '' Secrets to to be added to the JSON file generated from {option}`settings`, read from files. ''; example = lib.literalExpression '' { notifications.smtp.transport.password = "/path/to/secret"; oauth.clientSecret = "/path/to/other/secret"; } ''; type = let inherit (types) attrsOf either path; recursiveType = either (attrsOf recursiveType) path // { description = "nested " + (attrsOf path).description; }; in recursiveType; }; machine-learning = { enable = mkEnableOption "immich's machine-learning functionality to detect faces and search for objects" Loading Loading @@ -424,24 +420,10 @@ in postgresqlPackage ]; preStart = mkIf (cfg.settings != null) ( '' cat '${format.generate "immich-config.json" cfg.settings}' > /run/immich/config.json '' + lib.concatStrings ( lib.mapAttrsToListRecursive (attrPath: _: '' tmp="$(mktemp)" ${lib.getExe pkgs.jq} --rawfile secret "$CREDENTIALS_DIRECTORY/${attrPathToIndex attrPath}" \ '${attrPathToIndex attrPath} = ($secret | rtrimstr("\n"))' /run/immich/config.json > "$tmp" mv "$tmp" /run/immich/config.json '') cfg.secretSettings ) ); preStart = mkIf (cfg.settings != null) secretsReplacement.script; serviceConfig = commonServiceConfig // { LoadCredential = lib.mapAttrsToListRecursive ( attrPath: file: "${attrPathToIndex attrPath}:${file}" ) cfg.secretSettings; LoadCredential = secretsReplacement.credentials; ExecStart = lib.getExe cfg.package; EnvironmentFile = mkIf (cfg.secretsFile != null) cfg.secretsFile; Slice = "system-immich.slice"; Loading nixos/tests/web-apps/immich.nix +7 −7 Original line number Diff line number Diff line Loading @@ -17,14 +17,14 @@ services.immich = { enable = true; environment.IMMICH_LOG_LEVEL = "verbose"; settings.backup.database = { settings = { backup.database = { enabled = true; cronExpression = "invalid"; # Test loading secrets from files: cronExpression._secret = "${pkgs.writeText "cron" "0 02 * * *"}"; }; secretSettings = { backup.database.cronExpression = "${pkgs.writeText "cron" "0 02 * * *"}"; # thanks to LoadCredential files only readable by root should work notifications.smtp.transport.password = "/etc/shadow"; notifications.smtp.transport.password._secret = "/etc/shadow"; }; }; Loading Loading
nixos/lib/utils.nix +137 −38 Original line number Diff line number Diff line Loading @@ -229,7 +229,7 @@ let 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 { _secret = "/path/to/secret" } Loading @@ -237,6 +237,28 @@ let in the attrset replaced with the contents of the file "/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 converted to JSON, this makes it possible to let the user define arbitrary secret values. Loading @@ -245,7 +267,7 @@ let If the file "/path/to/secret" contains the string "topsecretpassword1234", genJqSecretsReplacementSnippet { genJqSecretsReplacement { } { example = [ { irrelevant = "not interesting"; Loading Loading @@ -293,7 +315,7 @@ let { "b": "topsecretpassword5678" } ] genJqSecretsReplacementSnippet { genJqSecretsReplacement { } { example = [ { irrelevant = "not interesting"; Loading Loading @@ -330,12 +352,12 @@ let ] } */ genJqSecretsReplacementSnippet = genJqSecretsReplacementSnippet' "_secret"; # Like genJqSecretsReplacementSnippet, but allows the name of the # attr which identifies the secret to be changed. genJqSecretsReplacementSnippet' = attr: set: output: genJqSecretsReplacement = { attr ? "_secret", loadCredential ? false, }: set: output: let secretsRaw = recursiveGetAttrsetWithJqPrefix set attr; # Set default option values Loading @@ -347,8 +369,23 @@ let // set ) secretsRaw; stringOrDefault = str: def: if str == "" then def else str; # Sanitize path to create a valid credential tag (same as in genLoadCredentialForJqSecretsReplacementSnippet) sanitizePath = path: lib.stringAsChars (c: if builtins.match "[a-zA-Z0-9_.#=!-]" c != null then c else "_") path; # Generate credential tag for a given index and path credentialTag = index: path: "${toString index}_${sanitizePath (secrets.${path}.${attr})}"; credentialPath = index: name: if loadCredential then ''"$CREDENTIALS_DIRECTORY/${credentialTag index name}"'' else "'${secrets.${name}.${attr}}'"; in '' { script = '' if [[ -h '${output}' ]]; then rm '${output}' fi Loading @@ -358,8 +395,12 @@ let shopt -s inherit_errexit '' + concatStringsSep "\n" ( imap1 (index: name: '' secret${toString index}=$(<'${secrets.${name}.${attr}}') imap1 ( index: name: # We keep variable assignment and export separated to avoid masking the return code of the file access. # With `set -e` this will now fail if a file doesn't exist. '' secret${toString index}=$(<${credentialPath index name}) export secret${toString index} '') (attrNames secrets) ) Loading @@ -380,6 +421,64 @@ let (( ! 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. Relies on package names and has quadratic complexity so use with caution! Loading
nixos/modules/services/web-apps/immich.nix +23 −41 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ config, lib, pkgs, utils, ... }: let Loading @@ -9,10 +10,9 @@ let format = pkgs.formats.json { }; isPostgresUnixSocket = lib.hasPrefix "/" cfg.database.host; isRedisUnixSocket = lib.hasPrefix "/" cfg.redis.host; # convert a Nix attribute path to jq object identifier-index: # https://jqlang.org/manual/#object-identifier-index attrPathToIndex = attrPath: "." + lib.concatStringsSep "." attrPath; secretsReplacement = utils.genJqSecretsReplacement { loadCredential = true; } cfg.settings "/run/immich/config.json"; commonServiceConfig = { Type = "simple"; Loading Loading @@ -55,6 +55,22 @@ let if cfg.database.enable then config.services.postgresql.package else pkgs.postgresql; in { imports = [ (lib.mkRemovedOptionModule [ "services" "immich" "secretSettings" ] '' `secretSettings` has been deprecated as secrets can now be specified directly in `settings`. To do so, set `_secret` of the desired attribute to a file path, for example: `services.immich.settings.oauth.clientSecret._secret = "/path/to/secret/file";` '' ) ]; options.services.immich = { enable = mkEnableOption "Immich"; package = lib.mkPackageOption pkgs "immich" { }; Loading Loading @@ -128,6 +144,7 @@ in <https://my.immich.app/admin/system-settings> for options and defaults. Setting it to `null` allows configuring Immich in the web interface. You can load secret values from a file in this configuration by setting `somevalue._secret = "/path/to/file"` instead of setting `somevalue` directly. ''; type = types.nullOr ( types.submodule { Loading @@ -151,27 +168,6 @@ in ); }; secretSettings = mkOption { default = { }; description = '' Secrets to to be added to the JSON file generated from {option}`settings`, read from files. ''; example = lib.literalExpression '' { notifications.smtp.transport.password = "/path/to/secret"; oauth.clientSecret = "/path/to/other/secret"; } ''; type = let inherit (types) attrsOf either path; recursiveType = either (attrsOf recursiveType) path // { description = "nested " + (attrsOf path).description; }; in recursiveType; }; machine-learning = { enable = mkEnableOption "immich's machine-learning functionality to detect faces and search for objects" Loading Loading @@ -424,24 +420,10 @@ in postgresqlPackage ]; preStart = mkIf (cfg.settings != null) ( '' cat '${format.generate "immich-config.json" cfg.settings}' > /run/immich/config.json '' + lib.concatStrings ( lib.mapAttrsToListRecursive (attrPath: _: '' tmp="$(mktemp)" ${lib.getExe pkgs.jq} --rawfile secret "$CREDENTIALS_DIRECTORY/${attrPathToIndex attrPath}" \ '${attrPathToIndex attrPath} = ($secret | rtrimstr("\n"))' /run/immich/config.json > "$tmp" mv "$tmp" /run/immich/config.json '') cfg.secretSettings ) ); preStart = mkIf (cfg.settings != null) secretsReplacement.script; serviceConfig = commonServiceConfig // { LoadCredential = lib.mapAttrsToListRecursive ( attrPath: file: "${attrPathToIndex attrPath}:${file}" ) cfg.secretSettings; LoadCredential = secretsReplacement.credentials; ExecStart = lib.getExe cfg.package; EnvironmentFile = mkIf (cfg.secretsFile != null) cfg.secretsFile; Slice = "system-immich.slice"; Loading
nixos/tests/web-apps/immich.nix +7 −7 Original line number Diff line number Diff line Loading @@ -17,14 +17,14 @@ services.immich = { enable = true; environment.IMMICH_LOG_LEVEL = "verbose"; settings.backup.database = { settings = { backup.database = { enabled = true; cronExpression = "invalid"; # Test loading secrets from files: cronExpression._secret = "${pkgs.writeText "cron" "0 02 * * *"}"; }; secretSettings = { backup.database.cronExpression = "${pkgs.writeText "cron" "0 02 * * *"}"; # thanks to LoadCredential files only readable by root should work notifications.smtp.transport.password = "/etc/shadow"; notifications.smtp.transport.password._secret = "/etc/shadow"; }; }; Loading