Generate wireguard public key in nix

Hi all,

I have a config for several hosts that are connected using WireGuard:

{ config, self, ... }: {
  services.sumo-wireguard-new = {
    enable = true;
    server = false;
    address = "10.64.7.8";
    inherit (config.sops.secrets) wireguardPrivateKey;
    wireguardPublicKey = "dW7GMrn4OXvZIHtlKru/cglao7jouLpN9ZgUzyQdsRk=";
    hosts = self.nixosConfigurations;
  };
}

and the service definition:

{ config, lib, tools, ... }:
let
  cfg = config.services.sumo-wireguard-new;
  netmask = "/10";
  port = 51821; # don't interfere with standard wireguard port
  endpoint = "65.108.76.77:${builtins.toString port}";
  network = "vpn.sustainablemotion.io";
  ipRegex =
    "10.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])(.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){2}";
  keyRegex = "^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$";
  peer = {
    options = {
      address = lib.mkOption {
        type = with lib.types; strMatching ipRegex;
        example = "10.64.0.1";
        description = "IPv4 address on the SUMO Wireguard VPN.";
      };
      wireguardPublicKey = lib.mkOption {
        type = with lib.types; strMatching keyRegex;
        example = "L4msD0mEG2ctKDtaMJW2y3cs1fT2LBRVV7iVlWZ2nZc=";
        description = "The public key of this host.";
      };
    };
  };
