My Automagic NixOS Wireguard Setup

Published on , 1096 words, 4 minutes to read

It's been a while since I went into detail about how my Site to Site Wireguard setup works. I've had a lot of time to think about how I can improve it since then, and I think I've come to a new setup that I'm happy with. I've replaced all of the manual setup, copying/pasting and more with a unified network metadata file and some generators that consume it. Here's my logic, influences and the details about how I implemented it.

When I worked at IMVU one of the most useful services was the asset database. This database ended up being a giant bag of state that a lot of the other SRE services consumed. This was used by the machine provisioner, DHCP server and the configuration management. My personal infrastructure isn't quite big enough yet to justify setting up a whole database for tracking it all, however I think I have a happy middle path with a file called hosts.toml.

At a high level it contains the following information:

Here's a random host description from hosts.toml:

network = "hexagone"
ip_addr = ""
ssh_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL3Jt26HXD7mLNjg+B+pB5+fXtxEmMeR6Bqv1Z5/819n"

pubkey = "S8XgS18Z8xiKwed6wu9FE/JEp1a/tFRemSgfUl3JPFw="
port = 51820
addrs = { v4 = "", v6 = "ed22:a601:31ef:e676:e9bd" }

This includes enough information for me to do the following things:

I also have two functions that generate peer configs from this metadata, roamPeer and serverPeer.

The main difference between these two functions is that serverPeer allows me to tell the target machine to actively reach out to the peer, whereas roamPeer sets up config for the other end to connect to that peer. This allows me to stick a machine behind a NAT firewall and still have it connect to the network.

I have two main peerlists based on the location of the machine in question:

# expected peer lists
hexagone = [
  # cloud
  (serverPeer lufta)
  (serverPeer firgu)
  (serverPeer kahless)
  # hexagone
  (serverPeer chrysalis)
  (serverPeer keanu)
  (serverPeer shachi)
  (serverPeer genza)

cloud = [
  # cloud
  (serverPeer lufta)
  (serverPeer firgu)
  (serverPeer kahless)
  # hexagone
  (roamPeer chrysalis)
  (roamPeer keanu)
  (roamPeer shachi)
  (roamPeer genza)

Inside hexagone, all of the machines can freely contact eachother. These IP addresses aren't very useful for cloud servers, so those servers get a roaming peer config.

Now that I have these peer lists all I need to do is generate the base Wireguard config for that machine. At a minimum we need to set the following:

So we do this in the very imaginatively named function interfaceInfo:

interfaceInfo = { network, wireguard, ... }:
    net = metadata.networks."${network}";
    v6subnet = net.ula;
  in {
    ips = [
    privateKeyFile = "/root/wireguard-keys/private";
    listenPort = wireguard.port;
    inherit peers;

interfaceInfo takes host information from hosts.toml and combines it with a peerlist in order to tell NixOS all it needs to set up the Wireguard interface. With this information plus the peerlists from before, we can set up host configurations:

hosts = {
  # hexagone
  chrysalis = interfaceInfo chrysalis hexagone;
  keanu = interfaceInfo keanu hexagone;
  shachi = interfaceInfo shachi hexagone;
  genza = interfaceInfo genza hexagone;

  # cloud
  lufta = interfaceInfo lufta cloud;
  firgu = interfaceInfo firgu cloud;

And then I can set up a akua.nix file in the host configuration folder that looks something like this:

{ config, pkgs, ... }:

let metadata = pkgs.callPackage ../../ops/metadata/peers.nix { };
in {
  networking.wireguard.interfaces.akua =

  within.secrets.wg-privkey = {
    source = ./secrets/wg.privkey;
    dest = "/root/wireguard-keys/private";
    owner = "root";
    group = "root";
    permissions = "0400";

Then when I push to my machines next, the new Wireguard config will be pushed across the network, seamlessly integrating any new machine into the mesh.

Mara is hmm

Wait, you have other machines like an iPad, iPhone and MacBook and I didn't see you detail those anywhere in this network. How do you manage Wireguard for them?

I don't!

I actually use Tailscale's subnet routing to handle this. I have my tower at home expose a route for and then it all works out automagically. Sure it doesn't expose everything if my tower goes and stays down, however in that case I'm probably going to just make one of my cloud servers into the subnet router.

Small disclaimer: Tailscale is my employer. I am not speaking for them with this section. I use them for this because it solves the problem I have with this so well that I don't have to care about this anymore. Seriously this has removed so much manual process from my Wireguard networks it's not even funny. I was a Tailscale user before I was a Tailscale employee.

Mara is hmm

Is it really a good idea to include those Wireguard public keys in a public git repo like that?

They are public keys, however I have no idea if it really is a good idea or not. It hasn't gotten me hacked yet (as far as I'm aware), so there's probably not much of a practical issue.

My logic behind making my NixOS config repo a public one is to act as an example for others to take inspiration from. I also wanted to make it harder for me to let the config drift. It also gives me a bunch of fodder for this blog.

This is basically what my setup has turned into. It's super easy to manage now. If I want to add machines to the network, I just generate a new wirguard keypair, modify hosts.toml and then push out the config to the network. That's it. It's beautiful and I love it.

Feel free to take inspiration from this setup. I'm sure you can do it in a nicer way somehow (maybe put the metadata table into the nix file itself? that way it would work on NixOS stable), but this works amazingly for my needs.

Facts and circumstances may have changed since publication. Please contact me before jumping to conclusions if something seems wrong or unclear.

Tags: wireguard, nixos, tailscale