feat(wireguard): generate psks only if needed; add most of the qr code generator

This commit is contained in:
oddlama 2023-04-15 01:51:33 +02:00
parent 925d3856e0
commit d5f2880457
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
13 changed files with 225 additions and 108 deletions

View file

@ -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 = {

View file

@ -21,5 +21,8 @@
};
};
extra.wireguard.vms.address = ["10.0.0.10/32"];
extra.wireguard.vms = {
via = "ward";
addresses = ["10.0.0.10/32"];
};
}

View file

@ -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"];
};
}

View file

@ -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"];
};
}

View file

@ -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);

View file

@ -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
''

View file

@ -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
'';

View file

@ -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 <<EOF | tee /dev/tty | ${pkgs.qrencode}/bin/qrencode -t ansiutf8
[Interface]
Address =
PrivateKey = $privKey
[Peer]
PublicKey = $serverPubkey
PresharedKey = $serverPsk
AllowedIPs =
Endpoint =
EOF
''

View file

@ -28,5 +28,5 @@ in {
rageImportEncrypted = identities: nixFile:
assert assertMsg (builtins.isPath nixFile) "The file to decrypt must be given as a path to prevent impurity.";
assert assertMsg (hasSuffix ".nix.age" nixFile) "The content of the decrypted file must be a nix expression and should therefore end in .nix.age";
exec ([./rage-decrypt.sh nixFile] ++ identities);
exec ([./rage-decrypt-and-cache.sh nixFile] ++ identities);
}

View file

@ -10,19 +10,14 @@
agenix-rekey,
...
} @ inputs: let
inherit
(nixpkgs.lib)
optionals
;
extraLib = import ./lib.nix inputs;
inherit (nixpkgs.lib) optionals;
in
nodeName: nodeMeta: {
inherit (nodeMeta) system;
pkgs = self.pkgs.${nodeMeta.system};
specialArgs = {
inherit (nixpkgs) lib;
inherit extraLib;
inherit (self) extraLib;
inherit inputs;
inherit nodeName;
inherit nodeMeta;

View file

@ -1,9 +1,15 @@
{nixpkgs, ...}: let
{
self,
nixpkgs,
...
}: let
inherit
(nixpkgs.lib)
attrNames
attrValues
concatMap
concatMapStrings
escapeShellArg
filter
flatten
foldl'
@ -12,6 +18,7 @@
mergeAttrs
nameValuePair
partition
substring
unique
;
in rec {
@ -33,8 +40,24 @@ in rec {
# Concatenates all given attrsets as if calling a // b in order.
concatAttrs = foldl' mergeAttrs {};
# True if the path or string starts with /
isAbsolutePath = x: substring 0 1 x == "/";
rageMasterIdentityArgs = concatMapStrings (x: ''-i ${escapeShellArg x} '') self.secrets.masterIdentities;
rageExtraEncryptionPubkeys =
concatMapStrings (
x:
if isAbsolutePath x
then ''-R ${escapeShellArg x} ''
else ''-r ${escapeShellArg x} ''
)
self.secrets.extraEncryptionPubkeys;
# The arguments required to de-/encrypt a secret in this repository
rageDecryptArgs = "${rageMasterIdentityArgs}";
rageEncryptArgs = "${rageMasterIdentityArgs} ${rageExtraEncryptionPubkeys}";
# Wireguard related functions that are reused in several files of this flake
wireguard = wgName: nodes: rec {
wireguard = wgName: rec {
sortedPeers = peerA: peerB:
if peerA < peerB
then {
@ -64,25 +87,27 @@ in rec {
# All nodes that are part of this network
associatedNodes =
filter
(n: builtins.hasAttr wgName nodes.${n}.config.extra.wireguard)
(attrNames nodes);
(n: builtins.hasAttr wgName self.nodes.${n}.config.extra.wireguard)
(attrNames self.nodes);
# Partition nodes by whether they are servers
_associatedNodes_isServerPartition =
partition
(n: nodes.${n}.config.extra.wireguard.${wgName}.server.enable)
(n: self.nodes.${n}.config.extra.wireguard.${wgName}.server.enable)
associatedNodes;
associatedServerNodes = _associatedNodes_isServerPartition.right;
associatedClientNodes = _associatedNodes_isServerPartition.wrong;
# Maps all nodes that are part of this network to their addresses
nodePeers = genAttrs associatedNodes (n: nodes.${n}.config.extra.wireguard.${wgName}.address);
nodePeers = genAttrs associatedNodes (n: self.nodes.${n}.config.extra.wireguard.${wgName}.addresses);
externalPeerName = p: "external-${p}";
# Only peers that are defined as externalPeers on the given node.
# Prepends "external-" to their name.
externalPeersForNode = node:
mapAttrs' (p: nameValuePair "external-${p}") nodes.${node}.config.extra.wireguard.${wgName}.externalPeers;
mapAttrs' (p: nameValuePair (externalPeerName p)) self.nodes.${node}.config.extra.wireguard.${wgName}.server.externalPeers;
# All peers that are defined as externalPeers on any node.
# Prepends "external-" to their name.
@ -92,11 +117,11 @@ in rec {
allPeers = nodePeers // allExternalPeers;
# Concatenation of all external peer names names without any transformations.
externalPeerNamesRaw = concatMap (n: attrNames nodes.${n}.config.extra.wireguard.${wgName}.externalPeers) associatedNodes;
externalPeerNamesRaw = concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.${wgName}.server.externalPeers) associatedNodes;
# A list of all occurring addresses.
usedAddresses =
concatMap (n: nodes.${n}.config.extra.wireguard.${wgName}.address) associatedNodes
++ flatten (concatMap (n: attrValues nodes.${n}.config.extra.wireguard.${wgName}.externalPeers) associatedNodes);
concatMap (n: self.nodes.${n}.config.extra.wireguard.${wgName}.addresses) associatedNodes
++ flatten (concatMap (n: attrValues self.nodes.${n}.config.extra.wireguard.${wgName}.server.externalPeers) associatedNodes);
};
}

View file

@ -15,8 +15,11 @@
self,
nixpkgs,
...
} @ inputs:
with nixpkgs.lib; let
} @ inputs: let
inherit
(nixpkgs.lib)
mapAttrs
;
# If the given expression is a bare set, it will be wrapped in a function,
# so that the imported file can always be applied to the inputs, similar to
# how modules can be functions or sets.