From aa968432fb49fab0364c48a2c9d26024ff7cb53e 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 | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index 2d32cf4517062..c5b9ea882069b 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -103,9 +103,66 @@ 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") ] /* If we need to override devices/folders, we iterate all currently configured From 142824eee56776e3b18bb9193806d2e45350bed2 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 | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix index c5b9ea882069b..5fd672fe3d649 100644 --- a/nixos/modules/services/networking/syncthing.nix +++ b/nixos/modules/services/networking/syncthing.nix @@ -34,12 +34,22 @@ let The options services.syncthing.settings.folders..{rescanInterval,watch,watchDelay} were removed. Please use, respectively, {rescanIntervalS,fsWatcherEnabled,fsWatcherDelayS} instead. '' { - devices = map (device: - if builtins.isString device then - { deviceId = cfg.settings.devices.${device}.id; } + 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 - device - ) folder.devices; + throw "Invalid type for devices in folder '${folderName}'; expected list or attrset."; }) (filterAttrs (_: folder: folder.enable ) cfg.settings.folders); @@ -435,11 +445,30 @@ 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 ac8169256d7984bcdbb183d9819b1afa2e419d17 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 65fdf19f6fdcb..d0ec0cb519aae 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1025,6 +1025,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 0000000000000..5914aacf0b2a3 --- /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 295aefebbaaae381e427fa1364c9893f8d6ecfd6 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 5914aacf0b2a3..46985b4709a69 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 181420eca1bbaa75f52928be6b406623833d14df 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 339c4bb38c36e..27f9b2cd6bf47 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -928,6 +928,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 5c19dbd17d8e14096d6f7c8480297c014918f838 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, 181 insertions(+), 155 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 27f9b2cd6bf47..339c4bb38c36e 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -928,8 +928,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 5fd672fe3d649..7bb532a952482 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,67 @@ 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 - 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}"; + jsonPreSecretsFile = pkgs.writeTextFile { + name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json"; + text = builtins.toJSON new_cfg; + }; + injectSecrtsJqCmd = { + # There are no secrets in `devs`, so no massaging needed. + "devs" = "${jq} ."; + "dirs" = let + 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} + ${injectSecrtsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress} '' )) (lib.concatStringsSep "\n") @@ -445,30 +445,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/all-tests.nix b/nixos/tests/all-tests.nix index d0ec0cb519aae..1a08a4d80d69a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1025,7 +1025,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 46985b4709a69..d0623f7194d56 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") + ''; +}