alexkarle.com

Source for alexkarle.com
git clone git://git.alexkarle.com/alexkarle.com.git
Log | Files | Refs | README | LICENSE

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).