nix wireguard key gen
Mar 30, 2026 - ⧖ 50 minPublished as part of 'Wireguard Nix Flake' series.
In the first post we talked about how at a minimum a Wireguard interface needs the Wireguard private key of the current machine, and the Wireguard public keys of all the machines it is directly connected to. We solved this with our basic option setup, but this requires that users bring their own keys. In this flake we're going to automate that process so that it's easy to setup or refresh your keys whenever you want.
Project Overview
Just like last time, here is an overview of the project files after we have made our changes - as a reference.
If you want to see a file in detail, checkout the repo at: https://codeberg.org/xvrqt/wgqt-flake/src/tag/blogpost2
# Wireguard Nix Project Layout
################################
# Adds nixpkgs as an input so we can build our keygen package
# Add agenix as an input so we can decrypt our secret keys
# Imports nixosModule.nix which contains our module
flake.nix
# Imports options.nix to setup this module's options for configuration
# Imports age.nix to allow us to use encrypted secrets
# Conditionally installs the keygen package
nixosModule.nix
# Sets up the options to configure Wireguard networks
options.nix
# Sets up Agenix secrets to decrypt our secret key files
age.nix
# This directory will hold our flake's packages
# Including our first program which will generate and encrypt Wireguard keys
./packages
└─ wgqt-keygen.sh
# Keys generated by our keygen program
# This directory holds Wireguard private/public key pairs
./wireguard_keys
# This subdirectory holds Wireguard preshared keys
└─ preshared_keys/
Agenix
Agenix is a way to encrypt files with your Age public key, and then when rebuilding your system, decrypt them with your Age private key and deploy them outside the Nix Store. This guide will not be covering Agenix in depth, but to be brief, once you set up Agenix you can create 'secrets' in your Nix config which take in an encrypted file (which is safe to copy to the /nix/store), decrypt it, and deploy it outside the /nix/store/ with safer permissions. You can then reference the path to this decrypted file in other parts of your Nix config.
# Example Agenix use case
{
# wgPrivate.key is encrypted with your Age public key
# 'wgPrivate.key' is located in our flake and is copied to the /nix/store
config.age.secrets.wgPrivateKey.file = "./wgPrivate.key";
# You can use the decrypted file path in your config
# When you reference this path, Agenix will decrypt the above file and deploy
# it on your system. e.g. to /etc/wireguard/private.key
networking.wgqt.private_key_file = config.age.secrets.wgPrivateKey.path
}
The usefulness should be immediately obvious. We can generate all our Wireguard keys, encrypt them with our Age keys, and keep them in our flake. Then each of our machines can use the flake as an input, and decrypt the Wireguard keys it needs to build its interfaces. This ensures that we're encrypting secrets at rest, and that all of our keys have secure permissions, helping avoid security mistakes.
Okay, so let's do just that!
Use An Age Encrypted Private Key
We currently allow our user to set their private key to a file path string, which is fine, because as a string nothing will be copied to the /nix/store. However this also means we can't ensure it exists at deployment time. Instead we'll make it an Agenix secret, wgqtPrivateKey, and we can update this to an Agenix secret path, so by default we expect our private key to be encrypted and use the path to where the key will eventually be decrypted and deployed.
#options.nix
{
# ...
options = {
# ...
# Private key which will be used to secure all the interfaces generated
# by this NixOS module
private_key_file = mkOption {
type = types.nullOr types.str;
# Now it will default to wherever `wgPrivateKey` will be decryted to
default = config.age.secrets.wgPrivateKey.path;
example = "/etc/wireguard/privatekey.key";
description = "Path to a Wireguard private key which matches this machines Wireguard public key. If left blank it will look for an Agenix encrypted key in at `config.age.secrets.wgqtPrivateKey.file which is `./wireguard_keys/<hostname>.key` by default.";
};
# ...
};
}
The truth is more complicated
We're actually going to create an internal option which shadows private_key_file because it allows us to:
- Avoid a
nullcheck in all our downstream functions - Allow us to avoid setting up Age secrets at all if the user provides a path
- Print a nicer and less obtuse error message if there is a missing file path
The reason we can't check if private_key_file == config.age.secrets.wgqtPrivateKey.path is because that requires the secret to exist to check its path, which leads to infinite recursion.
{
# ...
options = {
# ...
# Private key which will be used to secure all the interfaces generated
# by this NixOS module
private_key_file = mkOption {
type = types.nullOr types.str;
default = null;
example = "/etc/wg/privatekey.key";
description = "Path to a Wireguard private key which matches this machines Wireguard public key. If left blank it will look for an Agenix encrypted key in at config.age.secrets.wgqtPrivateKey.file which is ./wireguard_keys/<hostname>.key by default.";
};
# Internal option which uses the path at the Age secret for this
# machine's private key. This prevents us having to do a `null` check
# every time we want to access this, while still providing a more
# helpful error than Agenix provides when the file is not set.
_pkf = mkOption {
type = types.str;
# Now it will default to wherever wgPrivateKey will be decryted to
default =
if (cfg.private_key_file == null) then
config.age.secrets.wgqtPrivateKey.path
else cfg.private_key_file;
# This prevents the option from being shown in documentation
internal = true;
description = "Path to a Wireguard private key which matches this machines Wireguard public key. Shadow option of 'private_key_file'.";
};
};
}
Creating an Agenix secret
The default above assumes that config.age.secrets.wgqtPrivateKey exists, so let's set that up now. Since our options.nix file is getting a bit long, let's create a new file age.nix to keep our concerns separate.
# age.nix
{ lib, config, ... }:
let
cfg = config.networking.wgqt;
# Don't set any of these secrets if we're not using them
cfgCheck = cfg.enable;
# Use the name of the machine given in the wgqt config
machine = cfg.machine;
in
{
config.age.secrets = lib.mkIf cfgCheck {
# Wireguard private key of this machine
wgqtPrivateKey = lib.mkDefault {
# The secret file that will be decrypted
file = ./wireguard_keys + ("/" + "${machine}.key");
# Folder to decrypt into (config.age.secretDir/'path')
name = "wg/private.key";
# File Permissions - read only by root
mode = "400";
owner = "root";
# Symlink from the secretDir to the 'path'
symlink = true;
};
};
}
There's a lot in here, but basically it will look for a file in the flake at ./wireguard_keys/<machine>.key which is encrypted with the user's Age private key, and deploy it where ever the user keeps Age secrets on their system in a wg sub directory. Lastly, we set it to read only by root, just like we did with systemd tmpfile rules (which we still keep in case the user sets their own private key path).
If we try to build now, it will fail because it cannot find ./wireguard_keys/<machine>.key - which is good! If we just set a file path string, it would build but fail at run time if this file didn't exist.
Adding Agenix to Inputs
It will also fail to build because we haven't imported the Agenix module, so config.age.secrets doesn't exists as an option to set. We can add Agenix to our inputs, and have it import its module alongside our module so these configuration options are available to use.
# flake.nix
{
inputs = {
# Used to encrypt Wireguard secret keys
agenix.url = "github:/ryantm/agenix";
};
outputs = { agenix, ... }: {
nixosModules.default = {
imports = [
# Add Agenix options to the Nix config
agenix.nixosModules.default
# WGQT Main Module
./nixosModule.nix
];
};
};
}
Nice, now we have Agenix decrypting our Wireguard private key by default, now for its public keys counter part.
Public Keys
Unlike private keys, public keys are, well, public. This means they are much simpler to use because we don't need to store them in a file, and we don't need to encrypt them, the user can supply them as a simple string in their config.
However, by default we are going to look for public key files in the ./wireguard_keys directory so we can generate them for the user later. We can use Nix's readFile function to convert them into a string.
# options.nix
{
# ...
options = {
# ...
# The option submodule type for an individual machine
machine_type = types.submodule ({ name, ... }:
let
# Public Key Default Path for machine 'name'
# Working with paths in Nix is a total nightmare
pubKeyPath = name:
trimWith
{ start = true; end = true; }
(readFile (./wireguard_keys + ("/" + "${name}.pub")));
in
{
options = {
public_key = mkOption {
# Check that the key is the correct length
type = types.addCheck types.str (s: ((stringLength s) == 44));
# By default, read in the Wireguard public key for the corresponding machine
default = pubKeyPath name;
example = "CZc/OcuvBGUGDSll32yIidvRZr4WWRpKhS/a/ccPuwA=";
description = "This machine's Wireguard public key";
};
};
};
});
}
So now we look for our Wireguard private/public key pairs in files included inside this flake by default, but still let the user define them directly if they want to BYOK. Now let's go a step further and create shell script to generate them for you.
Generating Keys
We need to generate Wireguard keys for our machine, and preferably every other machine that is a part of our Wireguard interfaces so we can reuse this flake as is on each machine in our networks.
Specifically we need to:
- Generate Wireguard private keys for each machine in our networks, and add it to
./wireguard_keys/<machine>.key - Encrypt this key with that machine's public Agenix key so only that machine can decrypt it later
- Generate the corresponding Wireguard public key for each machine's Wireguard private key, and add it to
./wireguard_keys/<machine>.pub
I do not want to do this by hand. This is 3 steps per machine, and so for a small network of a dozen machines, that is 36 steps! Boo! Hiss, even! So let's create a shell script which will generate them for us.
Script v1
Let's make a shell script which takes a list of machine names as input, and outputs a Wireguard private key for each, i.e. wgqt-keygen "serverA,serverB,laptopC,laptopD,pinephone" to a ./wireguard_keys directory.
# -u Unset variables are considered errors
# -o pipefail If any command in a pipeline fails, the first error is returned
# as the pipeline's value
set -uo pipefail
# Read our list of machine names, splitting by ',' into an array
IFS=',' read -ra MACHINES <<< "$1"
# Where to store outputs
WG_KEYS_DIR="wireguard_keys"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Styling
BOLD=$(tput bold)
NS=$(tput sgr0)
# Logging
print_info() { echo -e "$1"; }
print_warn() { echo -e "${YELLOW}$1${NC}"; }
print_error() { echo -e "${RED}$1${NC}"; }
print_success() { echo -e "${GREEN}$1${NC}"; }
################
# PRIVATE KEYS #
################
# Generate a private key per machine and store in ./PRIVATE_KEYS_DIR
# Print out if key generation succeeded, failed, or was skipped because the key
# already exists
generate_all_private_keys() {
local total_pairs=0 generated=0 failed=0 skipped=0
echo "--------------------------------"
echo "Wireguard Private Key Generation"
echo "--------------------------------"
if [ ! -d "$WG_KEYS_DIR" ]; then
print_info "Creating ${BLUE}./${WG_KEYS_DIR}${NC} "
mkdir $WG_KEYS_DIR
fi
for name in "${MACHINES[@]}"; do
generate_private_keys "$name";
case $? in
0) generated=$((generated + 1)) ;;
1) skipped=$((skipped + 1)) ;;
2) failed=$((failed + 1)) ;;
esac
total_pairs=$((total_pairs + 1))
done
echo "--------------------------------"
echo "Generated $generated keys for $total_pairs machines"
if [ $skipped -gt 0 ]; then
echo -e " └─ ${BLUE}$skipped keys skipped${NC}"
fi
if [ $failed -gt 0 ]; then
echo -e " └─ ${RED}$failed errors${NC}"
fi
}
generate_private_key() {
local peer="$1"
local private_key_file="${WG_KEYS_DIR}/${peer}.key"
if [ -f "$encrypted_file" ]; then
# Skip if it already exists
echo -e "Skipping ${YELLOW}$peer${NC}"
return 1
else
# Generate a WG Private Key
print_info "Generating private key for ${YELLOW}${BOLD}${peer}${NS}${NC}"
local priv_key
priv_key=$(wg genkey) || return 2
# Save the private key to a file
if ! echo "$priv_key" > "$private_key_file"; then
return 2
fi
echo -e " └─ Saved to: ${GREEN}./$private_key_file${NC}"
fi
}
# Away we go
main() {
generate_all_private_keys
}
main "$@"
I'm going to elide all the boilerplate at the bottom and styling stuff at the top going forward to focus on what's important. In this case it's using the wg genkey to generate a Wireguard Private key for each machine listed in our arguments.
Why styling? The only thing I hate more than writing shell scripts, is using ugly shell scripts.
Let's see the output:
[nix-shell:~/dev/wgqt-flake/packages]$ ./wgqt-keygen.sh "serverA,serverB,laptopC,laptopD,pinephone"
--------------------------------
Wireguard Private Key Generation
--------------------------------
Creating ./wireguard_keys
Generating private key for serverA
└─ Saved to: ./wireguard_keys/serverA.key
Generating private key for serverB
└─ Saved to: ./wireguard_keys/serverB.key
Generating private key for laptopC
└─ Saved to: ./wireguard_keys/laptopC.key
Generating private key for laptopD
└─ Saved to: ./wireguard_keys/laptopD.key
Generating private key for pinephone
└─ Saved to: ./wireguard_keys/pinephone.key
--------------------------------
Generated 5 keys for 5 machines
[nix-shell:~/dev/wgqt-flake/packages]$ bat wireguard_keys/laptopD.key
─────┬───────────--------------─────────────────────
│ File: wireguard_keys/laptopD.key
─────┼──────────────────────────────────────────────
1 │ GIZy3G0x3GNxovE2SwoAOz6PQB2UvKYgqo5oo3iEP0c=
─────┴──────────────────────────────────────────────
[nix-shell:~/dev/wgqt-flake/packages]$ ./wgqt-keygen.sh "serverA,serverB,laptopC,laptopD,pinephone"
--------------------------------
Wireguard Private Key Generation
--------------------------------
Skipping serverA
Skipping serverB
Skipping laptopC
Skipping laptopD
Skipping pinephone
--------------------------------
Generated 0 keys for 5 machines
└─ 5 keys skipped
Perfect. Although it's much prettier in console with the styling; you should try running it! Also, don't write that private key down please, I might use it later - your cooperation helps keeps the internet safe.
Public Keys Script
While we're generating private keys, lets add a line or two to generate their corresponding public key, print it to the console, save it a copy of it to a ./wireguard_keys/<machine>.pub.
generate_private_key() {
local machine="$1"
local public_key_file="${WG_KEYS_DIR}/${machine}.pub"
local private_key_file="${WG_KEYS_DIR}/${machine}.key"
if [ -f "$private_key_file" ]; then
echo -e "Skipping ${YELLOW}$machine${NC}"
return 1
else
print_info "Generating private key for ${YELLOW}${BOLD}${machine}${NS}${NC}"
# Generate private key
local priv_key
priv_key=$(wg genkey) || return 2
# Derive the public key from the private key
local pub_key
pub_key=$(echo "$priv_key" | wg pubkey | tee "$public_key_file") || return 2
# Encrypt the Wireguard private key using Age public key of the same
# machine. This way each machine can only decrypt its own private key.
if ! echo "$priv_key" | age -r "${MACHINES[$machine]}" -o "$private_key_file"; then
return 2
fi
echo -e " └─ Saved to: ${GREEN}./$private_key_file${NC}"
echo -e " └─ Public Key: ${BLUE}$pub_key${NC}"
fi
}
The important bit here is using wg pubkey to create the corresponding Wireguard public key from the Wireguard private key, which we print to console and also save to ./wireguard_keys/<machine>.pub.
Let's check our work.
[nix-shell:~/dev/wgqt-flake/packages]$ ./wgqt-keygen.sh "serverA,serverB,laptopC,laptopD,pinephone"
--------------------------------
Wireguard Private Key Generation
--------------------------------
Creating ./wireguard_keys
Generating private key for serverA
└─ Saved to: ./wireguard_keys/serverA.key
└─ Public Key: Phuvrpl8hN+/nik2OD6tWQOPuL/OvOlFzI45OsJ//38=
Generating private key for serverB
└─ Saved to: ./wireguard_keys/serverB.key
└─ Public Key: LxvJCebq2FqQhS98MhzkxIVL+AVJWnJygIliAKiK3Uc=
Generating private key for laptopC
└─ Saved to: ./wireguard_keys/laptopC.key
└─ Public Key: bx+q1EC6nrsl99sFrWl69PlkmSJeBmkMhq/4DfuRLlw=
Generating private key for laptopD
└─ Saved to: ./wireguard_keys/laptopD.key
└─ Public Key: k3z0xGxxd2WMf51fcEST69vKrKHzANSjOIuvuQ6Jk2w=
Generating private key for pinephone
└─ Saved to: ./wireguard_keys/pinephone.key
└─ Public Key: Jv0VJHr6F4K3AizI0sDXJQzeI6nCrPJWngaal/lObk0=
--------------------------------
Generated 5 keys for 5 machines
Sweet. Now, "Let's Encrypt!"
Encrypt Wireguard Private Keys
It's not a good idea to have our Wireguard private keys in plain text if we're going to keep them in our flake. If we ever upload it to a public repository, or do something silly like make a blog post about it, then our security is compromised. Furthermore, if any of our machines become compromised, they would have access to all the private keys, allowing them to impersonate any machine on the net, and decrypt messages from any machine. Fortunately, our default private_key_file uses an Agenix secret as its default, causing a plain text private key to error.
To keep the more secure choice convenient, we can amend our shell script a bit further:
- Take in a map of machine names to their Age public keys
- Encrypt Wireguard private keys with their Age public key
First lets update our script parameters to take in a map of machine names to Age public keys.
# wgqt-keygen.sh
# Read script input parametes into a machine:public_key map
# wgqt-keygen "serverA=KEY_FOR_SERVER_A" "serverB=KEY_FOR_SERVER_B" ...
declare -A MACHINES
for arg in "$@"; do
# String slice before the first '=' sign
key="${arg%%=*}"
# String slice after the first equal sign
value="${arg#*=}"
MACHINES["$key"]="$value"
done
Then we can update generate_private_key() to use the age program, along with each machine's Age public key, to encrypt their Wireguard private key. This will prevent other machines from decrypting and using any private key but their own.
# wgqt-keygen.sh
generate_private_keys() {
# ...
# Encrypt the Wireguard private key with its corresponding Age public key
if ! echo "$priv_key" | age -r ${MACHINES[$machine]} -o "$encrypted_file"; then
return 2
fi
echo -e " └─ Saved to: ${GREEN}./$encrypted_file${NC}"
echo -e " └─ Public Key: ${BLUE}$pub_key${NC}"
# ...
}
With age we can pass an Age public key as an argument using the -r flag which denotes 'recipients'. As in, if the owner of the Age private key receives this file it should be able to decrypt it. We can pass it in multiple times, so more than one private key can decrypt it, a feature we'll make use of later. In the meantime, let's see if it works!
[nix-shell:~/dev/wgqt-flake/packages]$ ./wgqt-keygen.sh "serverA=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPfa2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ serverA@machine"
--------------------------------
Wireguard Private Key Generation
--------------------------------
Creating ./wireguard_keys
machine: serverA
Generating private key for serverA
└─ Saved to: ./wireguard_keys/serverA.key
└─ Public Key: 1Ltdc/Y/1q2ZVIF0J5wDQk0csvZKEpGEfEHBetXyiVQ=
--------------------------------
Generated 1 keys for 1 machines
[nix-shell:~/dev/wgqt-flake/packages]$ bat wireguard_keys/serverA.key
--------------------------------------
│ File: wireguard_keys/serverA.key
--------------------------------------
1 │ age-encryption.org/v1
2 │ -> ssh-ed25519 uRS8hw 4paTo8i5gj4WpwKO+svTOCPW2sneb8BMj8qwUypLemQ
3 │ w4PrUGOJmezFv3nLZcf0/I+o4i5vv8MFTtGuBb9VufY
4 │ --- K5wp9C0fLU3CF+hcsjRN4H2HEZlYpbUvPxUlDfdAytA
5 │ ^B^FJ,�^F}��d[]^F������^Z�Y�o*�Eg{NVٔ>�E^AO)���^V^N�͍^E�������^N�x^ ^Bd^U,�^Oy?�/�ʃ
Looking good! You can see from the gibberish above that the Wireguard private key is encrypted with the Age public key instead of being in plain text.
Package Time
The keen eyed among you will have noticed my deception; my shell prompt gives away my sinful game - it is a nix-shell and this is because I needed wg and age in my $PATH for this shell script to work. That's not very hermetic of me. Let's wrap this shell script in writeShellApplication, to ensure that both programs are available at run time. We can then set it as our flake's default package, and go a step further and set that package to our default app. This lets us use nix run inside the flake to run our shell script and generate all of our keys.
# flake.nix
{
inputs = {
# Used to encrypt Wireguard secret keys
agenix.url = "github:/ryantm/agenix";
# To build our wgqt-keygen shell script
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
};
outputs = { agenix, nixpkgs, ... }:
let
# Importing Flake-Utils is for weenies
forAllSystems = function:
nixpkgs.lib.genAttrs [
"x86_64-linux"
"aarch64-linux"
]
(system: function nixpkgs.legacyPackages.${system} system);
in
rec {
nixosModules.default = {
imports = [
# Add Agenix options
agenix.nixosModules.default
# WGQT Main Module Code
./nixosModule.nix
];
};
packages = forAllSystems (pkgs: _: rec {
wgqt-keygen = pkgs.writeShellApplication {
name = "wgqt-keygen";
text = builtins.readFile ./packages/wgqt-keygen.sh;
runtimeInputs = [ pkgs.wireguard-tools pkgs.age ];
# Disable 'set -e' because we use return codes in our script
bashOptions = [ ];
};
default = wgqt-keygen;
});
apps = forAllSystems (pkgs: system: rec {
wgqt-keygen = {
type = "app";
program = "${packages.${system}.wgqt-keygen}/bin/wgqt-keygen";
};
default = wgqt-keygen;
});
};
}
Wow, our flake got quite a bit bigger, but it's mostly boilerplate to read our script, add the run time dependencies, and then add it as a package and then again as a flake 'app'. Let's try it out. From inside your wgqt-flake directory we can run nix run to get:
$ nix run
--------------------------------
Wireguard Private Key Generation
--------------------------------
--------------------------------
Generated 0 keys for 0 machines
It works, however it's not very interesting. We didn't pass it any arguments, mostly because it's a pain in the butt to type our long machine-key pairs in a terminal. Let's make an option which will do this for us.
Age Public Keys Option
Let's update our options.nix to include a keygen mode. When enabled, Wireguard interfaces will not be built, and instead it will install our keygen shell script, and print out the command you need to run to generate the keys for each of your machines.
# options.nix
{
options = {
# ...
keygen = {
# This will prevent any networks from being installed, and instead
# install a package which will generate keys for all your interfaces
# and print our the command to run on rebuild
enable = mkEnableOption "Install the `wgqt-keygen` program to generate Wireguard Keys";
# The Age public keys that correspond to each of your machines so
# that they can decrypt their corresponding private_key_file
age_public_keys = mkOption {
type = types.attrsOf types.str;
default = { };
example = {
serverA = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPfa2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ serverA@example";
serverB = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDnpWeIBR+QCwclhSqSDKTsYCLYPX0b38lYnKPYBEMM serverB@exxample";
};
description = "The Age public keys used to encrypt the Wireguard private key for each machine.";
};
};
};
# ...
};
}
By adding another 'enable' option we can extend our cfgCheck and trivially prevent our assertions and other wgqt configuration from activating while in keygen mode. This will let you easily generate new keys without having to comment out the rest of your wgqt config.
Config Example
Let's remove our other options for now, so we just have wgqt and wgqt.keygen enabled and our Age public keys set. We're also going to start using my actual homelab network because it's very annoying to test on a theoretical network of machines. Although I'm eliding some machines in the example below, for completeness the full list is: archive, lighthouse, nyaa, spark, tavern
{
networking.wgqt = {
enable = true;
keygen = {
enable = true;
age_public_keys = {
nyaa = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPgA2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ nyaa@machine";
spark = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIeunpWeIBR+QCwclhSqSDKTsYCLYPX0b38lYnKPYBEMM spark@machine";
# ...my other machines
};
};
};
}
Print the Command
Let's add an activation script to our options.nix which will print out the full command to run.
# options.nix
{
# ...
config = {
# ...
# If the user is in keygen mode, print out the command to run to generate the keys.
system.activationScripts.wgqtCommand =
let
# Generate wgqt-keygen arguments based on the Age public keys in the user's
# NixOS config.
command_args = concatMapStringsSep
"\n"
# -YES- ShellEscapeHell is REAL and it CAN hurt you!!!
(s: ">&2 echo \"${s} \\\\\"")
(map
(s: escapeShellArg s)
(attrsets.mapAttrsToList
(machine: key: "${machine}=${key}")
cfg.keygen.age_public_keys));
in
lib.mkIf keygenMode {
# Allow users to avoid rebuilding their system
# This won't install wgqt-keygen so to run the command use:
# nix run . -- <paste command here>
supportsDryActivation = true;
# Redirect to 'error' so that it is always printed as a lot of users will
# have --quiet on be default. Nix really is a ball of bad design.
text = ''
>&2 echo ""
>&2 echo "==== wgqt keygen ======"
>&2 echo "To generate Wireguard keys for wgqt, run the following command inside your wgqt flake directory:"
>&2 echo ""
>&2 echo "nix run . -- \\"
${command_args}
>&2 echo ""
'';
};
};
}
We enable it for dry runs so you don't actually have change your entire system just to print out a command. I know you keep your flake's state perfectly clean at all times so this isn't an issue for you, but I'm sure there's someone out there who forgets they're halfway through a refactor and accidentally rebuilds.
Additionally, we redirect the output to stderr so that the user will see it even if they have --quiet enabled (more likely than you think!). It's a pseudo error anyway, as we expect you to run the command and rebuild with keygen mode off to actually build a working system using wgqt interfaces.
Try it out
Now that we have enabled the keygen, and set our Age private keys, let's rebuild our system, with --dry-activate to get our command.
==== wgqt keygen ======
To generate Wireguard keys for wgqt, run the following command inside your wgqt flake directory:
nix run . -- \
'archive=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6GH/nzYFaruIZ9ZORbBhYEzTHBnrCZXSJUK2rrs1jL archive@machine' \
'lighthouse=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHqtWTbqQE5iTFZiSJ47yTicBsOlIMHqG6ojON/jTcH lighthouse@machine' \
'nyaa=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPfa2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ nyaa@machine' \
'spark=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDnpWeIBR+QCwclhSqSDKTsYCLYPX0b38lYnKPYBEMM spark@machine' \
'tavern=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZJyFDFrSKAgKvm1ZvZdGJ7fuzk+ecPMxGlPaV7Usr6 tavern@machine' \
Now we can run it inside our flake to generate our keys.
nix run . -- \
'archive=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6GH/nzYFaruIZ9ZORbBhYEzTHBnrCZXSJUK2rrs1jL archive@machine' \
'lighthouse=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHqtWTbqQE5iTFZiSJ47yTicBsOlIMHqG6ojON/jTcH lighthouse@machine' \
'nyaa=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPfa2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ nyaa@machine' \
'spark=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDnpWeIBR+QCwclhSqSDKTsYCLYPX0b38lYnKPYBEMM spark@machine' \
'tavern=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZJyFDFrSKAgKvm1ZvZdGJ7fuzk+ecPMxGlPaV7Usr6 tavern@machine' \
--------------------------------
Wireguard Private Key Generation
--------------------------------
Generating private key for tavern
└─ Saved to: ./wireguard_keys/tavern.key
└─ Public Key: IdzsNamAwzcjEkoXYktP5bWtkE7EYXpzexErz8R6ljM=
Generating private key for lighthouse
└─ Saved to: ./wireguard_keys/lighthouse.key
└─ Public Key: lpelFadeXQ4SWuvOOn+6H42VedJarDtDPXSEo1I1AA0=
Generating private key for nyaa
└─ Saved to: ./wireguard_keys/nyaa.key
└─ Public Key: ghHw0Iveef3lf39ESxMImoFblH2KAOq8kEHUWkUvPRo=
Generating private key for spark
└─ Saved to: ./wireguard_keys/spark.key
└─ Public Key: NhM74wAivLoxpkmh6Zb9rOYLFp76IP+Z9Sn2Gnmc4Rk=
Generating private key for archive
└─ Saved to: ./wireguard_keys/archive.key
└─ Public Key: Zorf4zYQVkVZauj5jgHZNiNH7fTN/ZLR6fcD9m27umU=
--------------------------------
Generated 5 keys for 5 machines
Looks like it worked, we can see that the ./wireguard_keys directory was added to our flake:
$ ls
age.nix flake.nix options.nix wireguard_keys
flake.lock nixosModule.nix packages
Inside, we can see Wireguard public/private key pairs for each machine.
$ ls wireguard_keys
archive.key lighthouse.key nyaa.key spark.key tavern.key
archive.pub lighthouse.pub nyaa.pub spark.pub tavern.pub
With all the correct keys are in the right places, we can disable keygen mode in our config, but keep age_public_keys set as we're going to use them to generate more keys, and auto complete our wgqt config in the next steps.
Preshared Keys
While we're doing all of this, we might as well generate Wireguard Preshared Keys. Wireguard preshared keys are symmetric keys1. This symmetric key adds another layer of encryption to our network, boosting security while also providing post-quantum resistance to decryption. Symmetric keys need both machines to agree on a key to use, which we can coordinate ahead of time via our flake.
These keys must remain secret, so we will encrypt them with Age again. This time we will use the Age public keys of both machines, so that either of them can decrypt the secret they share, but no one else can.
More specifically, we'll update our script to:
- Generate a preshared symmetric key for each pair of machines in the network, and save it to
./wireguard_keys/preshared_keys/<machine1>-<machine2>.key - Encrypt that file with using the Age public keys of both machines so only they can decrypt it
Adding in PSK Generation
These functions should look pretty familiar, as they are modified copies of the Wireguard private key generation functions, which instead generate and encrypt a secret for each pair of machines. You can think of this as N choose 2, or the number edges in a complete graph, which gives the following function:
$$ \binom{n}{2} \equiv \frac{n(n-1)}{2} $$
i.e. for 12 machines we must generate 66 preshared keys, plus the 24 public/private keys for a total of 90 keys, and then we have to encrypt most of these keys. What a chore to do by hand, which is why we're going to automate it.
# wgqt-keygen.sh
# Where to store outputs
WG_KEYS_DIR="wireguard_keys";
PSK_DIR="$WG_KEYS_DIR/preshared_keys";
#...
##################
# PRESHARED KEYS #
##################
# Shared symmetric key secret between two machines
# Keys are referred to by the names of both machines, and always in
# alphabetical order
generate_pair_psk() {
local machine1="$1"
local machine2="$2"
local pair_name="${machine1}-${machine2}"
local psk_file="${PSK_DIR}/${pair_name}.key"
if [ -f "$psk_file" ]; then
echo -e "Skipping ${YELLOW}$machine1-$machine2 pair${NC}"
return 1
else
print_info "Generating PSK for ${pair_name}..."
if ! wg genpsk | age -r "${MACHINES[$machine1]}" -r "${MACHINES[$machine2]}" -o "$psk_file"; then
return 2
fi
print_success " └─ Saved to: $psk_file"
fi
}
generate_all_psks() {
local total_pairs=0 generated=0 failed=0 skipped=0
echo "==================================="
echo "Wireguard Preshared Key Generation"
echo "-----------------------------------"
if [ ! -d "$PSK_DIR" ]; then
print_info "Creating ${BLUE}./${PSK_DIR}${NC}"
mkdir $PSK_DIR
fi
# Convert into an array of the names
PEERS=("${!MACHINES[@]}")
mapfile -t PEERS < <(printf "%s\n" "${PEERS[@]}" | sort -n)
for ((i=0; i<${#PEERS[@]}; i++)); do
for ((j=i+1; j<${#PEERS[@]}; j++)); do
total_pairs=$((total_pairs + 1))
generate_pair_psk "${PEERS[$i]}" "${PEERS[$j]}"
case $? in
0) generated=$((generated + 1)) ;;
1) skipped=$((skipped + 1)) ;;
2) failed=$((failed + 1)) ;;
esac
done
done
echo "-----------------------------------"
echo "Generated $generated keys for $total_pairs pairs of machines"
if [ $skipped -gt 0 ]; then
echo -e " └─ ${BLUE}$skipped keys skipped${NC}"
fi
if [ $failed -gt 0 ]; then
echo -e " └─ ${RED}$failed errors${NC}"
fi
}
#...
This should look very familiar as it's nearly identical to the code to generate Wireguard public/private key pairs, except we a key per pair of machines, and we encrypt it using both of their public keys. We sort the pairs such that we will always refer to them in alphabetical order. e.g. serverA-serverB.key but never serverB-serverA.key so we can deterministically create paths and references to these files.
Testing Output
Let's take a look at the output after rerunning our script with my computers.
nix run . -- \
'archive=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6GH/nzYFaruIZ9ZORbBhYEzTHBnrCZXSJUK2rrs1jL archive@machine' \
'lighthouse=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHqtWTbqQE5iTFZiSJ47yTicBsOlIMHqG6ojON/jTcH lighthouse@machine' \
'nyaa=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPfa2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ nyaa@machine' \
'spark=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDnpWeIBR+QCwclhSqSDKTsYCLYPX0b38lYnKPYBEMM spark@machine' \
'tavern=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZJyFDFrSKAgKvm1ZvZdGJ7fuzk+ecPMxGlPaV7Usr6 tavern@machine' \
--------------------------------
Wireguard Private Key Generation
--------------------------------
Creating ./wireguard_keys
Generating private key for tavern
└─ Saved to: ./wireguard_keys/tavern.key
└─ Public Key: DLm6zgHlzcphYmVkoB+7o6Sen/r5WSzL536Sx17xNyo=
Generating private key for lighthouse
└─ Saved to: ./wireguard_keys/lighthouse.key
└─ Public Key: 59vPi1OpACxxX6lsNd1wUoxa8ssAUV2brur1DCoym34=
Generating private key for nyaa
└─ Saved to: ./wireguard_keys/nyaa.key
└─ Public Key: zn8nJ+aTeFTK8GoUr61IEDdEIh874VykAWP+fq64DAw=
Generating private key for spark
└─ Saved to: ./wireguard_keys/spark.key
└─ Public Key: M0Y1iaN+PCHi3nUr1ZCtHUGp7SxwXIcWp1ic/v/VGho=
Generating private key for archive
└─ Saved to: ./wireguard_keys/archive.key
└─ Public Key: 4ScQYUswl4UAygYkfR3KCb3v7j5h9n5elNfr/qZjhn0=
-----------------------------------
Generated 5 keys for 5 machines
===================================
Wireguard Preshared Key Generation
-----------------------------------
Creating ./wireguard/preshared_keys
Generating PSK for archive-lighthouse...
└─ Saved to: wireguard_keys/preshared_keys/archive-lighthouse.key
Generating PSK for archive-nyaa...
└─ Saved to: wireguard_keys/preshared_keys/archive-nyaa.key
Generating PSK for archive-spark...
└─ Saved to: wireguard_keys/preshared_keys/archive-spark.key
Generating PSK for archive-tavern...
└─ Saved to: wireguard_keys/preshared_keys/archive-tavern.key
Generating PSK for lighthouse-nyaa...
└─ Saved to: wireguard_keys/preshared_keys/lighthouse-nyaa.key
Generating PSK for lighthouse-spark...
└─ Saved to: wireguard_keys/preshared_keys/lighthouse-spark.key
Generating PSK for lighthouse-tavern...
└─ Saved to: wireguard_keys/preshared_keys/lighthouse-tavern.key
Generating PSK for nyaa-spark...
└─ Saved to: wireguard_keys/preshared_keys/nyaa-spark.key
Generating PSK for nyaa-tavern...
└─ Saved to: wireguard_keys/preshared_keys/nyaa-tavern.key
Generating PSK for spark-tavern...
└─ Saved to: wireguard_keys/preshared_keys/spark-tavern.key
-----------------------------------
Generated 10 keys for 10 pairs of machines
bat wireguard_keys/preshared_keys/archive-nyaa.key
-----------------------------------------------------------
│ File: wireguard_keys/preshared_keys/archive-nyaa.key
-----------------------------------------------------------
1 │ age-encryption.org/v1
2 │ -> ssh-ed25519 VHr3rw 0g/DFOxo2OBptbrKt5MOJt+eMoJN+4dztAxZIB5Sn1s
3 │ XKz9u23LOSc20pfCGKCv4p1MSm3hM5ExfZHcvAon1wo
4 │ -> ssh-ed25519 uRS8hw rdr54EDFnwCVtleYRRiiumz/ko5pF73mKsJv32nY62I
5 │ CvTphoDHSc+Kuo7AYZ2X+/uZZ8cagBVfMG4lFqeub+o
6 │ --- 3J2puhmNMqWQpHjjd/zHWLP9uc252+tXZw5z0uf6QnA
7 │ r�w+��̧%{^@I^X��^L�o�$^A�!E��.�-���WF��?)�$�H$�K�W&tM!̖<�Q���^O��^E��k|^B��zd-�
Yup, 5 choose 2 -> 10 pairs of machines; looks good to me :]
By examining the contents of our Wireguard preshared keys we can see that our preshared keys are similarly encrypted to our Wireguard private keys we created earlier. The only difference is that now either machine can decrypt this secret, because both were listed at recipients.
# Nyaa can decrypt the key shared by Archive & Nyaa
age -d -i /key/agenix/nyaa.key wireguard_keys/preshared_keys/archive-nyaa.key
KawIcMGnl9tzdWxl3aCTFWM0xGkJFohASAEUT6mVL0E=
# Archive can decrypt the key shared by Archive & Nyaa
age -d -i /key/agenix/archive.key wireguard_keys/preshared_keys/archive-nyaa.key
KawIcMGnl9tzdWxl3aCTFWM0xGkJFohASAEUT6mVL0E=
# Archive can decrypt the key shared by Archive & Spark
age -d -i /key/agenix/archive.key wireguard_keys/preshared_keys/archive-spark.key
YmzsMcHLzh1M0aW7KfL/cdEQEVP0OCUi3k3IAL+PrSw=
# Nyaa cannot decrypt the key shared by Archive & Spark
age -d -i /key/agenix/nyaa.key wireguard_keys/preshared_keys/archive-lighthouse.key
age: error: no identity matched any of the recipients
age: report unexpected or unhelpful errors at https://filippo.io/age/report
We can see that we can only decrypt preshared keys we're recipients of, as I switch back and forth between pretending to be archive and nyaa with the -i flag.
Adding PSKs to Options
Now let's add these new preshared keys to our options. We can create an attribute set on our machine_type which maps a machine name to the preshared key it will share with the host machine. We make the value a file path string, like we did for the private_key_file to the path where the decrypted preshared key can be found.
# options.nix
{ lib, config, ... }:
with lib; with builtins;
let
# ...
machine_type = types.submodule ({ name, ... }:
let
# Always alphabetical order
pskName = name1: name2:
if builtins.lessThan name1 name2
then "${name1}-${name2}"
else "${name2}-${name1}";
# Returns the default preshared key path for the psk shared between this
# machine and the machine specified in 'name'
pskPath = name:
if (name == cfg.machine)
then null
else config.age.secrets."wgqtPSK-${pskName name cfg.machine}".path;
# ...
in
{
options = {
#...
# The preshared key between this machine and the host machine
# For the host machine, this value is null
psk = mkOption {
type = types.nullOr types.str;
# Path to our Age preshared key pair secret
default = pskPath name;
example = "serverA-serverB.key";
description = "The preshared key between the host machine and this machine. Null on the host machine's entry.";
};
};
});
}
The truth is more complicated
We're actually going to create an internal option which shadows each psk entry because it allows us to:
- Avoid a
nullcheck in all our downstream functions - Allow us to avoid setting Age secrets at all if the user provides a custom path
- Print a nicer and less obtuse error message if there is a missing file path
The reason we can't check if
wgqt.machines.<machine>.psk == config.age.secrets.wgqtPSK-<machine1>-<machine2>.pathis because that requires the secret to exist to check its path, which leads to infinite recursion.
# options.nix
{
options = {
# The preshared key between this machine and the host machine
# For the host machine, this value is null
psk = mkOption {
type = types.nullOr types.str;
default = null;
example = "serverA-serverB.key";
description = "The preshared key between the host machine and this machine. Null on the host machine's entry.";
};
# Internal option which uses the path at the Age secret for the
# preshared key between this machine and the host machine if `psk` is
# not set. This prevents us having to do a `null` check every time we
# want to access this, while still providing a more helpful error than
# Agenix provides when the file is not set.
_psk = mkOption {
type = types.nullOr types.str;
default =
if (config.networking.wgqt.machines.${name}.psk == null)
then pskPath name
else config.networking.machines.${name}.psk;
example = "serverA-serverB.key";
internal = true;
description = "The preshared key between the host machine and this machine. Null on the host machine's entry.";
};
};
}
You can see that it's a bit more complicated than private_key_file because we have to dynamically create an Agenix secret from the pair of machine names, and be sure to always list them in lexicographical order.
Adding the Agenix Secrets
Then we'll add the Age secrets to age.nix to pull the encrypted files in wireguard_keys/preshared_keys/<machineA>-<machineB>.key in by default. To do this we create a list of pairs with the host machine, and the create an Agenix secret per each pair. This means we need a consistent naming scheme of the attributes, the encrypted files, and the decrypted files so that we can deterministally create these values when we need to look them up.
# age.nix
{
# ...
# Machines used in WGQT interfaces but are not this machine
other_machines = (filter
(machine: machine != cfg.machine)
machineNames);
# Returns the name which isn't the host machines name from a pair
notMe = pair: with pair; if (peer1 == machine) then peer2 else peer1;
# Ordered list of machine pairs between this machine, and every other machine
# used in WGQT interfaces. Peer1 is always lexicographically superior to peer2.
pskPairList = (map
(other:
if lessThan machine other
then { peer1 = machine; peer2 = other; }
else { peer1 = other; peer2 = machine; })
other_machines);
# Creates the name of the file where the psk is decrypted to
# i.e. /etc/agenix/secrets/wg/<agePskFileName>
agePskDecryptFileName = pair:
with pair;
"${peer1}-${peer2}.psk.key";
# Creates the name of the age encrypted psk file for a given pair of peers
# i.e. ./wireguard_keys/preshared_keys/<agePskFileName>
agePskFileName = pair:
with pair;
"${peer1}-${peer2}.key";
# Creates the name of the age secret PSK attribute for a given pair
# i.e. config.age.secrets.<agePskAttr>.path
agePskAttr = pair:
with pair;
"wgqtPSK-${peer1}-${peer2}";
# Creates a list of pairs between this machine and all the machines it
# will directly connect to. Then creates the age attrset for their secrets.
# This will be merged with the wgqtPrivateKey Agenix secret above
# We only create Agenix secrets if the user is not BYOK and the encrypted
# file exists.
pskPairedSecrets =
lib.listToAttrs (filter
(x: x != null)
(map
(pair:
let
other = notMe pair;
agePSKFile = ./wireguard_keys/preshared_keys + ("/" + agePskFileName pair);
pskSet = (cfg.machines.${other}.psk != null);
pskCheck = (!pskSet) && (pathExists agePSKFile);
in
if pskCheck then {
name = agePskAttr pair;
value = {
# The secret file that will be decrypted
file = ./wireguard_keys/preshared_keys + ("/" + "${agePskFileName pair}");
# Folder to decrypt into (config.age.secretDir/'path')
name = "wg/${agePskDecryptFileName pair}";
# File Permissions
mode = "400";
owner = "root";
# Symlink from the secretDir to the 'path'
# Doesn't matter since both are in the same partition
symlink = true;
};
} else null)
pskPairList));
}
This creates only the secrets needed for this machine, and skips any which the user has supplied themselves. We can then merge the wgqtPrivateKey with the many preshared key secrets, and add it to the config's Agenix secrets module.
# age.nix
{
#...
config = mkIf cfgCheck {
# Merge the private key with the preshared secrets
age.secrets = (wgqtPK // pskPairedSecrets);
#...
};
}
Agenix Assertions
We can add some checks to let the user know if they're missing any keys, and what to do about it.
# age.nix
{
let
# ...
##############
# Assertions #
##############
# Check if the private key path exists if it is set.
# If it is not set, check that the default path's backing file is set
agePKFile = config.age.secrets.wgqtPrivateKey.file;
privateKeySet = (cfg.private_key_file != null);
privateKeyCheck = privateKeySet || (pathExists agePKFile);
# Check that a file has been set for all machine's preshared keys attr
missingPSKs =
(filter
(x: x != null)
(map
(pair:
let
other = notMe pair;
agePSKFileExists = pathExists (./wireguard_keys/preshared_keys + ("/" + agePskFileName pair));
pskSet = (cfg.machines.${other}.psk != null);
pskCheck = pskSet || agePSKFileExists;
in
if pskCheck
then null
else
"${other}: ./wireguard_keys/preshared_keys/${agePskFileName pair}")
pskPairList
));
presharedKeysCheck = ((length missingPSKs) == 0);
in
config = mkIf cfgCheck {
# ...
assertions = [
# Check that the private key file exists
{
assertion = privateKeyCheck;
message = ''No private key file found at ${cfg.private_key_file} check your settings `networking.wgqt.private_key_file` or set `wgqt.keygen.enable = true;` and rebuild to generate keys.'';
}
# Check that all the expected preshared keys exist
{
assertion = presharedKeysCheck;
message =
''
Cannot find the age encrypted preshared key file for:
${concatLines missingPSKs}
Either set a path to the decrypted preshared key for each at `config.networking.wgqt.<machine>.psk` or set `wgqt.keygen.enable = true;` and rebuild to automatically generate preshared keys.
'';
}
];
};
}
I also moved the private key assertion into age.nix to help take some pressure off of options.nix.
So let's test it out by deleting some of my preshared keys, say for spark and tavern and rebuilding on nyaa;
error:
Failed assertions:
- Cannot find the age encrypted preshared key file for:
spark: ./wireguard_keys/preshared_keys/nyaa-spark.key
tavern: ./wireguard_keys/preshared_keys/nyaa-tavern.key
Either set a path to the decrypted preshared key for each at `config.networking.wgqt.<machine>.psk` or set `wgqt.keygen.enable = true;` and rebuild to automatically generate preshared keys.
Terrific.
Cleaning Up Options
Let's take a look back at our actual wgqt config to see if it's look ergonomic.
# My NixOS Config
{
networking.wgqt = {
enable = true;
keygen = {
enable = false;
# I keep my Age public keys in my config so I can always use them
# It also makes it convenient to elide them here.
age_public_keys = config.secrets.age.public_keys.machines;
};
machine = "nyaa";
# The public keys look for ./wireguard_keys/<machine>.pub by default
# So we don't need to define them for now.
machines = {
archive = { };
lighthouse = { };
nyaa = { };
spark = { };
tavern = { };
};
}
Oh dear, this looks kind of silly. Since we have sensible defaults, machines is just a map of empty objects. I'm sure we'll fill them up eventually, but we can set a default machines using our public_age_keys map. After all, we're generating keys for the machines we intend to use, so there's no need to write them down twice.
# options.nix
{
options = {
# ...
# Attrset of machines that can be part of wgqt networks
machines = mkOption {
type = types.attrsOf machine_type;
# Turn it in a map of empty objects, with a key for each machine for us
default = mapAttrs (name: _: { }) cfg.keygen.age_public_keys;
description = "An attribute set of your machines, containing their public key. If you fill out `age_public_keys` this will auto populate with the machines listed there.";
};
};
}
This allows us to drop the machines attrset entirely.
# My NixOS Config
{
networking.wgqt = {
enable = true;
keygen = {
enable = false;
age_public_keys = secrets.publicKeys.machines;
};
machine = "nyaa";
}
Now that I think about it, we can set a default for machine too. Let's have it default to the hostname of the system, since that's what people typically use for network names anyways.
# options.nix
{
options = {
# ...
# This Machine's name
# Must match one of the keys in the machines attribute set
machine = mkOption {
description = "The name of the machine listed in networking.wgqt.machines which is the machine for which the interface will be built (i.e. this machine). It uses the hostname of the system by default.";
type = types.str;
default = config.networking.hostName;
example = "hostname";
};
};
}
Leaving us with
# My NixOS Config
{
networking.wgqt = {
enable = true;
# Disabled by default
keygen.age_public_keys = config.secrets.age.public_keys.machines;
}
Very peaceful, don't you think? :]
We can check that our options are correctly set with help from the nix repl by changing our working directory to our Nix system flake and running :lf . to load our flake and then :p nixosConfigurations.<machine>.config.networking.wgqt to pretty print our wgqt module's computed config.
{
_pkf = "/key/secrets/wg/private.key";
enable = true;
keygen = {
age_public_keys = {
archive = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6GH/nzYFaruIZ9ZORbBhYEzTHBnrCZXSJUK2rrs1jL archive@machine";
lighthouse = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHqtWTbqQE5iTFZiSJ47yTicBsOlIMHqG6ojON/jTcH lighthouse@machine";
nyaa = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINtHIPfa2+AQGIHZcBRLgkIx+3mhwEt/zf5ClP2AVvZ+ nyaa@machine";
spark = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEDnpWeIBR+QCwclhSqSDKTsYCLYPX0b38lYnKPYBEMM spark@machine";
tavern = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZJyFDFrSKAgKvm1ZvZdGJ7fuzk+ecPMxGlPaV7Usr6 tavern@machine";
};
enable = false;
};
machine = "nyaa";
machines = {
archive = {
_psk = "/key/secrets/wg/archive-nyaa.psk.key";
psk = null;
public_key = "c1iq1pv7e9XULn25pWtq60SMmx9K6kNFp/TEe2FYcXg=";
};
lighthouse = {
_psk = "/key/secrets/wg/lighthouse-nyaa.psk.key";
psk = null;
public_key = "xkvGZmEcEWLa6qL0Op6R9cvKlLG0b4nE4d0MztuX8EM=";
};
nyaa = {
_psk = null;
psk = null;
public_key = "eQV7qsq0W5SF8Sghg4CJJl6TuQwf8ryrneUFhuQW3Q8=";
};
spark = {
_psk = "/key/secrets/wg/nyaa-spark.psk.key";
psk = null;
public_key = "h+bBptoR27q+EibZ8S91alrPkLC8GEApmSfyevVAx34=";
};
tavern = {
_psk = "/key/secrets/wg/nyaa-tavern.psk.key";
psk = null;
public_key = "RTtENusoZoA6VC1bTU6VRKWdnRRDOrVjFLGPxhQZICs=";
};
};
private_key_file = null;
}
If you're wondering about the _pkf and _psk entries, then you are simply afraid of the truth.
Fin
OKAY. We have our keys, and we have our basic configuration, so now we're finally ready to configure Wireguard interfaces. In the next post we set up the most simple use case, a network traffic proxy.