1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-11 07:10:39 +02:00

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;
};
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;
};
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))
})
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,21 +166,6 @@ in {
type = types.bool;
description = mdDoc "Whether to open the firewall for the specified `listenPort`, if {option}`listen` is `true`.";
};
};
priority = mkOption {
default = "20";
type = types.str;
description = mdDoc "The order priority used when creating systemd netdev and network files.";
};
address = 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);
@ -139,6 +181,30 @@ in {
'';
};
};
priority = mkOption {
default = "20";
type = types.str;
description = mdDoc "The order priority used when creating systemd netdev and network files.";
};
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.
'';
};
};
});
};
@ -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.