syncthing setup

This commit is contained in:
Henri Dohmen 2025-12-30 13:54:59 +01:00
parent 24df8a251b
commit 52c074f973
Signed by: hd
GPG key ID: AB79213B044674AE
19 changed files with 244 additions and 16 deletions

44
bin/gen-syncthing-cert Executable file
View file

@ -0,0 +1,44 @@
#!/bin/sh
set -euo pipefail
tmp=$(mktemp -d)
trap 'rm -rf -- "$tmp"' EXIT
FILEPATH="${MANAGED_CLIENTS:-./var/syncthing-managed-clients.json}"
PKI_PATH="${PKI_PATH:-./pki/syncthing}"
SECRETS_PATH="${SECRETS_PATH:-secrets/syncthing}"
first_missing=$(
jq -r '
. as $root
| $root.managed_clients[]
| select($root.hashes[.] | not)
' $FILEPATH \
| head -n 1 \
)
[ -z "$first_missing" ] && echo "Done" >&2 && exit 0
echo "Generating cerificate for $first_missing"
mkdir $tmp/$first_missing
hash=$(
syncthing generate \
--config $tmp/$first_missing \
--data $tmp/$first_missing/data \
| grep -oP '(?<=device=)[A-Z0-9-]+' \
)
mkdir -p $PKI_PATH
mv $tmp/$first_missing/cert.pem $PKI_PATH/$first_missing.cert
# Remove the file so agenix does not try to decrypt
[ -f "$SECRETS_PATH/$first_missing.age" ] && rm "$SECRETS_PATH/$first_missing.age"
agenix -e $SECRETS_PATH/$first_missing.age < $tmp/$first_missing/key.pem
jq --arg client "$first_missing" \
--arg hash "$hash" \
'.hashes[$client] = $hash' "$FILEPATH" \
> "$tmp/new-syncthing-managed-clients.json" \
&& mv "$tmp/new-syncthing-managed-clients.json" "$FILEPATH"
# Revoke self to handle next client
"$0"

View file

@ -137,6 +137,8 @@
colmena.packages.${system}.colmena
agenix.packages.${system}.default
pkgs.openssl
pkgs.jq
pkgs.syncthing
];
};
};

View file

@ -16,11 +16,12 @@
];
imports = [
./hardware-configuration.nix
inputs.disko.nixosModules.disko
./disko.nix
inputs.nixos-hardware.nixosModules.framework-amd-ai-300-series
inputs.lanzaboote.nixosModules.lanzaboote
./disko.nix
./hardware-configuration.nix
./syncthing.nix
];
# https://github.com/NixOS/nixos-hardware/issues/1603

8
host/fw/syncthing.nix Normal file
View file

@ -0,0 +1,8 @@
{ ... }:
{
services.syncthing = {
enable = true;
user = "hd";
configDir = "/home/hd/.config/syncthing";
};
}

View file

@ -1,6 +1,19 @@
{ ... }:
let
guiAddress = "127.0.0.1:8384";
in
{
services.syncthing = {
enable = false; # TODO
enable = true;
inherit guiAddress;
};
services.nginx = {
privateVirtualHosts."syncthing.roam.lan" = {
locations."/" = {
proxyPass = "http://${guiAddress}/";
proxyWebsockets = true;
};
};
};
}

View file

@ -23,7 +23,7 @@ with lib;
extraGroups = [ "wheel" ];
shell = pkgs.fish;
packages = [ ];
openssh.authorizedKeys.keys = var.ssh-keys.trusted;
openssh.authorizedKeys.keys = var.ssh-keys.trusted-hd;
hashedPasswordFile = config.age.secrets.hd-password.path;
};
users.root = {

View file

@ -5,5 +5,6 @@
./common
./desktop
./nginx.nix
./syncthing.nix
];
}

65
mod/syncthing.nix Normal file
View file

