wireguard-management.txt (9185B) [raw]
1 # Automating Multi-User WireGuard setup on OpenBSD 2 3 _Published: June 13, 2022_ 4 5 ## Background 6 7 In my previous post, I wrote about how I [started a tilde 8 community](/blog/starting-a-tilde.html) with my friend 9 [Anthony](https://anthonymorris.dev) in 2021. In this post I want 10 to do a deep dive into how we set up and manage our VPN. 11 12 We knew early on that we wanted to host web services such as a 13 web-based IRC client and mailing list archives, so we decided to 14 set up a VPN to ensure that only tilde members could access these 15 services. 16 17 It was pretty easy to settle on WireGuard as our VPN of choice--it 18 comes in base OpenBSD (added to the kernel in recent years) and has 19 great clients for all platforms. However, the set up can be a bit 20 manual, and after surveying the slew of WireGuard configuration 21 management tools out there, we decided it'd be a better learning 22 experience and more fun to write one ourselves (that's what the 23 tilde is all about!). 24 25 ## The Setup 26 27 Our tilde machine is a single VM on Linode, and we have multiple 28 clients that need to connect to it. While developing a mesh network 29 like Tailscale would be ideal, we don't currently have any use case 30 for client->client communication; we just want to enable members 31 to reach the VM (and keep non-members out). 32 33 As such, the resulting network topology mirrors a hub and spoke, 34 but the VM doesn't do any routing between clients--it can talk to 35 each of them but they can't talk to eachother without IP forwarding 36 enabled on the server (likewise they can't eat the VM's bandwidth 37 by sending external traffic through it). To a client, it's really 38 just an auth-wall and less of a network. 39 40 Regardless, the tool should be usable for setups where clients do 41 want access to eachother--the only changes needed would be to 42 configure the Allowed IPs to send traffic to the other hosts down 43 the wg interface and set up the sysctl.conf to allow IP forwarding. 44 45 46 ## Setting Up One Client 47 48 ### Technical Details 49 50 For the purpose of this demo, we'll use 10.6.6.1 as the private IP 51 for the VM and 10.6.6.0/24 addresses for the clients. 52 53 To understand how this topology is set up, a brief overview of the 54 "Allowed IPs" WireGuard concept is helpful. Because WireGuard is 55 peer to peer, there are no true clients and servers. In our case 56 we made a single server, but it doesn't change the fact that the 57 server is just a peer with N peers (clients) and the clients are 58 just a peer with 1 peer (the server). 59 60 Allowed IPs are used both for outbound and inbound traffic in 61 WireGuard. When sending traffic outbound, the packets are routed 62 to the peer with the most specific matching IP range. For a client, 63 having a peer of 10.6.6.1/32 will route all the traffic to that 64 address to that peer. When receiving traffic from a peer, the Allowed 65 IP range is used to filter incoming traffic. So in the case of the 66 server, my peer (10.6.6.2) can't send back traffic pretending to 67 be Anthony (10.6.6.3), because we configured the server's peers to 68 have Allowed IPs of just their specific IP address (10.6.6.2/32). 69 This server-side Allowed IP is necessary to route the traffic 70 destined for our clients back to the right client too. 71 72 ### Configuration 73 74 Since the server and client are all peers, the basic information 75 needed for each one is the same: 76 77 1. An IP address 78 2. A private key and the corresponding public key 79 80 In addition, the server requires choosing a port so clients can find 81 it (clients will choose their own port dynamically). 82 83 Each peer that needs to communicate with another peer requires the 84 public key of the other peer. So in our setup, the server is 85 configured to allow traffic from all the clients by specifying their 86 public keys and the clients require the public key of the server. 87 88 As of OpenBSD 7.0, here's the configuration files we used (for how 89 to create the keys, see "Generating the Key Combo" below). 90 91 *Note:* the [`wg(4)`](https://man.openbsd.org/wg.4) man page is a 92 great reference and should be referred to before copying any configs 93 in case they have changed. 94 95 #### Server 96 97 For the server, a [`hostname.if(5)`](https://man.openbsd.org/hostname.if) 98 file should be created (in our case `/etc/hostname.wg0` for the 99 `wg0` interface). 100 101 wgkey <server private key> wgport <secret port> 102 inet 10.6.6.1 255.255.255.0 103 wgpeer <public key 1> wgaip 10.6.6.2/32 104 wgpeer <public key 2> wgaip 10.6.6.3/32 105 ... 106 107 Where `wgpeer` defines a peer's public key and the Allowed IPs 108 for that peer are specified by `wgaip`. 109 110 Once created, the interface can be brought up with the following: 111 112 # sh /etc/netstart 113 114 Make sure to `chmod 600` and `chown root:wheel` that file! The 115 private key for the server is.. uh private. 116 117 #### Client 118 119 The client config file looks like so: 120 121 [Interface] 122 PrivateKey = <private> 123 Address = 10.6.6.2/24 124 125 [Peer] 126 PublicKey = <public key of server> 127 AllowedIPs = 10.6.6.1/32 128 Endpoint = <server-ip>:<secret-port> 129 130 The config file can be used with `wg-quick` on the client: 131 132 # wg-quick up client.conf 133 134 Notice that only traffic destined for the server will be routed 135 differently (due to the specific AllowedIPs). Normal internet traffic 136 will be sent through the default interface. 137 138 ## Creating a Config Management Tool 139 140 With our tool, which we called `wggen(1)`, we wanted to focus on 141 easing the maintenance burden more so than the initial setup. While 142 we only have a small handful of users (a couple more since I last 143 wrote!), we knew we'd need some key management to make creating new 144 users (and new secondary clients for existing members) easy. 145 146 Looking at the setup, the things that need to be done are: 147 148 1. Finding the next available IP address on the private network 149 2. Creating a public/private key combo 150 3. Updating the server `hostname.if` file to accept traffic from the public key 151 4. Generating a config file for the client 152 5. Sending the member their config file 153 154 ### Storing the Credentials 155 156 For each client, we create a directory `/etc/wg/<client>` to store 157 the client's keys and configuration file. 158 159 As a one time setup, the `/etc/wg` directory should be created with 160 permissions set to only give the root user access: 161 162 # mkdir /etc/wg 163 # chmod 700 /etc/sg 164 # chown root:wheel /etc/wg 165 166 ### Finding an IP Address 167 168 Given that we expect a small number of these, a simple solution 169 here was to register the hosts and their IP addresses in a flat 170 text file (managed by the tool). 171 172 This file, `/etc/wg/hosts`, looks like so: 173 174 server 10.6.6.1 175 alex 10.6.6.2 176 anthony 10.6.6.3 177 ... 178 179 To find the next available IP, we make use of the fact that the 180 file is sorted and grab the last line using `tail(1)` to see the 181 most recently used IP. From that, we get the last digit of that 182 line by using `cut(1)` splitting on the `.` separator. That number 183 is all we need to determine the next IP allocated. 184 185 NAME="$1" 186 DATADIR=${DATADIR:-/etc/wg} 187 HOSTFILE=${HOSTFILE:-${DATADIR}/hosts} 188 189 CUR=$(tail -n 1 "$HOSTFILE" | cut -d. -f 4) 190 NEXT=$((CUR + 1)) 191 192 Saving the selection back is as easy as appending: 193 194 echo "$NAME 10.6.6.$NEXT" >> "$HOSTFILE" 195 196 ### Generating the Key Combo 197 198 The private key is generated and saved into `/etc/wg/<hostname>` 199 by using the following `openssl` oneliner (from `wg(4)`): 200 201 CONF="$DATADIR/$NAME" 202 mkdir -p "$CONF" 203 openssl rand -base64 32 > "$CONF/private.key" 204 205 Obtaining the public key could use the `wg(1)` tool, but to prevent 206 the need to install `wireguard-tools`, we used the clever _"create 207 a temporary interface and grab the public key from that"_ trick 208 from `wg(4)`: 209 210 ifconfig wg9 destroy 2>/dev/null || true 211 ifconfig wg9 create wgport 13421 wgkey "$(cat "$CONF/private.key")" 212 ifconfig wg9 | grep wgpubkey | cut -d ' ' -f 2 > "$CONF/public.key" 213 ifconfig wg9 destroy 2>/dev/null || true 214 215 ### Generating the Config 216 217 Generating the config is straightforward. Just a heredoc 218 string `cat`'d into a file for safekeeping. (with the 219 server-specific bits hardcoded but left out for 220 the sake of publishing). 221 222 cat <<EOM > "$CONF/client.conf" 223 # public key: $(cat "$CONF/public.key") 224 [Interface] 225 PrivateKey = $(cat "$CONF/private.key") 226 Address = 10.6.6.$NEXT/24 227 228 [Peer] 229 PublicKey = <server-public-key> 230 AllowedIPs = 10.6.6.1/32 231 Endpoint = <server-ip>:<server-port> 232 EOM 233 234 ### Updating the Server's Known Peers 235 236 To update the known peers, we update the existing server config 237 file by appending the public key and the allocated IP as the 238 AllowedIP followed by a restart of the interface: 239 240 cat <<EOM >> /etc/hostname.wg0 241 wgpeer $(cat "$CONF/public.key") wgaip 10.6.6.$NEXT/32 242 EOM 243 244 sh /etc/netstart 245 246 ### Sending the Config 247 248 Sending the config is easy--we already have [email on the 249 VM](https://alexkarle.com/garbash/004-mail-server.html)! 250 Using the `mail(1)` client to deliver internally is a oneliner: 251 252 mail -s "Your wireguard info" "$USERNAME" < "/etc/wg/$USERNAME/client.conf" 253 254 ## Conclusion 255 256 Prior to writing `wggen(1)`, I'd set up WireGuard a few times on 257 my own mostly to learn the technology. The tilde was the perfect 258 excuse to solidify that knowledge and create something useful! 259 260 As with all our little tools that came out of the tilde, the source code 261 is FOSS and available [here](https://git.alexkarle.com/garbash-config/file/usr/local/bin/wggen.html).