diff --git a/bin/gen-syncthing-cert b/bin/gen-syncthing-cert new file mode 100755 index 0000000..9bae874 --- /dev/null +++ b/bin/gen-syncthing-cert @@ -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" \ No newline at end of file diff --git a/flake.nix b/flake.nix index 74e4967..d899b5c 100644 --- a/flake.nix +++ b/flake.nix @@ -137,6 +137,8 @@ colmena.packages.${system}.colmena agenix.packages.${system}.default pkgs.openssl + pkgs.jq + pkgs.syncthing ]; }; }; diff --git a/host/fw/default.nix b/host/fw/default.nix index dc3becb..b0ea11c 100644 --- a/host/fw/default.nix +++ b/host/fw/default.nix @@ -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 diff --git a/host/fw/syncthing.nix b/host/fw/syncthing.nix new file mode 100644 index 0000000..dd0c768 --- /dev/null +++ b/host/fw/syncthing.nix @@ -0,0 +1,8 @@ +{ ... }: +{ + services.syncthing = { + enable = true; + user = "hd"; + configDir = "/home/hd/.config/syncthing"; + }; +} diff --git a/host/roam/syncthing.nix b/host/roam/syncthing.nix index 06e6acc..4d5e129 100644 --- a/host/roam/syncthing.nix +++ b/host/roam/syncthing.nix @@ -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; + }; + }; }; } diff --git a/mod/common/users.nix b/mod/common/users.nix index 409cb22..98696c2 100644 --- a/mod/common/users.nix +++ b/mod/common/users.nix @@ -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 = { diff --git a/mod/default.nix b/mod/default.nix index 466ed6a..84e6895 100644 --- a/mod/default.nix +++ b/mod/default.nix @@ -5,5 +5,6 @@ ./common ./desktop ./nginx.nix + ./syncthing.nix ]; } diff --git a/mod/syncthing.nix b/mod/syncthing.nix new file mode 100644 index 0000000..20e8cc8 --- /dev/null +++ b/mod/syncthing.nix @@ -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"}"; + }; +} diff --git a/pki/syncthing/fw.cert b/pki/syncthing/fw.cert new file mode 100644 index 0000000..249ac76 --- /dev/null +++ b/pki/syncthing/fw.cert @@ -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----- diff --git a/pki/syncthing/roam.cert b/pki/syncthing/roam.cert new file mode 100644 index 0000000..b5e2d4d --- /dev/null +++ b/pki/syncthing/roam.cert @@ -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----- diff --git a/secrets.nix b/secrets.nix index 9bdc144..9d71168 100644 --- a/secrets.nix +++ b/secrets.nix @@ -1,12 +1,29 @@ let pkgs = import { }; 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) +]) diff --git a/secrets/syncthing-password.age b/secrets/syncthing-password.age new file mode 100644 index 0000000..5852b08 --- /dev/null +++ b/secrets/syncthing-password.age @@ -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[;qDrLA}ܬ~}KD_zgv(d +U젅<_ կ婞zIi, \ No newline at end of file diff --git a/secrets/syncthing/fw.age b/secrets/syncthing/fw.age new file mode 100644 index 0000000..4d066e5 Binary files /dev/null and b/secrets/syncthing/fw.age differ diff --git a/secrets/syncthing/roam.age b/secrets/syncthing/roam.age new file mode 100644 index 0000000..4d516c8 --- /dev/null +++ b/secrets/syncthing/roam.age @@ -0,0 +1,6 @@ +age-encryption.org/v1 +-> ssh-ed25519 gbs8eg hQNgGRxHRPhSq5iWURBpPd3nZDh4ofjOtGlC9V3jnWc +AJ8CDyayy0kUoPiUp0LMyHRBeKjRZUTD9BbQYPKuw90 +--- nxoh+y+XGoihcSXsc9CW5/KzuDCQkbpjiiXMnfJVs3A +\vU:I~t/uד͐YUw$78[x] ;nRG9< o,>S٥7aB9l{;;ke7gjÐ$ +e@8T;ǀFurLqvD8z"PT6n<] \ No newline at end of file diff --git a/var/default.nix b/var/default.nix index 412b230..f773fcc 100644 --- a/var/default.nix +++ b/var/default.nix @@ -1,13 +1,20 @@ -{ ... }@inputs: +{ + lib ? null, +}: let - inputs' = inputs // { + lib' = if builtins.isNull lib then (import { }).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 diff --git a/var/lan-dns.nix b/var/lan-dns.nix index 2f9e24c..f2b5e49 100644 --- a/var/lan-dns.nix +++ b/var/lan-dns.nix @@ -7,6 +7,7 @@ let }) var.wg.ips; custom-hosts = with var.wg.ips; { "git.lan" = roam; + "syncthing.roam.lan" = roam; }; in rec { diff --git a/var/ssh-keys.nix b/var/ssh-keys.nix index eb5fbc7..16726a8 100644 --- a/var/ssh-keys.nix +++ b/var/ssh-keys.nix @@ -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"; } diff --git a/var/syncthing-managed-clients.json b/var/syncthing-managed-clients.json new file mode 100644 index 0000000..d9203da --- /dev/null +++ b/var/syncthing-managed-clients.json @@ -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" + } +} diff --git a/var/syncthing.nix b/var/syncthing.nix new file mode 100644 index 0000000..668d66e --- /dev/null +++ b/var/syncthing.nix @@ -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