@ -0,0 +1,65 @@
{
var,
config,
lib,
secrets,
...
}:
let
cfg = config.services.syncthing;
this = config.networking.hostName;
is-managed = builtins.elem this var.syncthing-managed-clients.managed_clients;
is-server = this == "roam";
devices = lib.attrNames var.syncthing;
devices-without-this = lib.remove this devices;
type-encrypt = if is-server then "receiveencrypted" else "sendreceive";
devices-encrypt =
if is-server then
devices-without-this
else
lib.remove "roam" devices-without-this
++ [
{
name = "roam";
encryptionPasswordFile = config.age.secrets.syncthing-password.path;
}
];
folders = {
documents = {
id = "documents-hd";
path = if is-server then "/data/sync/documents-hd" else "/home/hd/Documents";
type = type-encrypt;
devices = devices-encrypt;
versioning = {
type = "simple";
params.keep = "10";
};
};
};
in
{
age.secrets.syncthing-password = lib.mkIf (cfg.enable && !is-server) {
file = secrets."syncthing-password.age";
mode = "440";
owner = config.services.syncthing.user;
group = config.services.syncthing.group;
};
age.secrets.syncthing-key = lib.mkIf (cfg.enable && is-managed) {
file = secrets.syncthing."${this}.age";
mode = "440";
owner = config.services.syncthing.user;
group = config.services.syncthing.group;
};
services.syncthing = lib.mkIf cfg.enable {
inherit folders;
settings = {
devices = var.syncthing;
};
key = lib.optionalAttrs is-managed config.age.secrets.syncthing-key.path;
cert = lib.optionalAttrs is-managed "${../pki/syncthing + "/${this}.cert"}";
};
}

11
pki/syncthing/fw.cert Normal file
View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBnzCCAVGgAwIBAgIIDgM/cFGbbhowBQYDK2VwMEoxEjAQBgNVBAoTCVN5bmN0
aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0ZWQxEjAQBgNVBAMT
CXN5bmN0aGluZzAeFw0yNTEyMzAwMDAwMDBaFw00NTEyMjUwMDAwMDBaMEoxEjAQ
BgNVBAoTCVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0
ZWQxEjAQBgNVBAMTCXN5bmN0aGluZzAqMAUGAytlcAMhAGUklM+K4ns56bvytz1O
dTs+XvCn2QQ+MbWh7z1ejy0Jo1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw
FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ
c3luY3RoaW5nMAUGAytlcANBAPmfwk4OUbL+S6TgCF2XqND4KeMO1LxXZ57IuqxC
NK0w/nLcfebOoRlxCXLnW/VIKnhysr92nvW3SOuRqJOdDAg=
-----END CERTIFICATE-----

11
pki/syncthing/roam.cert Normal file
View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBnzCCAVGgAwIBAgIIP1QH/ukEmpEwBQYDK2VwMEoxEjAQBgNVBAoTCVN5bmN0
aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0ZWQxEjAQBgNVBAMT
CXN5bmN0aGluZzAeFw0yNTEyMzAwMDAwMDBaFw00NTEyMjUwMDAwMDBaMEoxEjAQ
BgNVBAoTCVN5bmN0aGluZzEgMB4GA1UECxMXQXV0b21hdGljYWxseSBHZW5lcmF0
ZWQxEjAQBgNVBAMTCXN5bmN0aGluZzAqMAUGAytlcAMhAI8QrJk1duZ5hrAhWh/l
iaR/0kmK0H+TM+mtpt4YcOMMo1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw
FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ
c3luY3RoaW5nMAUGAytlcANBAP0aSI/TMmhS7RX5sRHlgCHRae4+4H5EDXckecGg
uUA87D5Hq2sVULwhPWDbMChA3dbhLVN09mJO8IU8loQ+0Qs=
-----END CERTIFICATE-----

View file

@ -1,12 +1,29 @@
let
pkgs = import <nixpkgs> { };
inherit (pkgs) lib;
keys = (import ./var { inherit lib; }).ssh-keys.root;
ssh-keys = (import ./var { inherit lib; }).ssh-keys;
keys = ssh-keys.root;
trusted-keys = ssh-keys.trusted-root;
secrets = [
"roam/rclone-conf"
"roam/firefox-sync-secret"
"hd-password"
"tlskey"
];
trusted-secrets = [
# Can only be decrypted by clients
"syncthing-password"
];
mkSecrets =
keys: secrets: lib.mergeAttrsList (map (x: { "secrets/${x}.age".publicKeys = keys; }) secrets);
syncthingManagedClients = (lib.importJSON ./var/syncthing-managed-clients.json).managed_clients;
mkSyncthingSecret = client: {
"secrets/syncthing/${client}.age".publicKeys = [ ssh-keys.by-host.root.${client} ];
};
syncthingSercrets = lib.mergeAttrsList (map mkSyncthingSecret syncthingManagedClients);
in
builtins.foldl' (acc: x: acc // { "secrets/${x}.age".publicKeys = keys; }) { } secrets
lib.mergeAttrsList ([
(mkSecrets keys secrets)
(mkSecrets trusted-keys trusted-secrets)
(syncthingSercrets)
])

View file

