From 848e754b8156a878050dd12b54e0c12b454fdf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Sun, 15 Sep 2024 21:10:12 +0100 Subject: [PATCH 1/6] syncthing: handle encryptionPassword secret Rewrite the syncthing config update script to embed secrets into the json request. Specifically, we handle the `encryptionPassword` secret. With this code, the user can embed path to the encrpyption password for a given device the folder is shared with, and have it loaded in, without touching the nix store. --- .../modules/services/networking/syncthing.nix | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index f76509d78b69..6e3150891250 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -128,9 +128,73 @@ let # don't exist in the array given. That's why we use here `POST`, and # only if s.override == true then we DELETE the relevant folders # afterwards. - (map (new_cfg: '' - curl -d ${lib.escapeShellArg (builtins.toJSON new_cfg)} -X POST ${s.baseAddress} - '')) + (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 + let + varName = "secret_${builtins.hashString "sha256" value}"; + 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 ] + 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; + in + '' + ${secretVarsScript} + + curl -d "${escapedJson}" -X POST ${s.baseAddress} + '' + )) (lib.concatStringsSep "\n") ] /* From d8b850d88f416c1d8da0ba6c194939fcf182695a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Sun, 15 Sep 2024 21:10:12 +0100 Subject: [PATCH 2/6] syncthing: expose encryptionPassword - Change `folder.devices` type into `oneOf [(listOf str) (attrsOf (submodule { ... }))]`. - Expose `encryptionPassord` within the attrSet of the devices option. This allows the user to set the encrpyption password use to share the folder's data with. We do this by file path, as opposed to string literal, because we do not want to embed the encrpyption password into the nix store. --- .../modules/services/networking/syncthing.nix | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index 6e3150891250..478882cd9b89 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -55,10 +55,21 @@ let were removed. Please use, respectively, {rescanIntervalS,fsWatcherEnabled,fsWatcherDelayS} instead. '' { - devices = map ( - device: - if builtins.isString device then { deviceId = cfg.settings.devices.${device}.id; } else device - ) folder.devices; + 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."; } ) (filterAttrs (_: folder: folder.enable) cfg.settings.folders); @@ -502,11 +513,35 @@ in }; devices = mkOption { - type = types.listOf types.str; + 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. + ''; + }; + }; + } + ) + )) + ]; 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. ''; }; From a8cd913df279463f29b43526487d08800d473ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Thu, 19 Sep 2024 16:36:23 +0100 Subject: [PATCH 3/6] nixosTests.syncthing: define test for declarative folders --- nixos/tests/all-tests.nix | 1 + nixos/tests/syncthing-folders.nix | 69 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 nixos/tests/syncthing-folders.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index d1fe552bbc1f..5c81a9cb3ece 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1219,6 +1219,7 @@ in syncthing-no-settings = handleTest ./syncthing-no-settings.nix { }; syncthing-init = handleTest ./syncthing-init.nix { }; syncthing-many-devices = handleTest ./syncthing-many-devices.nix { }; + syncthing-folders = handleTest ./syncthing-folders.nix { }; syncthing-relay = handleTest ./syncthing-relay.nix { }; sysinit-reactivation = runTest ./sysinit-reactivation.nix; systemd = handleTest ./systemd.nix { }; diff --git a/nixos/tests/syncthing-folders.nix b/nixos/tests/syncthing-folders.nix new file mode 100644 index 000000000000..5914aacf0b2a --- /dev/null +++ b/nixos/tests/syncthing-folders.nix @@ -0,0 +1,69 @@ +import ../make-test-python.nix ( + { lib, pkgs, ... }: + let + genNodeId = + name: + pkgs.runCommand "syncthing-test-certs-${name}" { } '' + mkdir -p $out + ${pkgs.syncthing}/bin/syncthing generate --config=$out + ${pkgs.libxml2}/bin/xmllint --xpath 'string(configuration/device/@id)' $out/config.xml > $out/id + ''; + idA = genNodeId "a"; + idB = genNodeId "b"; + 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"; + }; + folders.foo = { + path = "/var/lib/syncthing/foo"; + devices = [ "b" ]; + }; + }; + }; + }; + b = { + services.syncthing = { + enable = true; + openDefaultPorts = true; + cert = "${idB}/cert.pem"; + key = "${idB}/key.pem"; + settings = { + devices.a = { + id = lib.fileContents "${idA}/id"; + }; + folders.foo = { + path = "/var/lib/syncthing/foo"; + devices = [ "a" ]; + }; + }; + }; + }; + }; + + testScript = '' + start_all() + a.wait_for_unit("syncthing.service") + b.wait_for_unit("syncthing.service") + a.wait_for_open_port(22000) + b.wait_for_open_port(22000) + a.wait_for_file("/var/lib/syncthing/foo") + b.wait_for_file("/var/lib/syncthing/foo") + a.succeed("echo a2b > /var/lib/syncthing/foo/a2b") + b.succeed("echo b2a > /var/lib/syncthing/foo/b2a") + a.wait_for_file("/var/lib/syncthing/foo/b2a") + b.wait_for_file("/var/lib/syncthing/foo/a2b") + ''; + } +) From b7f1fd5caa1ea4aee3335be3240ec23610e1285e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Thu, 19 Sep 2024 16:51:11 +0100 Subject: [PATCH 4/6] nixosTests.syncthing: create encrypted device test --- nixos/tests/syncthing-folders.nix | 63 ++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/nixos/tests/syncthing-folders.nix b/nixos/tests/syncthing-folders.nix index 5914aacf0b2a..46985b4709a6 100644 --- a/nixos/tests/syncthing-folders.nix +++ b/nixos/tests/syncthing-folders.nix @@ -10,6 +10,8 @@ import ../make-test-python.nix ( ''; idA = genNodeId "a"; idB = genNodeId "b"; + idC = genNodeId "c"; + testPasswordFile = pkgs.writeText "syncthing-test-password" "it's a secret"; in { name = "syncthing"; @@ -23,13 +25,16 @@ import ../make-test-python.nix ( cert = "${idA}/cert.pem"; key = "${idA}/key.pem"; settings = { - devices.b = { - id = lib.fileContents "${idB}/id"; - }; + 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}"; + }; }; }; }; @@ -40,13 +45,36 @@ import ../make-test-python.nix ( cert = "${idB}/cert.pem"; key = "${idB}/key.pem"; settings = { - devices.a = { - id = lib.fileContents "${idA}/id"; - }; + 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}"; + }; + }; + }; + }; + c = { + services.syncthing = { + enable = true; + openDefaultPorts = true; + cert = "${idC}/cert.pem"; + key = "${idC}/key.pem"; + settings = { + devices.a.id = lib.fileContents "${idA}/id"; + devices.b.id = lib.fileContents "${idB}/id"; + folders.bar = { + path = "/var/lib/syncthing/bar"; + devices = [ + "a" + "b" + ]; + type = "receiveencrypted"; + }; }; }; }; @@ -54,16 +82,39 @@ import ../make-test-python.nix ( testScript = '' start_all() + a.wait_for_unit("syncthing.service") b.wait_for_unit("syncthing.service") + c.wait_for_unit("syncthing.service") a.wait_for_open_port(22000) b.wait_for_open_port(22000) + c.wait_for_open_port(22000) + + # Test foo + a.wait_for_file("/var/lib/syncthing/foo") b.wait_for_file("/var/lib/syncthing/foo") + a.succeed("echo a2b > /var/lib/syncthing/foo/a2b") b.succeed("echo b2a > /var/lib/syncthing/foo/b2a") + a.wait_for_file("/var/lib/syncthing/foo/b2a") b.wait_for_file("/var/lib/syncthing/foo/a2b") + + # Test bar + + a.wait_for_file("/var/lib/syncthing/bar") + b.wait_for_file("/var/lib/syncthing/bar") + c.wait_for_file("/var/lib/syncthing/bar") + + a.succeed("echo plaincontent > /var/lib/syncthing/bar/plainname") + + # 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") + + # Bar on C is untrusted, check that content is not in cleartext + c.fail("grep -R plaincontent /var/lib/syncthing/bar") ''; } ) From 715ba701fa4a4e48f2e2bdae81d25c7274d9cc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Mon, 30 Sep 2024 10:46:00 +0100 Subject: [PATCH 5/6] doc: 24.11: release note for syncthing changes --- nixos/doc/manual/release-notes/rl-2411.section.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index fc335a19f4ef..cd6c7fcdb5bc 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -933,6 +933,8 @@ - `buildNimSbom` was added as an alternative to `buildNimPackage`. `buildNimSbom` uses [SBOMs](https://cyclonedx.org/) to generate packages whereas `buildNimPackage` uses a custom JSON lockfile format. +- `services.syncthing.folders..devices` now accepts an `attrset`, allowing to set `encryptionPassword` file for a device. + ## Detailed Migration Information {#sec-release-24.11-migration} ### `sound` options removal {#sec-release-24.11-migration-sound} From 1b7b89c4efc7e7338cc68eb0e34db94a43bd2200 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Sun, 12 Jan 2025 19:17:54 -0800 Subject: [PATCH 6/6] Demonstration of an alternate way to embed secrets into syncthing config --- .../manual/release-notes/rl-2411.section.md | 2 - .../modules/services/networking/syncthing.nix | 181 ++++++++++-------- nixos/tests/all-tests.nix | 2 +- nixos/tests/syncthing-folders.nix | 151 ++++++++------- 4 files changed, 183 insertions(+), 153 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index cd6c7fcdb5bc..fc335a19f4ef 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -933,8 +933,6 @@ - `buildNimSbom` was added as an alternative to `buildNimPackage`. `buildNimSbom` uses [SBOMs](https://cyclonedx.org/) to generate packages whereas `buildNimPackage` uses a custom JSON lockfile format. -- `services.syncthing.folders..devices` now accepts an `attrset`, allowing to set `encryptionPassword` file for a device. - ## Detailed Migration Information {#sec-release-24.11-migration} ### `sound` options removal {#sec-release-24.11-migration-sound} diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index 478882cd9b89..0a901148dc9c 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -59,17 +59,15 @@ let 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); @@ -142,68 +140,74 @@ let (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 - let - varName = "secret_${builtins.hashString "sha256" value}"; - 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 ] - else - [ ]; - - # Function to generate variable assignments for the secrets - generateSecretVars = - paths: - concatStringsSep "\n" ( - map ( - path: + jsonPreSecretsFile = pkgs.writeTextFile { + name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json"; + text = builtins.toJSON new_cfg; + }; + injectSecretsJqCmd = + { + # There are no secrets in `devs`, so no massaging needed. + "devs" = "${jq} ."; + "dirs" = let - varName = "secret_${builtins.hashString "sha256" path}"; + 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; + })) + ]; + # At this point, `jsonPreSecretsFile` looks something like this: + # + # { + # ..., + # "devices": [ + # { + # "deviceId": "id1", + # "encryptionPasswordFile": "/etc/bar-encryption-password", + # "name": "..." + # } + # ], + # } + # + # We now generate a `jq` command that can replace those + # `encryptionPasswordFile`s with `encryptionPassword`. + # The `jq` command ends up looking like this: + # + # jq --rawfile secret_DEADBEEF /etc/bar-encryption-password ' + # .devices[] |= ( + # if .deviceId == "id1" then + # del(.encryptionPasswordFile) | + # .encryptionPassword = $secret_DEADBEEF + # else + # . + # end + # ) + # ' + 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 - '' - 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} ${lib.concatStringsSep " " jqRawFiles} ${ + lib.escapeShellArg (lib.concatStringsSep "|" ([ "." ] ++ jqUpdates)) + }"; + } + .${conf_type}; in '' - ${secretVarsScript} - - curl -d "${escapedJson}" -X POST ${s.baseAddress} + ${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress} '' )) (lib.concatStringsSep "\n") @@ -513,16 +517,30 @@ in }; devices = mkOption { - type = types.oneOf [ - (types.listOf types.str) - (types.attrsOf ( - types.submodule ( - { name, ... }: + type = types.listOf ( + types.oneOf [ + types.str + (types.submodule ( + { ... }: { freeformType = settingsFormat.type; options = { - encryptionPassword = mkOption { - type = types.nullOr types.str; + 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 @@ -531,17 +549,16 @@ in }; }; } - ) - )) - ]; + )) + ] + ); 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/all-tests.nix b/nixos/tests/all-tests.nix index 5c81a9cb3ece..268d1ae3b0b7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1219,7 +1219,7 @@ in syncthing-no-settings = handleTest ./syncthing-no-settings.nix { }; syncthing-init = handleTest ./syncthing-init.nix { }; syncthing-many-devices = handleTest ./syncthing-many-devices.nix { }; - syncthing-folders = handleTest ./syncthing-folders.nix { }; + syncthing-folders = runTest ./syncthing-folders.nix; syncthing-relay = handleTest ./syncthing-relay.nix { }; sysinit-reactivation = runTest ./sysinit-reactivation.nix; systemd = handleTest ./systemd.nix { }; diff --git a/nixos/tests/syncthing-folders.nix b/nixos/tests/syncthing-folders.nix index 46985b4709a6..d0623f7194d5 100644 --- a/nixos/tests/syncthing-folders.nix +++ b/nixos/tests/syncthing-folders.nix @@ -1,24 +1,26 @@ -import ../make-test-python.nix ( - { lib, pkgs, ... }: - let - genNodeId = - name: - pkgs.runCommand "syncthing-test-certs-${name}" { } '' - mkdir -p $out - ${pkgs.syncthing}/bin/syncthing generate --config=$out - ${pkgs.libxml2}/bin/xmllint --xpath 'string(configuration/device/@id)' $out/config.xml > $out/id - ''; - idA = genNodeId "a"; - idB = genNodeId "b"; - idC = genNodeId "c"; - testPasswordFile = pkgs.writeText "syncthing-test-password" "it's a secret"; - in - { - name = "syncthing"; - meta.maintainers = with pkgs.lib.maintainers; [ zarelit ]; +{ lib, pkgs, ... }: +let + genNodeId = + name: + pkgs.runCommand "syncthing-test-certs-${name}" { } '' + mkdir -p $out + ${pkgs.syncthing}/bin/syncthing generate --config=$out + ${pkgs.libxml2}/bin/xmllint --xpath 'string(configuration/device/@id)' $out/config.xml > $out/id + ''; + idA = genNodeId "a"; + idB = genNodeId "b"; + idC = genNodeId "c"; + testPassword = "it's a secret"; +in +{ + name = "syncthing"; + meta.maintainers = with pkgs.lib.maintainers; [ zarelit ]; - nodes = { - a = { + nodes = { + a = + { config, ... }: + { + environment.etc.bar-encryption-password.text = testPassword; services.syncthing = { enable = true; openDefaultPorts = true; @@ -33,12 +35,20 @@ import ../make-test-python.nix ( }; folders.bar = { path = "/var/lib/syncthing/bar"; - devices.c.encryptionPassword = "${testPasswordFile}"; + devices = [ + { + name = "c"; + encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}"; + } + ]; }; }; }; }; - b = { + b = + { config, ... }: + { + environment.etc.bar-encryption-password.text = testPassword; services.syncthing = { enable = true; openDefaultPorts = true; @@ -53,68 +63,73 @@ import ../make-test-python.nix ( }; folders.bar = { path = "/var/lib/syncthing/bar"; - devices.c.encryptionPassword = "${testPasswordFile}"; + devices = [ + { + name = "c"; + encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}"; + } + ]; }; }; }; }; - c = { - services.syncthing = { - enable = true; - openDefaultPorts = true; - cert = "${idC}/cert.pem"; - key = "${idC}/key.pem"; - settings = { - devices.a.id = lib.fileContents "${idA}/id"; - devices.b.id = lib.fileContents "${idB}/id"; - folders.bar = { - path = "/var/lib/syncthing/bar"; - devices = [ - "a" - "b" - ]; - type = "receiveencrypted"; - }; + c = { + services.syncthing = { + enable = true; + openDefaultPorts = true; + cert = "${idC}/cert.pem"; + key = "${idC}/key.pem"; + settings = { + devices.a.id = lib.fileContents "${idA}/id"; + devices.b.id = lib.fileContents "${idB}/id"; + folders.bar = { + path = "/var/lib/syncthing/bar"; + devices = [ + "a" + "b" + ]; + type = "receiveencrypted"; }; }; }; }; + }; - testScript = '' - start_all() + testScript = '' + start_all() - a.wait_for_unit("syncthing.service") - b.wait_for_unit("syncthing.service") - c.wait_for_unit("syncthing.service") - a.wait_for_open_port(22000) - b.wait_for_open_port(22000) - c.wait_for_open_port(22000) + a.wait_for_unit("syncthing.service") + b.wait_for_unit("syncthing.service") + c.wait_for_unit("syncthing.service") + a.wait_for_open_port(22000) + b.wait_for_open_port(22000) + c.wait_for_open_port(22000) - # Test foo + # Test foo - a.wait_for_file("/var/lib/syncthing/foo") - b.wait_for_file("/var/lib/syncthing/foo") + a.wait_for_file("/var/lib/syncthing/foo") + b.wait_for_file("/var/lib/syncthing/foo") - a.succeed("echo a2b > /var/lib/syncthing/foo/a2b") - b.succeed("echo b2a > /var/lib/syncthing/foo/b2a") + a.succeed("echo a2b > /var/lib/syncthing/foo/a2b") + b.succeed("echo b2a > /var/lib/syncthing/foo/b2a") - a.wait_for_file("/var/lib/syncthing/foo/b2a") - b.wait_for_file("/var/lib/syncthing/foo/a2b") + a.wait_for_file("/var/lib/syncthing/foo/b2a") + b.wait_for_file("/var/lib/syncthing/foo/a2b") - # Test bar + # Test bar - a.wait_for_file("/var/lib/syncthing/bar") - b.wait_for_file("/var/lib/syncthing/bar") - c.wait_for_file("/var/lib/syncthing/bar") + a.wait_for_file("/var/lib/syncthing/bar") + b.wait_for_file("/var/lib/syncthing/bar") + c.wait_for_file("/var/lib/syncthing/bar") - a.succeed("echo plaincontent > /var/lib/syncthing/bar/plainname") + a.succeed("echo plaincontent > /var/lib/syncthing/bar/plainname") - # 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") + # B should be able to decrypt, check that content of file matches + b.wait_for_file("/var/lib/syncthing/bar/plainname") + file_contents = b.succeed("cat /var/lib/syncthing/bar/plainname") + assert "plaincontent\n" == file_contents, f"Unexpected file contents: {file_contents=}" - # Bar on C is untrusted, check that content is not in cleartext - c.fail("grep -R plaincontent /var/lib/syncthing/bar") - ''; - } -) + # Bar on C is untrusted, check that content is not in cleartext + c.fail("grep -R plaincontent /var/lib/syncthing/bar") + ''; +}