From d5f2880457f5670739c73176b984f35c8b7ca7e9 Mon Sep 17 00:00:00 2001 From: oddlama Date: Sat, 15 Apr 2023 01:51:33 +0200 Subject: [PATCH] feat(wireguard): generate psks only if needed; add most of the qr code generator --- flake.nix | 2 + hosts/nom/net.nix | 5 +- hosts/ward/net.nix | 12 +- hosts/zackbiene/net.nix | 8 +- modules/wireguard.nix | 154 +++++++++++++----- nix/apps/format-secrets.nix | 24 +-- nix/apps/generate-wireguard-keys.nix | 18 +- nix/apps/show-wireguard-qr.nix | 47 +++++- nix/extra-builtins.nix | 2 +- nix/generate-node.nix | 9 +- nix/lib.nix | 45 +++-- ...e-decrypt.sh => rage-decrypt-and-cache.sh} | 0 nix/secrets.nix | 7 +- 13 files changed, 225 insertions(+), 108 deletions(-) rename nix/{rage-decrypt.sh => rage-decrypt-and-cache.sh} (100%) diff --git a/flake.nix b/flake.nix index 00f6cdb..1e96687 100644 --- a/flake.nix +++ b/flake.nix @@ -52,6 +52,8 @@ ... } @ inputs: { + extraLib = import ./nix/lib.nix inputs; + # The identities that are used to rekey agenix secrets and to # decrypt all repository-wide secrets. secrets = { diff --git a/hosts/nom/net.nix b/hosts/nom/net.nix index 133a94e..80d20c9 100644 --- a/hosts/nom/net.nix +++ b/hosts/nom/net.nix @@ -21,5 +21,8 @@ }; }; - extra.wireguard.vms.address = ["10.0.0.10/32"]; + extra.wireguard.vms = { + via = "ward"; + addresses = ["10.0.0.10/32"]; + }; } diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index 5d9c0e3..5cfc490 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -27,12 +27,12 @@ enable = true; port = 51822; openFirewall = true; + externalPeers = { + test1 = ["10.0.0.91/32"]; + test2 = ["10.0.0.92/32"]; + test3 = ["10.0.0.93/32"]; + }; }; - address = ["10.0.0.1/24"]; - externalPeers = { - test1 = ["10.0.0.91/32"]; - test2 = ["10.0.0.92/32"]; - test3 = ["10.0.0.93/32"]; - }; + addresses = ["10.0.0.1/24"]; }; } diff --git a/hosts/zackbiene/net.nix b/hosts/zackbiene/net.nix index 97f4f0b..d2b2ed5 100644 --- a/hosts/zackbiene/net.nix +++ b/hosts/zackbiene/net.nix @@ -23,10 +23,10 @@ enable = true; port = 51822; openFirewall = true; + externalPeers = { + zack1 = ["10.0.0.90/32"]; + }; }; - address = ["10.0.0.2/24"]; - externalPeers = { - zack1 = ["10.0.0.90/32"]; - }; + addresses = ["10.0.0.2/24"]; }; } diff --git a/modules/wireguard.nix b/modules/wireguard.nix index 64c8798..5d41870 100644 --- a/modules/wireguard.nix +++ b/modules/wireguard.nix @@ -11,9 +11,11 @@ (lib) any attrNames + attrValues concatMap concatMapStrings concatStringsSep + filter filterAttrs head mapAttrsToList @@ -23,6 +25,7 @@ mkOption mkEnableOption optionalAttrs + optionals splitString types ; @@ -35,10 +38,12 @@ cfg = config.extra.wireguard; - configForNetwork = wgName: wg: let + configForNetwork = wgName: wgCfg: let inherit - (extraLib.wireguard wgName nodes) - allPeers + (extraLib.wireguard wgName) + associatedServerNodes + associatedClientNodes + externalPeerName peerPresharedKeyPath peerPresharedKeySecret peerPrivateKeyPath @@ -46,17 +51,32 @@ peerPublicKeyPath ; - otherPeers = filterAttrs (n: _: n != nodeName) allPeers; + filterSelf = filter (x: x != nodeName); + wgCfgOf = node: nodes.${node}.config.extra.wireguard.${wgName}; + + ourClientNodes = + optionals wgCfg.server.enable + (filter (n: (wgCfgOf n).via == nodeName) associatedClientNodes); + + # The list of peers that we have to know the psk to. + neededPeers = + if wgCfg.server.enable + then + filterSelf associatedServerNodes + ++ map externalPeerName (attrNames wgCfg.server.externalPeers) + ++ ourClientNodes + else [wgCfg.via]; in { secrets = concatAttrs (map (other: { - ${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other; - }) (attrNames otherPeers)) + ${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other; + }) + neededPeers) // { ${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName; }; - netdevs."${wg.priority}-${wgName}" = { + netdevs."${wgCfg.priority}-${wgName}" = { netdevConfig = { Kind = "wireguard"; Name = "${wgName}"; @@ -66,27 +86,64 @@ { PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path; } - // optionalAttrs wg.server.enable { - ListenPort = wg.server.port; + // optionalAttrs wgCfg.server.enable { + ListenPort = wgCfg.server.port; }; wireguardPeers = - mapAttrsToList (peerName: peerAllowedIPs: { - wireguardPeerConfig = - { + if wgCfg.server.enable + then + # Always include all other server nodes. + map (serverNode: { + wireguardPeerConfig = { + PublicKey = builtins.readFile (peerPublicKeyPath serverNode); + PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName serverNode}.path; + # The allowed ips of a server node are it's own addreses, + # plus each external peer's addresses, + # plus each client's addresses that is connected via this node. + AllowedIPs = + (wgCfgOf serverNode).addresses + ++ attrValues (wgCfgOf serverNode).server.externalPeers + ++ map (n: (wgCfgOf n).addresses) ourClientNodes; + }; + }) (filterSelf associatedServerNodes) + # All our external peers + ++ mapAttrsToList (extPeer: allowedIPs: let + peerName = externalPeerName extPeer; + in { + wireguardPeerConfig = { PublicKey = builtins.readFile (peerPublicKeyPath peerName); PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName peerName}.path; - AllowedIPs = peerAllowedIPs; - } - // optionalAttrs wg.server.enable { + AllowedIPs = allowedIPs; PersistentKeepalive = 25; }; - }) - otherPeers; + }) + wgCfg.server.externalPeers + # All client nodes that have their via set to us. + ++ mapAttrsToList (clientNode: { + wireguardPeerConfig = { + PublicKey = builtins.readFile (peerPublicKeyPath clientNode); + PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName clientNode}.path; + AllowedIPs = (wgCfgOf clientNode).addresses; + PersistentKeepalive = 25; + }; + }) + ourClientNodes + else + # We are a client node, so only include our via server. + [ + { + wireguardPeerConfig = { + PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.via); + PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName wgCfg.via}.path; + AllowedIPs = (wgCfgOf wgCfg.via).addresses; + }; + } + ]; }; - networks."${wg.priority}-${wgName}" = { + networks."${wgCfg.priority}-${wgName}" = { matchConfig.Name = wgName; - networkConfig.Address = wg.address; + networkConfig.Address = wgCfg.addresses; }; }; in { @@ -109,6 +166,20 @@ in { type = types.bool; description = mdDoc "Whether to open the firewall for the specified `listenPort`, if {option}`listen` is `true`."; }; + + externalPeers = mkOption { + type = types.attrsOf (types.listOf types.str); + default = {}; + example = {my-android-phone = ["10.0.0.97/32"];}; + description = mdDoc '' + Allows defining an extra set of peers that should be added to this wireguard network, + but will not be managed by this flake. (e.g. phones) + + These external peers will only know this node as a peer, which will forward + their traffic to other members of the network if required. This requires + this node to act as a server. + ''; + }; }; priority = mkOption { @@ -117,27 +188,22 @@ in { description = mdDoc "The order priority used when creating systemd netdev and network files."; }; - address = mkOption { + via = mkOption { + default = null; + type = types.uniq (types.nullOr types.str); + description = mdDoc '' + The server node via which to connect to the network. + This must defined if and only if this node is not a server. + ''; + }; + + addresses = mkOption { type = types.listOf types.str; description = mdDoc '' The addresses to configure for this interface. Will automatically be added as this peer's allowed addresses to all other peers. ''; }; - - externalPeers = mkOption { - type = types.attrsOf (types.listOf types.str); - default = {}; - example = {my-android-phone = ["10.0.0.97/32"];}; - description = mdDoc '' - Allows defining an extra set of peers that should be added to this wireguard network, - but will not be managed by this flake. (e.g. phones) - - These external peers will only know this node as a peer, which will forward - their traffic to other members of the network if required. This requires - this node to act as a server. - ''; - }; }; }); }; @@ -148,12 +214,14 @@ in { in { assertions = concatMap (wgName: let inherit - (extraLib.wireguard wgName nodes) + (extraLib.wireguard wgName) externalPeerNamesRaw usedAddresses associatedNodes ; + wgCfg = cfg.${wgName}; + wgCfgOf = node: nodes.${node}.config.extra.wireguard.${wgName}; duplicatePeers = duplicates externalPeerNamesRaw; duplicateAddrs = duplicates (map (x: head (splitString "/" x)) usedAddresses); in [ @@ -169,9 +237,19 @@ in { assertion = duplicateAddrs == []; message = "Wireguard network '${wgName}': Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}"; } - # TODO externalPeers != [] -> server.listen - # TODO externalPeers != [] -> ip forwarding - # TODO psks only between all nodes and each node-externalpeer pair + { + assertion = wgCfg.server.externalPeers != {} -> wgCfg.server.enable; + message = "Wireguard network '${wgName}': Defining external peers requires server.enable = true."; + } + { + assertion = wgCfg.server.enable == (wgCfg.via == null); + message = "Wireguard network '${wgName}': A via server must be defined exactly iff this isn't a server node."; + } + { + assertion = wgCfg.via != null -> (wgCfgOf wgCfg.via).server.enable; + message = "Wireguard network '${wgName}': The specified via node '${wgCfg.via}' must be a wireguard server."; + } + # TODO externalPeers != {} -> ip forwarding # TODO no overlapping allowed ip range? 0.0.0.0 would be ok to overlap though ]) (attrNames cfg); diff --git a/nix/apps/format-secrets.nix b/nix/apps/format-secrets.nix index 2c9e5ef..778dbec 100644 --- a/nix/apps/format-secrets.nix +++ b/nix/apps/format-secrets.nix @@ -1,36 +1,22 @@ { self, pkgs, + nixpkgs, ... }: let - inherit - (pkgs.lib) - concatMapStrings - concatStringsSep - escapeShellArg - substring - ; - isAbsolutePath = x: substring 0 1 x == "/"; - masterIdentityArgs = concatMapStrings (x: ''-i ${escapeShellArg x} '') self.secrets.masterIdentities; - extraEncryptionPubkeys = - concatMapStrings ( - x: - if isAbsolutePath x - then ''-R ${escapeShellArg x} '' - else ''-r ${escapeShellArg x} '' - ) - self.secrets.extraEncryptionPubkeys; + inherit (nixpkgs.lib) concatStringsSep; + inherit (extraLib) rageEncryptArgs; in pkgs.writeShellScript "format-secrets" '' set -euo pipefail [[ -d .git ]] && [[ -f flake.nix ]] || { echo "error: Please execute this from the project's root folder (the folder with flake.nix)" >&2; exit 1; } for f in $(find . -type f -name '*.nix.age'); do echo "Formatting $f ..." - decrypted=$(${../rage-decrypt.sh} --print-out-path "$f" ${concatStringsSep " " self.secrets.masterIdentities}) \ + decrypted=$(${../rage-decrypt-and-cache.sh} --print-out-path "$f" ${concatStringsSep " " self.secrets.masterIdentities}) \ || { echo "error: Failed to decrypt!" >&2; exit 1; } formatted=$(${pkgs.alejandra}/bin/alejandra --quiet < "$decrypted") \ || { echo "error: Failed to format $decrypted!" >&2; exit 1; } - ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$formatted" > "$f" \ + ${pkgs.rage}/bin/rage -e ${rageEncryptArgs} <<< "$formatted" > "$f" \ || { echo "error: Failed to re-encrypt!" >&2; exit 1; } done '' diff --git a/nix/apps/generate-wireguard-keys.nix b/nix/apps/generate-wireguard-keys.nix index d73f1a9..6650138 100644 --- a/nix/apps/generate-wireguard-keys.nix +++ b/nix/apps/generate-wireguard-keys.nix @@ -17,24 +17,14 @@ unique ; - extraLib = import ../lib.nix inputs; - isAbsolutePath = x: substring 0 1 x == "/"; - masterIdentityArgs = concatMapStrings (x: ''-i ${escapeShellArg x} '') self.secrets.masterIdentities; - extraEncryptionPubkeys = - concatMapStrings ( - x: - if isAbsolutePath x - then ''-R ${escapeShellArg x} '' - else ''-r ${escapeShellArg x} '' - ) - self.secrets.extraEncryptionPubkeys; + inherit (self.extraLib) rageEncryptArgs; nodeNames = attrNames self.nodes; wireguardNetworks = unique (concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard) nodeNames); generateNetworkKeys = wgName: let inherit - (extraLib.wireguard wgName self.nodes) + (self.extraLib.wireguard wgName) allPeers associatedNodes associatedServerNodes @@ -57,7 +47,7 @@ echo "Generating "${keyBasename}".{age,pub}" privkey=$(${pkgs.wireguard-tools}/bin/wg genkey) echo "$privkey" | ${pkgs.wireguard-tools}/bin/wg pubkey > ${pubkeyFile} - ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$privkey" > ${privkeyFile} \ + ${pkgs.rage}/bin/rage -e ${rageEncryptArgs} <<< "$privkey" > ${privkeyFile} \ || { echo "error: Failed to encrypt wireguard private key for peer ${peerName} on network ${wgName}!" >&2; exit 1; } fi ''; @@ -73,7 +63,7 @@ mkdir -p $(dirname ${pskFile}) echo "Generating "${pskFile}"" psk=$(${pkgs.wireguard-tools}/bin/wg genpsk) - ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$psk" > ${pskFile} \ + ${pkgs.rage}/bin/rage -e ${rageEncryptArgs} <<< "$psk" > ${pskFile} \ || { echo "error: Failed to encrypt wireguard psk for peers ${peer1} and ${peer2} on network ${wgName}!" >&2; exit 1; } fi ''; diff --git a/nix/apps/show-wireguard-qr.nix b/nix/apps/show-wireguard-qr.nix index 489888e..eefd563 100644 --- a/nix/apps/show-wireguard-qr.nix +++ b/nix/apps/show-wireguard-qr.nix @@ -9,22 +9,57 @@ concatMap concatStringsSep escapeShellArg - filter unique ; - extraLib = import ../lib.nix inputs; + inherit (self.extraLib) rageDecryptArgs; nodeNames = attrNames self.nodes; wireguardNetworks = unique (concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard) nodeNames); externalPeersForNet = wgName: - map (peer: {inherit wgName peer;}) - (attrNames (extraLib.wireguard wgName self.nodes).allExternalPeers); + concatMap (serverNode: + map + (peer: {inherit wgName serverNode peer;}) + (attrNames self.nodes.${serverNode}.config.extra.wireguard.${wgName}.server.externalPeers)) + (self.extraLib.wireguard wgName).associatedServerNodes; allExternalPeers = concatMap externalPeersForNet wireguardNetworks; in - # TODO generate "classic" config and run qrencode pkgs.writeShellScript "show-wireguard-qr" '' set -euo pipefail - echo ${escapeShellArg (concatStringsSep "\n" (map (x: "${x.wgName}.${x.peer}") allExternalPeers))} | ${pkgs.fzf}/bin/fzf + json_sel=$(echo ${escapeShellArg (concatStringsSep "\n" (map (x: "${builtins.toJSON x}\t${x.wgName}.${x.serverNode}.${x.peer}") allExternalPeers))} \ + | ${pkgs.fzf}/bin/fzf --delimiter='\t' --ansi --multi --query="''${1-}" --tiebreak=end --bind=tab:down,btab:up,change:top,ctrl-space:toggle --with-nth=2.. --height='~50%' --tac \ + | ${pkgs.coreutils}/bin/cut -d$'\t' -f1) + [[ -n "$json_sel" ]] || exit 1 + + # TODO for each output line + # TODO maybe just call a json -> make script that gives wireguard config to make this easier + + wgName=$(${pkgs.jq}/bin/jq -r .wgName <<< "$json_sel") + serverNode=$(${pkgs.jq}/bin/jq -r .serverNode <<< "$json_sel") + peer=$(${pkgs.jq}/bin/jq -r .peer <<< "$json_sel") + + serverPubkey=$(nix eval --raw ".#extraLib" \ + --apply 'extraLib: builtins.readFile ((extraLib.wireguard "'"$wgName"'").peerPublicKeyPath "'"$serverNode"'")') + privKeyPath=$(nix eval --raw ".#extraLib" \ + --apply 'extraLib: (extraLib.wireguard "'"$wgName"'").peerPrivateKeyPath "'"$peer"'"') + serverPskPath=$(nix eval --raw ".#extraLib" \ + --apply 'extraLib: (extraLib.wireguard "'"$wgName"'").peerPresharedKeyPath "'"$serverNode"'" "'"$peer"'"') + + privKey=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} "$privKeyPath") \ + || { echo "error: Failed to decrypt!" >&2; exit 1; } + serverPsk=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} "$serverPskPath") \ + || { echo "error: Failed to decrypt!" >&2; exit 1; } + + cat <