@ -0,0 +1,10 @@
age-encryption.org/v1
-> ssh-ed25519 FTMbvw XgjPSuj97t3rQ4LUyKQUFrcFF6cwQMSJFjTJDAG/Nj0
ygJ4dT50p4F92ZOEBOA9gZbQ6DoSFLF7ES1nNA+5EkI
-> ssh-ed25519 ydxpSQ 32hIAyI93zJvZ9W/RwzLAMM+HuICDXiu6nkwZYryl0Y
O2gITm4v/e40dVjE5KAeea+j4VwdYVnxXSTbKVmc1ag
-> ssh-ed25519 IbE9zA Uvjx9kbxOt6zIVZsIiLValtgKeL7hufv8O45OrU7Nww
yAzSC6ggAInYOfbh9DmcnoAbrun8LLTWpqF9YK9dzCw
--- oVhNPf8TcFS/eZpcR0Yae3jddZziNWXPweId4HledhM
„кÖ—¦ { ät •q[ö<>;qDª€ðr£LA}ªÚú‚ܬ~}œÙKD_zgýv(¡d¢
âUÝì …<ð_òôåÄ ¹°¥Õ°§ðÞ<14>Õ¯Ú婞zœIúiä,²

BIN
secrets/syncthing/fw.age Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
age-encryption.org/v1
-> ssh-ed25519 gbs8eg hQNgGRxHRPhSq5iWURBpPd3nZDh4ofjOtGlC9V3jnWc
AJ8CDyayy0kUoPiUp0LMyHRBeKjRZUTD9BbQYPKuw90
--- nxoh+y+XGoihcSXsc9CW5/KzuDCQkbpjiiXMnfJVs3A
á\v:I~ù¯§t/u§ßדÍ<E2809C>YU<>w$7š8[”xƒ] <0A>¸;nÜRëG9Ð<á â ·úûþ×o,>áSÙ¥Ë7Í¥aàB9láð{ˆ;;Úkä<6B>e7gjÃ<6A>¡$€¦
e<EFBFBD>@8Èü³TÕÒñ;Ç€Ûã…ÌFužrLq¯váòËDîš8z"òªÐìãŽPT6nˆ<]<5D>

View file

@ -1,13 +1,20 @@
{ ... }@inputs:
{
lib ? null,
}:
let
inputs' = inputs // {
lib' = if builtins.isNull lib then (import <nixpkgs> { }).lib else lib;
inputs' = {
lib = lib';
var = outputs;
};
load-var = x: import x inputs';
# watch out for cycles
outputs = {
"lan-dns" = import ./lan-dns.nix inputs';
"ssh-keys" = import ./ssh-keys.nix inputs';
"wg" = import ./wg.nix inputs';
"lan-dns" = load-var ./lan-dns.nix;
"ssh-keys" = load-var ./ssh-keys.nix;
"wg" = load-var ./wg.nix;
"syncthing" = load-var ./syncthing.nix;
"syncthing-managed-clients" = lib'.importJSON ./syncthing-managed-clients.json;
};
in
outputs

View file

@ -7,6 +7,7 @@ let
}) var.wg.ips;
custom-hosts = with var.wg.ips; {
"git.lan" = roam;
"syncthing.roam.lan" = roam;
};
in
rec {

View file

@ -16,12 +16,15 @@ let
};
};
keys' = mkKeys keys;
mkTrusted =
user: with keys'.by-host.${user}; [
solo
c2
fw
];
in
keys'
// {
trusted = with keys'.by-host.hd; [
solo
c2
fw
];
trusted-hd = mkTrusted "hd";
trusted-root = mkTrusted "root";
}

View file

@ -0,0 +1,10 @@
{
"managed_clients": [
"fw",
"roam"
],
"hashes": {
"fw": "YZGGXOT-MPFD7O4-ACLGOGT-LIMZVD3-7JBSZZR-LFCFWQL-BLO435I-LLH6GAL",
"roam": "HMB7ZRF-OODFHHW-2QCIFFJ-M7COVK5-YUB3GKT-SI56D2U-CPTTJEP-R3ZKOQ7"
}
}

18
var/syncthing.nix Normal file
View file

@ -0,0 +1,18 @@
{ var, lib, ... }:
let
inherit (var.syncthing-managed-clients) managed_clients hashes;
unmanaged = {
# "roam".id = "OIKOKOT-LY4JWPX-T7OXE4D-I4ZC3IR-ZLMKFCO-IXSVEYZ-Y3FZOUB-LIG2XAO";
};
in
assert (
lib.assertMsg (
builtins.attrNames hashes == managed_clients
) "Not all declaratively configured syncthing clients have keys. Rerun ./bin/gen-syncthing-cert"
);
assert (
lib.assertMsg (
[ ] == (lib.intersectLists managed_clients (builtins.attrNames unmanaged))
) "Syncthing clients must either be unmanaged or declaratively configured."
);
unmanaged // builtins.mapAttrs (_: v: { id = v; }) hashes