diff --git a/README.md b/README.md index b66b293..7dca090 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,8 @@ $ echo -e "paste the big text result from nix repl in here" ``` # Integrations: -By default, WireNix supports setting wireguard keypairs with agenix-rekey. +By default, WireNix supports setting wireguard keypairs with +[agenix-rekey](https://github.com/oddlama/agenix-rekey). WireNix also supports networkd, network manager, and the nixos static network configuration (default). diff --git a/agenix-rekey-compat.nix b/agenix-rekey-compat.nix new file mode 100644 index 0000000..787de05 --- /dev/null +++ b/agenix-rekey-compat.nix @@ -0,0 +1,36 @@ +{ config, lib, ... }@inputs: +with lib; +with import ./lib.nix; +{ +options = { + wirenix = { + enable = mkOption { + default = true; + type = with lib.types; bool; + description = '' + Wirenix + ''; + }; + secretsDir = mkOption { + type = types.path; + description = mdDoc '' + where you want the wireguard secrets stored. + ''; + }; + }; + }; + config = + let + configurers = defaultConfigurers // config.modules.wirenix.additionalConfigurers; + parsers = defaultParsers // config.modules.wirenix.additionalParsers; + acl = config.modules.wirenix.aclConfig; + parser = parsers."${acl.version}" inputs; + configurer = configurers."${config.modules.wirenix.configurer}" inputs; + nixosConfigForPeer = peerName: builtins.head (builtins.attrValues ( + lib.attrsets.filterAttrs ( + name: value: (lib.attrsets.attrByPath ["config" "modules" "wirenix" "peerName"] null value) == peerName + ) nixosConfigurations)); + in + lib.mkIf (config.modules.wirenix.enable) + configurer (parser acl) config.modules.wirenix.peerName; +} \ No newline at end of file diff --git a/configurers/static.nix b/configurers/static.nix index 8781b5c..6a8288b 100644 --- a/configurers/static.nix +++ b/configurers/static.nix @@ -1,32 +1,43 @@ -{lib, ...}: intermediateConfig: peerName: +{lib, ...}@inputs: keyProviders: intermediateConfig: peerName: with lib.trivial; with lib.attrsets; with lib.lists; with lib; +with builtins; +with import ../lib.nix; let thisPeer = intermediateConfig.peers."${peerName}"; # these aren't really important, I just wanted to reverse the argument order forEachAttr' = flip mapAttrs'; forEachAttrToList = flip mapAttrsToList; - mergeIf = attr: key: if builtins.hasAttr key attr then {"${key}" = attr."${key}";} else {}; + keyProvidersInit = map (x: x inputs intermediateConfig peerName) keyProviders; + getPeerPubKey = otherPeerName: findFirst (x: x != null) (throw "Wirenix: Could not find public key for " + otherPeerName) + (map (provider: provider.getPeerPubKey otherPeerName) keyProvidersInit); + getPrivKeyFile = getPrivKeyFile (x: x != null) (throw "Wirenix: Could not find private key file for " + peerName) + (map (provider: provider.getPrivKeyFile) keyProvidersInit); + getPubKey = findFirst (x: x != null) (throw "Wirenix: Could not find public key for " + peerName) + (map (provider: provider.getPubKey) keyProvidersInit); + getSubnetPSKFile = subnetName: findFirst (x: x != null) (null) + (map (provider: provider.getSubnetPSKFile subnetName) keyProvidersInit); in { networking.wireguard = { - interfaces = forEachAttr' thisPeer.subnetConnections (name: subnetConnection: { name = "wg-${name}"; + interfaces = forEachAttr' thisPeer.subnetConnections (subnetName: subnetConnection: { name = "wn-${subnetName}"; value = { ips = subnetConnection.ipAddresses; listenPort = subnetConnection.listenPort; - privateKeyFile = thisPeer.privateKeyFile; - peers = forEachAttrToList subnetConnection.peerConnections (peerName: peerConnection: + privateKeyFile = getPrivKeyFile; + peers = forEachAttrToList subnetConnection.peerConnections (otherPeerName: peerConnection: { - name = peerName; - publicKey = peerConnection.peer.publicKey; + name = otherPeerName; + publicKey = getPeerPubKey otherPeerName; + presharedKeyFile = getSubnetPSKFile subnetName; allowedIPs = peerConnection.ipAddresses; endpoint = "${peerConnection.endpoint.ip}:${builtins.toString peerConnection.endpoint.port}"; - } // - (mergeIf peerConnection.endpoint "persistentKeepalive") // - (mergeIf peerConnection.endpoint "dynamicEndpointRefreshSeconds") // - (mergeIf peerConnection.endpoint "dynamicEndpointRefreshRestartSeconds") + } + // (mergeIf peerConnection.endpoint "persistentKeepalive") + // (mergeIf peerConnection.endpoint "dynamicEndpointRefreshSeconds") + // (mergeIf peerConnection.endpoint "dynamicEndpointRefreshRestartSeconds") ); };} ); diff --git a/key-providers/acl.nix b/key-providers/acl.nix new file mode 100644 index 0000000..94fdbab --- /dev/null +++ b/key-providers/acl.nix @@ -0,0 +1,10 @@ +{lib, ...}: intermediateConfig: +with import ../lib.nix; +with lib.attrsets; +with builtins; +{ + config = {}; + getPeerPubKey = peerName: attrByPath [peerName "publicKey"] null intermediateConfig.peers; + getPeerPrivKeyFile = peerName: attrByPath [peerName "privateKeyFile"] null intermediateConfig.peers; + getSubnetPSK = subnetName: attrByPath [subnetName "presharedKeyFile"] null intermediateConfig.subnets; +} \ No newline at end of file diff --git a/key-providers/agenix-rekey.nix b/key-providers/agenix-rekey.nix new file mode 100644 index 0000000..f1a6ab7 --- /dev/null +++ b/key-providers/agenix-rekey.nix @@ -0,0 +1,33 @@ +{config, nixosConfigurations, lib, ...}: intermediateConfig: peerName: +with (import ../lib.nix); +with lib.attrsets; +with builtins; +let secretsDir = peerName: (nixosConfigForPeer nixosConfigurations peerName).config.modules.wirenix.secrestsDir; in +{ + config = { + age.generators.wireguard-priv = {pkgs, file, ...}: '' + priv=$(${pkgs.wireguard-tools}/bin/wg genkey) + ${pkgs.wireguard-tools}/bin/wg pubkey <<< "$priv" > ${lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub")} + echo "$priv" + ''; + age.generators.wireguard-psk = {pkgs, file, ...}: '' + psk=$(${pkgs.wireguard-tools}/bin/wg genpsk) + echo "$psk" + ''; + age.secrets = { + age.secrets = { + "wirenix-peer-${peerName}" = { + rekeyFile = config.modules.wirenix.secretsDir + /wirenix- + peerName + ".age"; + generator.script = "wireguard-priv"; + }; + } // mapAttrs' (name: value: nameValuePair ("wirenix-subnet-${name}") { + rekeyFile = config.modules.wirenix.subnetSecretsDir + /wirenix-subnet- + name + ".age"; + generator.script = "wireguard-psk"; + }) intermediateConfig.peers."${peerName}".subnetConnections; + + }; + getPeerPubKey = otherPeerName: lib.removeSuffix ".age" ((secretsDir otherPeerName).config.secrets."wirenix-peer-${peerName}".path) + ".pub"; + getPrivKeyFile = config.age.secrets."wirenix-peer-${peerName}".path; + getPubKey = lib.removeSuffix ".age" (config.age.secrets."wirenix-peer-${peerName}".path) + ".pub"; + getSubnetPSKFile = subnetName: config.age.secrets."wirenix-subnet-${subnetName}".path; +} \ No newline at end of file diff --git a/lib.nix b/lib.nix index bac3b4f..6f14ca9 100644 --- a/lib.nix +++ b/lib.nix @@ -12,7 +12,12 @@ rec { auto = static; # TODO: make smart static = import ./configurers/static.nix; networkd = import ./configurers/networkd.nix; - networkmanager = import ./configurers/networkmanager.nix; + network-manager = import ./configurers/networkmanager.nix; + }; + /** Builtin key providers */ + defaultKeyProviders = { + acl = import ./key-providers/acl.nix; + agenix-rekey = import ./key-providers/agenix-rekey.nix; }; /** listOfSetsToSetByKey :: string -> list -> attrSet * Example: @@ -87,4 +92,11 @@ rec { ); in mapAttrs (name: value: recurse "" value) intermediateConfig; + + nixosConfigForPeer = nixosConfigurations: peerName: builtins.head (builtins.attrValues ( + lib.attrsets.filterAttrs ( + name: value: (lib.attrsets.attrByPath ["config" "modules" "wirenix" "peerName"] null value) == peerName + ) nixosConfigurations)); + + mergeIf = attr: key: if builtins.hasAttr key attr then {"${key}" = attr."${key}";} else {}; } \ No newline at end of file diff --git a/parsers/v1.nix b/parsers/v1.nix index a0d2622..c36e679 100644 --- a/parsers/v1.nix +++ b/parsers/v1.nix @@ -8,25 +8,24 @@ let /** parsePeer :: acl_peer -> ic_peer */ parsePeer = acl_peer: { subnetConnections = listOfSetsToSetByKey "name" (pipeMap [subnetFromName (getSubnetConnectionAndName acl_peer)] (attrNames acl_peer.subnets)); - publicKey = acl_peer.publicKey; - privateKeyFile = acl_peer.privateKeyFile; - } // - (if acl_peer ? extraArgs then {extraArgs = acl_peer.extraArgs;} else {}) // - { - publicKey = acl_peer.publicKey; - privateKeyFile = acl_peer.privateKeyFile; - } // - (if acl_peer ? groups then {groups = map groupFromName acl_peer.groups;} else {groups = {};}); + } + // mergeIf acl_peer "extraArgs" + // mergeIf acl_peer "publicKey" + // mergeIf acl_peer "privateKeyFile" + // (if acl_peer ? groups then {groups = map groupFromName acl_peer.groups;} else {groups = {};}); /** parseGroup :: acl_group -> ic_group */ parseGroup = acl_group: { peers = mapListOfSetsToSetByKey "name" parsePeer (selectPeers [{type="group"; rule="is"; value="${acl_group.name}";}]); - } // (if acl_group ? extraArgs then {extraArgs = acl_group.extraArgs;} else {}); + } + // mergeIf acl_group "extraArgs"; /** parseSubnet :: acl_subnet -> ic_subnet */ parseSubnet = acl_subnet: { peers = mapListOfSetsToSetByKey "name" parsePeer (selectPeers [{type="subnet"; rule="is"; value="${acl_subnet.name}";}]); - } // (if acl_subnet ? extraArgs then {extraArgs = acl_subnet.extraArgs;} else {}); + } + // mergeIf acl_subnet "extraArgs" + // mergeIf acl_subnet "presharedKeyFile"; /** getSubnetConnection :: acl_peer -> acl_subnet -> (subnetConnection // {name}) */ getSubnetConnectionAndName = acl_peer: acl_subnet: { @@ -35,8 +34,9 @@ let ipAddresses = getIpAddresses acl_subnet acl_peer; listenPort = acl_peer.subnets."${acl_subnet.name}".listenPort; peerConnections = getPeerConnections acl_peer acl_subnet; - } // (if acl_peer.subnets."${acl_subnet.name}" ? extraArgs then {extraArgs = acl_peer.subnets."${acl_subnet.name}".extraArgs;} else {}); - + } + // mergeIf (getAttr acl_subnet.name acl_peer.subnets) "extraArgs"; + /** getIpAddresses :: acl_peer -> acl_subnet -> [str] */ getIpAddresses = acl_subnet: acl_peer: if (acl_peer.subnets."${acl_subnet.name}" ? ipAddresses) then ( @@ -133,4 +133,5 @@ in peers = mapListOfSetsToSetByKey "name" parsePeer v1_acl.peers; subnets = mapListOfSetsToSetByKey "name" parseSubnet v1_acl.subnets; groups = mapListOfSetsToSetByKey "name" parseGroup v1_acl.groups; -} // (if v1_acl ? extraArgs then {extraArgs = v1_acl.extraArgs;} else {}) \ No newline at end of file +} +// mergeIf v1_acl "extraArgs" \ No newline at end of file diff --git a/wire.nix b/wire.nix index a4785ca..859d4d2 100644 --- a/wire.nix +++ b/wire.nix @@ -22,27 +22,33 @@ with import ./lib.nix; ''; }; configurer = mkOption { - default = "auto"; - type = types.str; + default = defaultConfigurers.static; + defaultText = literalExpression "wirenix.lib.defaultConfigurers.static"; + type = with types; functionTo (functionTo (functionTo (functionTo attrset))); description = mdDoc '' - Configurer to use. Builtin values can be "auto", "networkmanager", or "networkd". - See the `additionalConfigurers` for adding more options. + Configurer to use. Builtin values can be + `wirenix.lib.defaultConfigurers.static` + `wirenix.lib.defaultConfigurers.networkd` or + `wirenix.lib.defaultConfigurers.network-manager` + Or you can put your own configurer here. ''; }; - additionalConfigurers = mkOption { - default = "auto"; - type = with types; attrsOf (functionTo attrset); + keyProviders = mkOption { + default = [defaultKeyProviders.acl]; + type = with types; listOf (functionTo attrset); + defaultText = literalExpression "[ wirenix.lib.defaultKeyProviders.acl ]"; description = mdDoc '' - Additional configurers to load, with their names being used to select from the - configurer option. + List of key providers. Key providers will be queried in order. + Builtin providers are `wirenix.lib.defaultKeyProviders.acl` + and `wirenix.lib.defaultKeyProviders.agenix-rekey`. The latter + requires the agenix-rekey flake. ''; }; additionalParsers = mkOption { - default = "auto"; type = with types; attrsOf (functionTo attrset); description = mdDoc '' Additional parsers to load, with their names being used to compare to the acl's - "version" feild. + "version" field. ''; }; aclConfig = mkOption { @@ -52,6 +58,19 @@ with import ./lib.nix; Shared configuration file that describes all clients ''; }; + secretsDir = mkOption { + type = types.path; + description = mdDoc '' + If using a secrets manager, where you have wireguard secrets stored for the client. + ''; + }; + subnetSecretsDir = mkOption { + type = types.path; + description = mdDoc '' + If using a secrets manager, where you have wireguard secrets stored for subnets. + Needs to be the same on all clients. + ''; + }; }; }; @@ -59,12 +78,12 @@ with import ./lib.nix; config = let - configurers = defaultConfigurers // config.modules.wirenix.additionalConfigurers; parsers = defaultParsers // config.modules.wirenix.additionalParsers; acl = config.modules.wirenix.aclConfig; parser = parsers."${acl.version}" inputs; - configurer = configurers."${config.modules.wirenix.configurer}" inputs; + configurer = config.modules.wirenix.configurer inputs; + keyProviders = config.modules.wirenix.keyProviders; in lib.mkIf (config.modules.wirenix.enable) - configurer (parser acl) config.modules.wirenix.peerName; + configurer (parser acl) keyProviders config.modules.wirenix.peerName; } \ No newline at end of file