in {
  options.services.sumo-wireguard-new = {
    enable = lib.mkEnableOption "SUMO Wireguard VPN configuration";
    server = lib.mkOption {
      type = with lib.types; bool;
      example = true;
      default = false;
      description = "Set to true if this is the VPN server, otherwise false.";
    };
    wireguardPrivateKey = lib.mkOption {
      type = with lib.types; attrs;
      description = "Sops secret containing the private key of this host.";
    };
    hosts = lib.mkOption {
      type = with lib.types; attrs;
      default = { };
      example = lib.options.literalExpression "self.nixosConfigurations";
      description =
        "nixosConfigurations of all systems, if this host is the VPN server all peers will be looked up, otherwise the VPN server will be looked up.";
    };
    extraPeers = lib.mkOption {
      type = with lib.types; listOf (submodule peer);
      default = [ ];
      description = "Extra peers that are not managed in this configuration";
    };
  } // peer.options;

  config = let
    hosts = lib.attrsets.mapAttrsToList (systemName: systemConfig: {
      inherit systemName;
      inherit (systemConfig.config.services.sumo-wireguard-new) address server;
    }) (lib.filterAttrs
      (_: systemConfig: systemConfig.config.services.sumo-wireguard-new.enable)
      cfg.hosts);
    servers = lib.attrsets.mapAttrsToList (_: systemConfig: {
      PublicKey =
        systemConfig.config.services.sumo-wireguard-new.wireguardPublicKey;
      AllowedIPs =
        "${systemConfig.config.services.sumo-wireguard-new.address}${netmask}";
      Endpoint = endpoint;
      PersistentKeepalive = 25;
    }) (lib.filterAttrs (_: systemConfig:
      systemConfig.config.services.sumo-wireguard-new.enable
      && systemConfig.config.services.sumo-wireguard-new.server) cfg.hosts);
    peers = lib.attrsets.mapAttrsToList (_: systemConfig: {
      PublicKey =
        systemConfig.config.services.sumo-wireguard-new.wireguardPublicKey;
      AllowedIPs =
        "${systemConfig.config.services.sumo-wireguard-new.address}/32";
    }) (lib.filterAttrs (_: systemConfig:
      systemConfig.config.services.sumo-wireguard-new.enable
      && !systemConfig.config.services.sumo-wireguard-new.server) cfg.hosts)
      ++ builtins.map (p: {
        PublicKey = p.wireguardPublicKey;
        AllowedIPs = "${p.address}/32";
      }) cfg.extraPeers;
    addresses =
      builtins.map (p: builtins.elemAt (builtins.split "/" p.AllowedIPs) 0)
      (servers ++ peers);
  in lib.mkIf cfg.enable {
    warnings = if cfg.server && peers == [ ] then
      [
        "This is a SUMO Wireguard server but the list of peers empty, no peers will be able to connect to this server."
      ]
    else
      [ ];
    assertions = [
      {
        assertion = cfg.hosts != { };
        message = "The hosts attrs can not be empty.";
      }
      {
        assertion = cfg.wireguardPrivateKey ? "path";
        message = "The SUMO Wireguard private key is not set.";
      }
      {
        assertion = (builtins.length servers) == 1;
        message =
          "Only one host can be the VPN server and one host must be the VPN server in the SUMO Wireguard network.";
      }
      {
        assertion = tools.list.duplicates addresses == [ ];
        message = ''
          Duplicate addresses found for the SUMO Wireguard network:

          ${lib.strings.concatLines (tools.list.duplicates addresses)}'';
      }
    ];

    boot.kernelModules = [ "wireguard" ];

    systemd.network = {
      netdevs = {
        "10-wg-sumo" = {
          netdevConfig = {
            Kind = "wireguard";
            Name = "wg-sumo";
          };
          wireguardConfig = {
            PrivateKeyFile = cfg.wireguardPrivateKey.path;
            ListenPort = port;
          };
          wireguardPeers = if cfg.server then peers else servers;
        };
      };
      networks = {
        "10-wg-sumo" = {
          matchConfig.Name = "wg-sumo";
          address = [ "${cfg.address}${netmask}" ];
          dns = builtins.map (h: h.address) (lib.filter (h: h.server) hosts);
          networkConfig = lib.mkIf cfg.server {
            IPMasquerade = "ipv4";
            IPv4Forwarding = true;
          };
        };
      };
    };

  meta.maintainers = with lib.maintainers; [ munnik ];
}

This works fine, but I want to achieve one additional thing. When I update the wireguardPrivateKey for a host in the secrets.yaml file and forget to update the corresponding wireguardPublicKey the WireGuard config is broken. The public key can be generated via the wg pubkey command. Is it possible to call that script in the nix config so that I don’t need to specify the public key anymore? I know that calling a script is unpure, but in this case the script itself is pure.

https://ryantm.github.io/nixpkgs/builders/trivial-builders/#trivial-builder-runCommandLocal

I tried a simple runCommandLocal and that works:

    wireguardPublicKey = builtins.readFile (pkgs.runCommandLocal "pubkey" {
      buildInputs = with pkgs; [ coreutils ];
    } ''
      echo -n "IW33U9/A/34CWz54mbzQ93dHro+zPphMSh518APbhWE=" > $out
    '');

Then I tried one that is using the secrets file. It fails, I think this is because the nix command doesn’t run as myself and therefore can’t use sops to decrypt the secrets.yaml file:

    wireguardPublicKey = builtins.readFile (pkgs.runCommandLocal "pubkey" {
      buildInputs = with pkgs; [ sops yq-go wireguard-tools ];
    } ''
      sops --decrypt ${config.sops.secrets.wireguardPrivateKey.sopsFile} | yq '.wireguardPrivateKey' | wg pubkey > $out
    '');

returns:

       error: builder for '/nix/store/wb9cyq5hjli64qfifw269zngly6h8iqq-pubkey.drv' failed with exit code 1;
       last 25 log lines:
       >
       >   age16x9muhxswj7unrnvf3ysvd7d0drjxe8stedv5fhj90pkcjxsudqssrwsr9: FAILED
       >     - | failed to load age identities: failed to open file: open
       >       | /homeless-shelter/.config/sops/age/keys.txt: no such file or
       >       | directory
       >
       >   age1t6rhfr43ktkaqjcph6vc50f8fa8aueyqxa2dvpmn952nsf88p5jsehg7f6: FAILED
       >     - | failed to load age identities: failed to open file: open
       >       | /homeless-shelter/.config/sops/age/keys.txt: no such file or
       >       | directory
       >
       >   age1rrqqz60fjxzgwh4swt8ykpqq9pu8h2zqv8q8nnynxncngt5dlywsg3909u: FAILED
       >     - | failed to load age identities: failed to open file: open
       >       | /homeless-shelter/.config/sops/age/keys.txt: no such file or
       >       | directory
       >
       >   age13djpxmsafzwp3h22c7zux2ex0h9plupjtv5vwjd8d2ws7kauk9lsja3qk6: FAILED
       >     - | failed to load age identities: failed to open file: open
       >       | /homeless-shelter/.config/sops/age/keys.txt: no such file or
       >       | directory
       >
       > Recovery failed because no master key was able to decrypt the file. In
       > order for SOPS to recover the file, at least one key has to be successful,
       > but none were.
       > /nix/store/wz6wxirp0k5x6lqvy7nmsyj1b8nm7lsy-wireguard-tools-1.0.20210914/bin/wg: Key is not the correct length or format
       For full logs, run:
         nix log /nix/store/wb9cyq5hjli64qfifw269zngly6h8iqq-pubkey.drv

I think I hit this problem: GitHub - Mic92/sops-nix: Atomic secret provisioning for NixOS based on sops