mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-09 19:13:26 +03:00
Demonstration of an alternate way to embed secrets into syncthing config
This commit is contained in:
parent
715ba701fa
commit
1b7b89c4ef
4 changed files with 183 additions and 153 deletions
|
@ -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.
|
- `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.<name>.devices` now accepts an `attrset`, allowing to set `encryptionPassword` file for a device.
|
|
||||||
|
|
||||||
## Detailed Migration Information {#sec-release-24.11-migration}
|
## Detailed Migration Information {#sec-release-24.11-migration}
|
||||||
|
|
||||||
### `sound` options removal {#sec-release-24.11-migration-sound}
|
### `sound` options removal {#sec-release-24.11-migration-sound}
|
||||||
|
|
|
@ -59,17 +59,15 @@ let
|
||||||
let
|
let
|
||||||
folderDevices = folder.devices;
|
folderDevices = folder.devices;
|
||||||
in
|
in
|
||||||
if builtins.isList folderDevices then
|
map (
|
||||||
map (
|
device:
|
||||||
device:
|
if builtins.isString device then
|
||||||
if builtins.isString device then { deviceId = cfg.settings.devices.${device}.id; } else device
|
{ deviceId = cfg.settings.devices.${device}.id; }
|
||||||
) folderDevices
|
else if builtins.isAttrs device then
|
||||||
else if builtins.isAttrs folderDevices then
|
{ deviceId = cfg.settings.devices.${device.name}.id; } // device
|
||||||
mapAttrsToList (
|
else
|
||||||
deviceName: deviceValue: deviceValue // { deviceId = cfg.settings.devices.${deviceName}.id; }
|
throw "Invalid type for devices in folder '${folderName}'; expected list or attrset."
|
||||||
) folderDevices
|
) folderDevices;
|
||||||
else
|
|
||||||
throw "Invalid type for devices in folder '${folderName}'; expected list or attrset.";
|
|
||||||
}
|
}
|
||||||
) (filterAttrs (_: folder: folder.enable) cfg.settings.folders);
|
) (filterAttrs (_: folder: folder.enable) cfg.settings.folders);
|
||||||
|
|
||||||
|
@ -142,68 +140,74 @@ let
|
||||||
(map (
|
(map (
|
||||||
new_cfg:
|
new_cfg:
|
||||||
let
|
let
|
||||||
isSecret = attr: value: builtins.isString value && attr == "encryptionPassword";
|
jsonPreSecretsFile = pkgs.writeTextFile {
|
||||||
|
name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json";
|
||||||
resolveSecrets =
|
text = builtins.toJSON new_cfg;
|
||||||
attr: value:
|
};
|
||||||
if builtins.isAttrs value then
|
injectSecretsJqCmd =
|
||||||
# Attribute set: process each attribute
|
{
|
||||||
builtins.mapAttrs (name: val: resolveSecrets name val) value
|
# There are no secrets in `devs`, so no massaging needed.
|
||||||
else if builtins.isList value then
|
"devs" = "${jq} .";
|
||||||
# List: process each element
|
"dirs" =
|
||||||
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
|
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
|
in
|
||||||
''
|
"${jq} ${lib.concatStringsSep " " jqRawFiles} ${
|
||||||
if [ ! -r ${path} ]; then
|
lib.escapeShellArg (lib.concatStringsSep "|" ([ "." ] ++ jqUpdates))
|
||||||
echo "${path} does not exist"
|
}";
|
||||||
exit 1
|
}
|
||||||
fi
|
.${conf_type};
|
||||||
${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
|
in
|
||||||
''
|
''
|
||||||
${secretVarsScript}
|
${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress}
|
||||||
|
|
||||||
curl -d "${escapedJson}" -X POST ${s.baseAddress}
|
|
||||||
''
|
''
|
||||||
))
|
))
|
||||||
(lib.concatStringsSep "\n")
|
(lib.concatStringsSep "\n")
|
||||||
|
@ -513,16 +517,30 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
devices = mkOption {
|
devices = mkOption {
|
||||||
type = types.oneOf [
|
type = types.listOf (
|
||||||
(types.listOf types.str)
|
types.oneOf [
|
||||||
(types.attrsOf (
|
types.str
|
||||||
types.submodule (
|
(types.submodule (
|
||||||
{ name, ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
freeformType = settingsFormat.type;
|
freeformType = settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
encryptionPassword = mkOption {
|
name = mkOption {
|
||||||
type = types.nullOr types.str;
|
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;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Path to encryption password. If set, the file will be read during
|
Path to encryption password. If set, the file will be read during
|
||||||
|
@ -531,17 +549,16 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
))
|
||||||
))
|
]
|
||||||
];
|
);
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
The devices this folder should be shared with. Each device must
|
The devices this folder should be shared with. Each device must
|
||||||
be defined in the [devices](#opt-services.syncthing.settings.devices) option.
|
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
|
A list of either strings or attribute sets, where values
|
||||||
[devices](#opt-services.syncthing.settings.devices) option, and values are
|
are device names or device configurations.
|
||||||
device configurations.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1219,7 +1219,7 @@ in
|
||||||
syncthing-no-settings = handleTest ./syncthing-no-settings.nix { };
|
syncthing-no-settings = handleTest ./syncthing-no-settings.nix { };
|
||||||
syncthing-init = handleTest ./syncthing-init.nix { };
|
syncthing-init = handleTest ./syncthing-init.nix { };
|
||||||
syncthing-many-devices = handleTest ./syncthing-many-devices.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 { };
|
syncthing-relay = handleTest ./syncthing-relay.nix { };
|
||||||
sysinit-reactivation = runTest ./sysinit-reactivation.nix;
|
sysinit-reactivation = runTest ./sysinit-reactivation.nix;
|
||||||
systemd = handleTest ./systemd.nix { };
|
systemd = handleTest ./systemd.nix { };
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
import ../make-test-python.nix (
|
{ lib, pkgs, ... }:
|
||||||
{ lib, pkgs, ... }:
|
let
|
||||||
let
|
genNodeId =
|
||||||
genNodeId =
|
name:
|
||||||
name:
|
pkgs.runCommand "syncthing-test-certs-${name}" { } ''
|
||||||
pkgs.runCommand "syncthing-test-certs-${name}" { } ''
|
mkdir -p $out
|
||||||
mkdir -p $out
|
${pkgs.syncthing}/bin/syncthing generate --config=$out
|
||||||
${pkgs.syncthing}/bin/syncthing generate --config=$out
|
${pkgs.libxml2}/bin/xmllint --xpath 'string(configuration/device/@id)' $out/config.xml > $out/id
|
||||||
${pkgs.libxml2}/bin/xmllint --xpath 'string(configuration/device/@id)' $out/config.xml > $out/id
|
'';
|
||||||
'';
|
idA = genNodeId "a";
|
||||||
idA = genNodeId "a";
|
idB = genNodeId "b";
|
||||||
idB = genNodeId "b";
|
idC = genNodeId "c";
|
||||||
idC = genNodeId "c";
|
testPassword = "it's a secret";
|
||||||
testPasswordFile = pkgs.writeText "syncthing-test-password" "it's a secret";
|
in
|
||||||
in
|
{
|
||||||
{
|
name = "syncthing";
|
||||||
name = "syncthing";
|
meta.maintainers = with pkgs.lib.maintainers; [ zarelit ];
|
||||||
meta.maintainers = with pkgs.lib.maintainers; [ zarelit ];
|
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
a = {
|
a =
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
environment.etc.bar-encryption-password.text = testPassword;
|
||||||
services.syncthing = {
|
services.syncthing = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openDefaultPorts = true;
|
openDefaultPorts = true;
|
||||||
|
@ -33,12 +35,20 @@ import ../make-test-python.nix (
|
||||||
};
|
};
|
||||||
folders.bar = {
|
folders.bar = {
|
||||||
path = "/var/lib/syncthing/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 = {
|
services.syncthing = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openDefaultPorts = true;
|
openDefaultPorts = true;
|
||||||
|
@ -53,68 +63,73 @@ import ../make-test-python.nix (
|
||||||
};
|
};
|
||||||
folders.bar = {
|
folders.bar = {
|
||||||
path = "/var/lib/syncthing/bar";
|
path = "/var/lib/syncthing/bar";
|
||||||
devices.c.encryptionPassword = "${testPasswordFile}";
|
devices = [
|
||||||
|
{
|
||||||
|
name = "c";
|
||||||
|
encryptionPasswordFile = "/etc/${config.environment.etc.bar-encryption-password.target}";
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
c = {
|
c = {
|
||||||
services.syncthing = {
|
services.syncthing = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openDefaultPorts = true;
|
openDefaultPorts = true;
|
||||||
cert = "${idC}/cert.pem";
|
cert = "${idC}/cert.pem";
|
||||||
key = "${idC}/key.pem";
|
key = "${idC}/key.pem";
|
||||||
settings = {
|
settings = {
|
||||||
devices.a.id = lib.fileContents "${idA}/id";
|
devices.a.id = lib.fileContents "${idA}/id";
|
||||||
devices.b.id = lib.fileContents "${idB}/id";
|
devices.b.id = lib.fileContents "${idB}/id";
|
||||||
folders.bar = {
|
folders.bar = {
|
||||||
path = "/var/lib/syncthing/bar";
|
path = "/var/lib/syncthing/bar";
|
||||||
devices = [
|
devices = [
|
||||||
"a"
|
"a"
|
||||||
"b"
|
"b"
|
||||||
];
|
];
|
||||||
type = "receiveencrypted";
|
type = "receiveencrypted";
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
testScript = ''
|
testScript = ''
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
a.wait_for_unit("syncthing.service")
|
a.wait_for_unit("syncthing.service")
|
||||||
b.wait_for_unit("syncthing.service")
|
b.wait_for_unit("syncthing.service")
|
||||||
c.wait_for_unit("syncthing.service")
|
c.wait_for_unit("syncthing.service")
|
||||||
a.wait_for_open_port(22000)
|
a.wait_for_open_port(22000)
|
||||||
b.wait_for_open_port(22000)
|
b.wait_for_open_port(22000)
|
||||||
c.wait_for_open_port(22000)
|
c.wait_for_open_port(22000)
|
||||||
|
|
||||||
# Test foo
|
# Test foo
|
||||||
|
|
||||||
a.wait_for_file("/var/lib/syncthing/foo")
|
a.wait_for_file("/var/lib/syncthing/foo")
|
||||||
b.wait_for_file("/var/lib/syncthing/foo")
|
b.wait_for_file("/var/lib/syncthing/foo")
|
||||||
|
|
||||||
a.succeed("echo a2b > /var/lib/syncthing/foo/a2b")
|
a.succeed("echo a2b > /var/lib/syncthing/foo/a2b")
|
||||||
b.succeed("echo b2a > /var/lib/syncthing/foo/b2a")
|
b.succeed("echo b2a > /var/lib/syncthing/foo/b2a")
|
||||||
|
|
||||||
a.wait_for_file("/var/lib/syncthing/foo/b2a")
|
a.wait_for_file("/var/lib/syncthing/foo/b2a")
|
||||||
b.wait_for_file("/var/lib/syncthing/foo/a2b")
|
b.wait_for_file("/var/lib/syncthing/foo/a2b")
|
||||||
|
|
||||||
# Test bar
|
# Test bar
|
||||||
|
|
||||||
a.wait_for_file("/var/lib/syncthing/bar")
|
a.wait_for_file("/var/lib/syncthing/bar")
|
||||||
b.wait_for_file("/var/lib/syncthing/bar")
|
b.wait_for_file("/var/lib/syncthing/bar")
|
||||||
c.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 should be able to decrypt, check that content of file matches
|
||||||
b.wait_for_file("/var/lib/syncthing/bar/plainname")
|
b.wait_for_file("/var/lib/syncthing/bar/plainname")
|
||||||
b.succeed("grep plaincontent /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
|
# Bar on C is untrusted, check that content is not in cleartext
|
||||||
c.fail("grep -R plaincontent /var/lib/syncthing/bar")
|
c.fail("grep -R plaincontent /var/lib/syncthing/bar")
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue