# Automating Multi-User WireGuard setup on OpenBSD _Published: June 13, 2022_ ## Background In my previous post, I wrote about how I [started a tilde community](/blog/starting-a-tilde.html) with my friend [Anthony](https://anthonymorris.dev) in 2021. In this post I want to do a deep dive into how we set up and manage our VPN. We knew early on that we wanted to host web services such as a web-based IRC client and mailing list archives, so we decided to set up a VPN to ensure that only tilde members could access these services. It was pretty easy to settle on WireGuard as our VPN of choice--it comes in base OpenBSD (added to the kernel in recent years) and has great clients for all platforms. However, the set up can be a bit manual, and after surveying the slew of WireGuard configuration management tools out there, we decided it'd be a better learning experience and more fun to write one ourselves (that's what the tilde is all about!). ## The Setup Our tilde machine is a single VM on Linode, and we have multiple clients that need to connect to it. While developing a mesh network like Tailscale would be ideal, we don't currently have any use case for client->client communication; we just want to enable members to reach the VM (and keep non-members out). As such, the resulting network topology mirrors a hub and spoke, but the VM doesn't do any routing between clients--it can talk to each of them but they can't talk to eachother without IP forwarding enabled on the server (likewise they can't eat the VM's bandwidth by sending external traffic through it). To a client, it's really just an auth-wall and less of a network. Regardless, the tool should be usable for setups where clients do want access to eachother--the only changes needed would be to configure the Allowed IPs to send traffic to the other hosts down the wg interface and set up the sysctl.conf to allow IP forwarding. ## Setting Up One Client ### Technical Details For the purpose of this demo, we'll use 10.6.6.1 as the private IP for the VM and 10.6.6.0/24 addresses for the clients. To understand how this topology is set up, a brief overview of the "Allowed IPs" WireGuard concept is helpful. Because WireGuard is peer to peer, there are no true clients and servers. In our case we made a single server, but it doesn't change the fact that the server is just a peer with N peers (clients) and the clients are just a peer with 1 peer (the server). Allowed IPs are used both for outbound and inbound traffic in WireGuard. When sending traffic outbound, the packets are routed to the peer with the most specific matching IP range. For a client, having a peer of 10.6.6.1/32 will route all the traffic to that address to that peer. When receiving traffic from a peer, the Allowed IP range is used to filter incoming traffic. So in the case of the server, my peer (10.6.6.2) can't send back traffic pretending to be Anthony (10.6.6.3), because we configured the server's peers to have Allowed IPs of just their specific IP address (10.6.6.2/32). This server-side Allowed IP is necessary to route the traffic destined for our clients back to the right client too. ### Configuration Since the server and client are all peers, the basic information needed for each one is the same: 1. An IP address 2. A private key and the corresponding public key In addition, the server requires choosing a port so clients can find it (clients will choose their own port dynamically). Each peer that needs to communicate with another peer requires the public key of the other peer. So in our setup, the server is configured to allow traffic from all the clients by specifying their public keys and the clients require the public key of the server. As of OpenBSD 7.0, here's the configuration files we used (for how to create the keys, see "Generating the Key Combo" below). *Note:* the [`wg(4)`](https://man.openbsd.org/wg.4) man page is a great reference and should be referred to before copying any configs in case they have changed. #### Server For the server, a [`hostname.if(5)`](https://man.openbsd.org/hostname.if) file should be created (in our case `/etc/hostname.wg0` for the `wg0` interface). wgkey wgport inet 10.6.6.1 255.255.255.0 wgpeer wgaip 10.6.6.2/32 wgpeer wgaip 10.6.6.3/32 ... Where `wgpeer` defines a peer's public key and the Allowed IPs for that peer are specified by `wgaip`. Once created, the interface can be brought up with the following: # sh /etc/netstart Make sure to `chmod 600` and `chown root:wheel` that file! The private key for the server is.. uh private. #### Client The client config file looks like so: [Interface] PrivateKey = Address = 10.6.6.2/24 [Peer] PublicKey = AllowedIPs = 10.6.6.1/32 Endpoint = : The config file can be used with `wg-quick` on the client: # wg-quick up client.conf Notice that only traffic destined for the server will be routed differently (due to the specific AllowedIPs). Normal internet traffic will be sent through the default interface. ## Creating a Config Management Tool With our tool, which we called `wggen(1)`, we wanted to focus on easing the maintenance burden more so than the initial setup. While we only have a small handful of users (a couple more since I last wrote!), we knew we'd need some key management to make creating new users (and new secondary clients for existing members) easy. Looking at the setup, the things that need to be done are: 1. Finding the next available IP address on the private network 2. Creating a public/private key combo 3. Updating the server `hostname.if` file to accept traffic from the public key 4. Generating a config file for the client 5. Sending the member their config file ### Storing the Credentials For each client, we create a directory `/etc/wg/` to store the client's keys and configuration file. As a one time setup, the `/etc/wg` directory should be created with permissions set to only give the root user access: # mkdir /etc/wg # chmod 700 /etc/sg # chown root:wheel /etc/wg ### Finding an IP Address Given that we expect a small number of these, a simple solution here was to register the hosts and their IP addresses in a flat text file (managed by the tool). This file, `/etc/wg/hosts`, looks like so: server 10.6.6.1 alex 10.6.6.2 anthony 10.6.6.3 ... To find the next available IP, we make use of the fact that the file is sorted and grab the last line using `tail(1)` to see the most recently used IP. From that, we get the last digit of that line by using `cut(1)` splitting on the `.` separator. That number is all we need to determine the next IP allocated. NAME="$1" DATADIR=${DATADIR:-/etc/wg} HOSTFILE=${HOSTFILE:-${DATADIR}/hosts} CUR=$(tail -n 1 "$HOSTFILE" | cut -d. -f 4) NEXT=$((CUR + 1)) Saving the selection back is as easy as appending: echo "$NAME 10.6.6.$NEXT" >> "$HOSTFILE" ### Generating the Key Combo The private key is generated and saved into `/etc/wg/` by using the following `openssl` oneliner (from `wg(4)`): CONF="$DATADIR/$NAME" mkdir -p "$CONF" openssl rand -base64 32 > "$CONF/private.key" Obtaining the public key could use the `wg(1)` tool, but to prevent the need to install `wireguard-tools`, we used the clever _"create a temporary interface and grab the public key from that"_ trick from `wg(4)`: ifconfig wg9 destroy 2>/dev/null || true ifconfig wg9 create wgport 13421 wgkey "$(cat "$CONF/private.key")" ifconfig wg9 | grep wgpubkey | cut -d ' ' -f 2 > "$CONF/public.key" ifconfig wg9 destroy 2>/dev/null || true ### Generating the Config Generating the config is straightforward. Just a heredoc string `cat`'d into a file for safekeeping. (with the server-specific bits hardcoded but left out for the sake of publishing). cat < "$CONF/client.conf" # public key: $(cat "$CONF/public.key") [Interface] PrivateKey = $(cat "$CONF/private.key") Address = 10.6.6.$NEXT/24 [Peer] PublicKey = AllowedIPs = 10.6.6.1/32 Endpoint = : EOM ### Updating the Server's Known Peers To update the known peers, we update the existing server config file by appending the public key and the allocated IP as the AllowedIP followed by a restart of the interface: cat <> /etc/hostname.wg0 wgpeer $(cat "$CONF/public.key") wgaip 10.6.6.$NEXT/32 EOM sh /etc/netstart ### Sending the Config Sending the config is easy--we already have [email on the VM](https://garbash.com/~alex/notes/004-mail-server.html)! Using the `mail(1)` client to deliver internally is a oneliner: mail -s "Your wireguard info" "$USERNAME" < "/etc/wg/$USERNAME/client.conf" ## Conclusion Prior to writing `wggen(1)`, I'd set up WireGuard a few times on my own mostly to learn the technology. The tilde was the perfect excuse to solidify that knowledge and create something useful! As with all our little tools that came out of the tilde, the source code is FOSS and available [here](https://git.garbash.com/alex/config/file/usr/local/bin/wggen.html).