Skip to content

Commit

Permalink
Demonstration of an alternate way to embed secrets into syncthing config
Browse files Browse the repository at this point in the history
  • Loading branch information
jfly committed Feb 20, 2025
1 parent 181420e commit 3e527dd
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 155 deletions.
4 changes: 4 additions & 0 deletions doc/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@

- This should not be necessary going forward, because loading app state from 0.7.0 or newer is now supported. This is documented in the [0.7.1 changelog](https://github.com/Nexus-Mods/NexusMods.App/releases/tag/v0.7.1).

## Other Notable Changes {#sec-nixpkgs-release-25.05-notable-changes}

- `services.syncthing.settings.folders.<name>.devices` previously only accepted strings, but not also accepts `attrset`s, which allows you to set `encryptionPasswordFile` for a device.

## Nixpkgs Library {#sec-nixpkgs-release-25.05-lib}

### Breaking changes {#sec-nixpkgs-release-25.05-lib-breaking}
Expand Down
2 changes: 0 additions & 2 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.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}
Expand Down
181 changes: 97 additions & 84 deletions nixos/modules/services/networking/syncthing.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 = lib.traceVal (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")
Expand Down Expand Up @@ -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.
'';
};

Expand Down
2 changes: 1 addition & 1 deletion nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down
Loading

0 comments on commit 3e527dd

Please sign in to comment.