diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index 5fd672fe3d6499..6d5ae4db7d0f5f 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -37,19 +37,14 @@ let devices = let folderDevices = folder.devices; in - if builtins.isList folderDevices then - map (device: - if builtins.isString device then - { deviceId = cfg.settings.devices.${device}.id; } - else - device - ) folderDevices - else if builtins.isAttrs folderDevices then - mapAttrsToList (deviceName: deviceValue: - deviceValue // { deviceId = cfg.settings.devices.${deviceName}.id; } - ) folderDevices - else - throw "Invalid type for devices in folder '${folderName}'; expected list or attrset."; + map (device: + if builtins.isString device then + { deviceId = cfg.settings.devices.${device}.id; } + else if builtins.isAttrs device then + { deviceId = cfg.settings.devices.${device.name}.id; } // device + else + throw "Invalid type for devices in folder '${folderName}'; expected list or attrset." + ) folderDevices; }) (filterAttrs (_: folder: folder.enable ) cfg.settings.folders); @@ -115,62 +110,41 @@ let # afterwards. (map (new_cfg: let - isSecret = attr: value: builtins.isString value && attr == "encryptionPassword"; - - resolveSecrets = attr: value: - if builtins.isAttrs value then - # Attribute set: process each attribute - builtins.mapAttrs (name: val: resolveSecrets name val) value - else if builtins.isList value then - # List: process each element - map (item: resolveSecrets "" item) value - else if isSecret attr value then - # String that looks like a path: replace with placeholder + jsonPreSecrets = pkgs.writeTextFile { + name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json"; + text = builtins.toJSON new_cfg; + }; + injectSecrtsJqCmd = ( + if conf_type == "dirs" then let - varName = "secret_${builtins.hashString "sha256" value}"; + folder = new_cfg; + devicesWithSecrets = lib.pipe folder.devices [ + (lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile)) + (map (device: { + deviceId = device.deviceId; + variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}"; + secretPath = device.encryptionPasswordFile; + })) + ]; + jqUpdates = map (device: '' + .devices[] |= ( + if .deviceId == "${device.deviceId}" then + del(.encryptionPasswordFile) | + .encryptionPassword = ''$${device.variableName} + else + . + end + ) + '') devicesWithSecrets; + jqRawFiles = map (device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}") devicesWithSecrets; in - "\${${varName}}" - else - # Other types: return as is - value; - - # Function to collect all file paths from the configuration - collectPaths = attr: value: - if builtins.isAttrs value then - concatMap (name: collectPaths name value.${name}) (builtins.attrNames value) - else if builtins.isList value then - concatMap (name: collectPaths "" name) value - else if isSecret attr value then - [ value ] + "${jq} ${lib.concatStringsSep " " jqRawFiles} ${lib.escapeShellArg (lib.concatStringsSep "|" (["."] ++ jqUpdates))}" else - []; - - # Function to generate variable assignments for the secrets - generateSecretVars = paths: - concatStringsSep "\n" (map (path: - let - varName = "secret_${builtins.hashString "sha256" path}"; - in - '' - if [ ! -r ${path} ]; then - echo "${path} does not exist" - exit 1 - fi - ${varName}=$(<${path}) - '' - ) paths); - - resolved_cfg = resolveSecrets "" new_cfg; - secretPaths = collectPaths "" new_cfg; - secretVarsScript = generateSecretVars secretPaths; - - jsonString = builtins.toJSON resolved_cfg; - escapedJson = builtins.replaceStrings ["\""] ["\\\""] jsonString; + "${jq} ." + ); in '' - ${secretVarsScript} - - curl -d "${escapedJson}" -X POST ${s.baseAddress} + ${injectSecrtsJqCmd} ${jsonPreSecrets} | curl --json @- -X POST ${s.baseAddress} '' )) (lib.concatStringsSep "\n") @@ -445,30 +419,43 @@ in { }; devices = mkOption { - type = types.oneOf [ - (types.listOf types.str) - (types.attrsOf (types.submodule ({ name, ... }: { - freeformType = settingsFormat.type; - options = { - encryptionPassword = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Path to encryption password. If set, the file will be read during - service activation, without being embedded in derivation. - ''; + type = types.listOf ( + types.oneOf [ + types.str + (types.submodule ({ ... }: { + freeformType = settingsFormat.type; + options = { + name = mkOption { + type = types.str; + default = null; + description = '' + The name of a device defined in the + [devices](#opt-services.syncthing.settings.devices) + option. + ''; + }; + encryptionPasswordFile = mkOption { + type = types.nullOr (types.pathWith { + inStore = false; + absolute = true; + }); + default = null; + description = '' + Path to encryption password. If set, the file will be read during + service activation, without being embedded in derivation. + ''; + }; }; - }; - })) - )]; + })) + ] + ); default = []; description = '' The devices this folder should be shared with. Each device must be defined in the [devices](#opt-services.syncthing.settings.devices) option. - Either a list of strings, or an attribute set, where keys are defined in the - [devices](#opt-services.syncthing.settings.devices) option, and values are - device configurations. + A list of either strings or attribute sets, where values + are device names or device configurations. ''; }; diff --git a/nixos/tests/syncthing-folders.nix b/nixos/tests/syncthing-folders.nix index 46985b4709a698..4a6a5717b95228 100644 --- a/nixos/tests/syncthing-folders.nix +++ b/nixos/tests/syncthing-folders.nix @@ -1,4 +1,4 @@ -import ../make-test-python.nix ( +import ./make-test-python.nix ( { lib, pkgs, ... }: let genNodeId = @@ -11,53 +11,69 @@ import ../make-test-python.nix ( idA = genNodeId "a"; idB = genNodeId "b"; idC = genNodeId "c"; - testPasswordFile = pkgs.writeText "syncthing-test-password" "it's a secret"; + testPassword = "it's a secret"; in { name = "syncthing"; meta.maintainers = with pkgs.lib.maintainers; [ zarelit ]; nodes = { - a = { - services.syncthing = { - enable = true; - openDefaultPorts = true; - cert = "${idA}/cert.pem"; - key = "${idA}/key.pem"; - settings = { - devices.b.id = lib.fileContents "${idB}/id"; - devices.c.id = lib.fileContents "${idC}/id"; - folders.foo = { - path = "/var/lib/syncthing/foo"; - devices = [ "b" ]; - }; - folders.bar = { - path = "/var/lib/syncthing/bar"; - devices.c.encryptionPassword = "${testPasswordFile}"; + a = + { config, ... }: + { + environment.etc.bar-encryption-password.text = testPassword; + services.syncthing = { + enable = true; + openDefaultPorts = true; + cert = "${idA}/cert.pem"; + key = "${idA}/key.pem"; + settings = { + devices.b.id = lib.fileContents "${idB}/id"; + devices.c.id = lib.fileContents "${idC}/id"; + folders.foo = { + path = "/var/lib/syncthing/foo"; + devices = [ "b" ]; + }; + folders.bar = { + path = "/var/lib/syncthing/bar"; + devices = [ + { + name = "c"; + encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}"; + } + ]; + }; }; }; }; - }; - b = { - services.syncthing = { - enable = true; - openDefaultPorts = true; - cert = "${idB}/cert.pem"; - key = "${idB}/key.pem"; - settings = { - devices.a.id = lib.fileContents "${idA}/id"; - devices.c.id = lib.fileContents "${idC}/id"; - folders.foo = { - path = "/var/lib/syncthing/foo"; - devices = [ "a" ]; - }; - folders.bar = { - path = "/var/lib/syncthing/bar"; - devices.c.encryptionPassword = "${testPasswordFile}"; + b = + { config, ... }: + { + environment.etc.bar-encryption-password.text = testPassword; + services.syncthing = { + enable = true; + openDefaultPorts = true; + cert = "${idB}/cert.pem"; + key = "${idB}/key.pem"; + settings = { + devices.a.id = lib.fileContents "${idA}/id"; + devices.c.id = lib.fileContents "${idC}/id"; + folders.foo = { + path = "/var/lib/syncthing/foo"; + devices = [ "a" ]; + }; + folders.bar = { + path = "/var/lib/syncthing/bar"; + devices = [ + { + name = "c"; + encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}"; + } + ]; + }; }; }; }; - }; c = { services.syncthing = { enable = true; @@ -111,10 +127,10 @@ import ../make-test-python.nix ( # B should be able to decrypt, check that content of file matches b.wait_for_file("/var/lib/syncthing/bar/plainname") - b.succeed("grep plaincontent /var/lib/syncthing/bar/plainname") + assert "plaincontent" == b.succeed("cat /var/lib/syncthing/bar/plainname") # Bar on C is untrusted, check that content is not in cleartext - c.fail("grep -R plaincontent /var/lib/syncthing/bar") + assert "plaincontent" not in c.succeed("cat /var/lib/syncthing/bar") ''; } )