Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

0-1: Introduction

Welcome! This course is for anyone interested in creating their own private networks using Wireguard. In just a few short modules, we’ll dive into the essentials of the Wireguard technology: how it works, why it matters, and how to deploy it correctly. We’ll also demonstrate multiple deployment strategies, from a fully manual network, all the way to a managed mesh ready to use for secure community-building.

Learning Objectives

As always with TTI Courses, we break down our learning objectives into Skills and Concepts.

Skills

By the end of this course, learners will be able to:

  • Create and connect Wireguard peers
  • Establish Wireguard networks across NAT boundaries
  • Manage complex Wireguard networks using Netbird

Concepts

By the end of this course, learners will understand:

  • Wireguard tunnel design
  • Wireguard asymmetric cryptography usage
  • Wireguard networking strategies
  • Basic mesh networking principles
  • Netbird deployment considerations

Prerequisites

To succeed in this course, learners should have experience with:

  • The Linux command line (start here)
  • Basic IP networking concepts, including:
    • Subnets
    • Network Address Translation (NAT)
    • Firewall rules

Materials

To fully participate in this course, you will need:

  • A computer you can install things on—Linux is ideal, but Windows will work
  • A recent version of Podman
  • The Just command runner
  • A cloud server for the final deployment

The cloud server will cost real money, but will become an asset for you in the long-term. We will demonstrate with Digital Ocean, but other providers (Hetzner, AWS, Azure, GCP, Vultr) will work as well, if you’re familiar with them.

Self-Directed Learning

Taking a course like this on your own time, without direct guidance from an instructor, can be a daunting challenge. Self-directed learning requires time, discipline, and commitment in order to be successful. To help you along the way, TTI has a short course dedicated to the how and why of self-directed learning. We encourage all new TTI students to first complete The Learning Journey

How to Use This Course

At TTI, we don’t waste our time with multiple choice questions. Guessing isn’t learning. Instead, at the end of each lesson, we provide Checks for Understanding. These are questions that you should be able to answer yourself, in detail. We will not grade them: you will. It works like this:

  1. Read the question carefully. It is usually a “How” or “Why” question that requires a thoughtful response.
  2. Craft the response in writing or out loud—to yourself, or someone else. If speaking aloud, consider recording the answer.
  3. Think about how it felt to answer. Were you sure? Were there gaps in your explanations? If so, you have work to do. If not, you’re ready to move on to the next lesson.

Join our Community

Learning is a social undertaking. Connect with other learners taking this course, or on similar paths, by joining the TTI Discord! We hope to see you there.

The Exhibition of Mastery

The end of every TTI course comes with a recommended “capstone” project that we call the Exhibition of Mastery. It’s an opportunity to prove to yourself and others that you have gained the skills and understandings you hoped from this course. We do not grade these—only you can assess whether you think you’ve accomplished your goals—but we encourage you to share them with our community!

Statement on AI

no AI logo

No generative models were used in the creation of the text or code of this course. Everything you see here was created by the author.

0-2: Setup

Since this course relies on a few specialized tools, it’s worth taking the time to get them set up properly.

Podman

We use Podman instead of Docker as our container runtime to create ad-hoc networks for our labs.

Follow Podman’s Installation Instructions for macOS and Linux.

For Windows, we will take advantage of Windows Subsystem for Linux. Make sure it’s installed and ready to go, and use a WSL instance as the base for our work.

just

just is a command runner. We use it to simplify complex shell commands and create a unified tool for all the tasks in the lab.

There are myriad ways to install just. Use whichever one suits your environment best. For Linux, I recommend grabbing the pre-packaged binaries—unless you’re a Rust/Cargo user, in which case cargo is best. For Windows, just is available via winget, and that’s probably best for that platform.

Zellij

Because we’re going to be running so many containers at one time, Zellij will help corral them into a single view. Zellij splits a single terminal window into several, allowing easy switching between shells. Install it from the releases page.

Cloud VPS Provider

The last section of the course involves setting up a cloud server as the “hub” for a Wireguard-powered network. Our design requires a public server that all network nodes can see at all times, hence the cloud service. This will cost some money, although what we’re doing can be accomplished at a minimum of expense.

We will be demonstrating the process with Digital Ocean, but the concepts will map onto any provider of your choosing.

For now, make sure you have an account ready to go.

Clone the Repository

The Codeberg Repository has all the code you’ll need to perform the labs in this course. If you’re comfortable with Git on the command line, run the following command:

git clone https://codeberg.org/The-Taggart-Institute/wireguard-from-scratch

Otherwise, use the “download ZIP” option available from the repository homepage.

download zip menu option

Initial Setup

Once downloaded, navigate to the repository folder. Make sure just is working by running:

just help

You should see “available recipes.” If so, then complete the repo setup by running:

just setup

That’s it for setup. Let’s get into the course material.

1-1: What is Wireguard?

Wireguard is an encrypted networking protocol designed for secure peer-to-peer networking, and as an alternative to more “traditional” private networking technologies. It uses strong, modern cryptography to secure tunneled data and plaintext configuration files to govern its operation.

Wireguard is baked into the Linux kernel, making it extremely fast and simple to deploy on servers. But Wireguard also has implementations for about any platform you can imagine.

Differences from Other VPNs

In the past, you may have used VPN technologies like OpenVPN or built-in IPSec tunnels through enterprise firewalls. Wireguard differs from these in its simplicity and flexibility. You do not need a firewall to run a Wireguard network (although many can).

Additionally, Wireguard networks can take many different shapes. In a traditional VPN, you have one or many “external” clients gain access to an internal network via a tunneling service hosted by a firewall or edge device, granting the external device a presence on the internal network. While Wireguard can function this way, it operates on the principle of connecting peers together. What the peers do with the Wireguard traffic after the connection is established is up to them. This subtle difference significantly alters how we approach Wireguard and what we can do with it.

For a much deeper dive on how Wireguard works, I recommend reading their protocol explainer and their whitepaper.

What Can You Do with Wireguard?

Wireguard excels at building trusted networks across and independent of existing networking layers. Here are some use cases:

The Self-Hoster

Suppose you have a well-appointed homelab (perhaps with some guidance from a high-quality resource?). You want to publish your blog and a few other applications to the internet, but you don’t want to open any ports on your home router. With Wireguard, you can use a cloud-based server as a simple reverse proxy, with a point-to-point Wireguard tunnel between your in-home assets (properly isolated, of course) and the public reverse proxy. The result: internet availability of your apps without inviting a flood of attack traffic to your home router.

The Activist

Suppose you’re an activist that wants to coordinate with others in your organization, but you don’t want to advertise this activity to the prying eyes of an oppressive state. They may have visibility at the ISP level, meaning general traffic across the internet should be considered insecure. Commercial VPNs shift the risk to the VPN provider, but that is little improvement. With Wireguard, you can create a network of peers, between which all sensitive traffic (shared resources, communications) can be routed through that network, and wholly unreadable by any service provider.

The Archivist

Suppose you’re a collector of media. You have built an impressive collection of movies, TV shows, and music over the years. With streaming costs skyrocketing and crackdowns on sharing of account credentials, you want your extended family to be able to access your media library with a minimum of fuss.

By creating a Wireguard connection between your media server and your family’s home networks, the server becomes available without setting up a full-tunnel VPN between your home and theirs.

Built on Wireguard

Several professional tools have been built on Wireguard’s core technology. The most well-known is Tailscale, which is sort of Wireguard-as-a-service. It takes the basic tunneling technology, adds some light coordination and host configuration, and produces a highly resilient and flexible mesh network. The “open core” Netmaker provides a similar service, as does the actually open source Netbird.

Check For Understanding

  • How does Wireguard differ from other VPNs?

1-2: How Wireguard Works

Before diving in to running Wireguard commands, it’s worth a moment to explore Wireguard’s design. It’s fairly simple!

Like all private networking, Wireguard encrypts packets of data between a sender and a receiver. Wireguard’s elegance is in how that encryption and send takes place.

Asymmetric Cryptography

Every member of a Wireguard network, also known as a Peer, is associated with a cryptographic keypair. You may have encountered public/private keys before, such as with SSH or HTTPS certificates. This is not a cryptography course, so we won’t go too far into the weeds on this, but remember that the public key is shared with the world, and the private key is kept secret.

In Wireguard, every Peer is identified on the network by their public key, not their IP address! IP addresses change, but keys should remain the same no matter what.

If you’re curious, the keys are 256-bit Curve25519 points.

Key Exchange

When two Peers want to communicate, a shared key for the session is established via Wireguard’s handshake protocol. The keypairs from each party are used to establish a shared encryption key for the session. Optionally, a preshared key is also used for encrypting the session, which adds additional cryptographic strength and quantum resistance to the secrets.

Sending Data

When sending data to a Peer, the destination address is referenced on a cryptokey routing table that connects known public keys to allowed IP addresses for that key. If there’s a match between the destination address and an allowed IP, the appropriate session is used for encrypting the data. If no such session exists, a new one is created using the respective keypairs.

The packet is encrypted with the ChaCha20-Poly1305 authenticated encryption algorithm.

UDP

Any type of network data may be sent over a Wireguard connection, but the connection itself uses User Datagram Protocol (UDP) for transmission. This keeps the tunnel extremely lightweight.

Okay, enough theory. Let’s walk through a simple Wireguard connection to see how this all works in practice.

Check For Understanding

  • How are Peers identified in a Wireguard network?
  • How does Wireguard decide where to send data?

1-3: Wireguard Configuration

Note

This section involves code in the Git repository for this course. If you haven’t already, download it/clone it to a computer with the required software installed.

A Wireguard connection is defined by a configuration file. Wireguard uses these files, written in INI format, to create a network interface to handle Wireguard traffic. Let’s look at a simple Wireguard config to understand its structure.

# This section defines the local Wireguard interface
[Interface]
# The configuration doesn't actually need the PublicKey, but we keep it here
# for convenience
#PublicKey = mowyK+B79BzJDSvMOcKI7NpZZ/db8n6SnHhootGXdgk=
PrivateKey = 2BPdFaSlX15arFSnAcfw9RcjlkqoR2GQZ+/QEnZBZV0=
Address = 172.16.100.1/24
ListenPort = 51820


# This is where we define peers we connect to, or who
# connect to us
[Peer]
# This key must be provided to us
PublicKey = 4OgoXDOzpacd+DGY2SNFSkcax8lsaiYxIISsKQvVV3I=
# Pay attention to the CIDR notaton!
AllowedIPs = 172.16.100.2/32

The [Interface]

The first section of a Wireguard config file contains definitions for the local Wireguard interface. There are additional optional settings, but you need the PrivateKey and the Address at the very least. I also like to define ListenPort so any firewall rules are predictable. This is all Wireguard needs to bring up an interface (we’ll discuss how that works shortly), but we need additional information to connect to Peers.

The [Peer]s

For each Peer in our Wireguard network, we’ll need a unique [Peer] entry. Each of these requires at least a PublicKey and an AllowedIPs definition. That’s it!

Well, sorta. That’s it if the Peer you’re configuring with this file will be receiving initial Wireguard connections. I’m being careful here to not call such a Peer a “server,” but functionally that’s what it is. When the network is brought up, somebody has to receive initial handshakes. If you’re the recipient, then advertising your interface and defining Peer keys/allowed source addresses is sufficient.

On the other hand, if you’re the one initiating a connection, you need an additional definition for the Peer to which you’re connecting:

[Peer]
# This key must be provided to us
PublicKey = 4OgoXDOzpacd+DGY2SNFSkcax8lsaiYxIISsKQvVV3I=
AllowedIPs = 172.16.100.2/32
# The accessible Host:Port where the remote Wireguard
# interface is listening. Can be a hostname, FQDN, or IP.
Endpoint = remote.test.com:51820

Endpoint tells Wireguard where to look to initiate the handshake.

And then, one more thing. Wireguard is lazy. It will shut down inactive connections, which means you might find Peers unavailable after a long period of inactivity. To prevent this, we add a PersistentKeepalive entry with a number of seconds between which to send heartbeat packets to keep the session up.

So altogether, a Peer entry for an outgoing connection looks like:

[Peer]
# This key must be provided to us
PublicKey = 4OgoXDOzpacd+DGY2SNFSkcax8lsaiYxIISsKQvVV3I=
AllowedIPs = 172.16.100.2/32
# The accessible Host:Port where the remote Wireguard
# interface is listening. Can be a hostname, FQDN, or IP.
Endpoint = remote.test.com:51820
PersistentKeepalive = 25

Let’s fire this up in the lab to see how it all works.

Check For Understanding

  • What is the difference between [Interface] and [Peer] sections of a Wireguard configuration?
  • Why is PersistentKeepalive important?

1-4: LAB | A Simple Wireguard Network

Our first lab!

Make sure you have everything installed from Setup. Then, with your terminal in the repository directory, run:

just up 1-4

Whoah! What just happened?

zellij grid

Zellij has taken a single terminal window and split into four. In the top left quadrant is your OS shell. Bottom left is peer-1; top right is peer-2; and bottom right is peer-3. These are the machines we want to connect together in our Wireguard network. But each one needs some help.

Configuring the Peers

In any of the three Peer terminal windows, run the following Bash command:

cat /etc/wireguard/lab_1-4.conf

You’ll get something back that looks like:

# Peer 1
[Interface]
#PublicKey = <<PUBLIC_KEY>>
PrivateKey = <<PRIVATE_KEY>>
Address = <<WG_ADDRESS>>
ListenPort = 51820

About like what we discussed in the last section, right? But there’s some information missing from each config file. Pay attention to the comments for each [Peer] entry across the configs. Each node needs each other node’s information, and it’s easy to get mixed up as you fill these in.

How do you fill these in, anyhow?

The Wireguard Command Line

The wg command is used to manage Wireguard connections. It also has utilities for generating private a public keys for use in configs.

wg genkey creates a new private key, represented in base64 encoding. wg pubkey takes such a key from stdin and outputs the associated public key in the same format. That’s not the most ergonomic, but it allows us to easily create a keypair on the command line. Goes like this:

privkey=$(wg genkey)
pubkey=$(echo $privkey | wg pubkey)
echo $privkey
echo $pubkey

Interface Keys

Each container has both Nano and Helix editors installed, so you can manually fill in these values. The <<PUBLIC_KEY>> and <<PRIVATE_KEY>> placeholder values are there for a reason, though! We can use the sed command line tool to surgically replace those values.

sed -i "s#<<PUBLIC_KEY>>#$pubkey#" /etc/wireguard/lab_1-4.conf
sed -i "s#<<PRIVATE_KEY>>#$privkey#" /etc/wireguard/lab_1-4.conf

"s#<<PUBLIC_KEY>>#pubkey" instructs sed to replace the <<PUBLIC_KEY>> pattern with value in $pubkey.

The tee command outputs the result of sed to the command line as well as writing the result back to the named file. That way you can review the changes.

Note

Advanced command line users may notice something weird about our sed invocations. The # delimiter is used instead of the more common / between command components because base64 strings can themselves contain /. Since sed doesn’t care what we use as a separator, we choose an alternative.

On each of your three Peer shells, run the commands to generate the keys and add them to the respective configs.

Now, what about that Address field?

Interface Address

We need to decide on an address space to use. I didn’t hardcode this because I don’t know what your network environment is like. Choose a subnet that is different from your normal network. If your primary IP address is something like 192.168.1.10, maybe consider an address space like 172.16.100.0/24. We’ll use that in these examples.

Note

By default, Podman uses the 10.88.0.0/16 subnet for its own networking. If you run ip address show (or ip a s) in one of your lab containers, you’ll see such an address assigned to the container.

All the Peers in our Wireguard network will have an address in the same subnet. Wireguard does not supply addresses for you, so you will need to keep straight what Peer is assigned to what address. I like to keep it simple, so Peer 1 will get 172.16.100.1/24, Peer 2 will get 172.16.100.2/24, and Peer 3 will get 172.16.100.3/24.

Since there’s a <<WG_ADDRESS>> placeholder in the config, we can repeat our sed trick for each Peer with these addresses.

# On Peer 1
sed -i "s#<<WG_ADDRESS>>#172.16.100.1/24#" /etc/wireguard/lab_1-4.conf
# On Peer 2
sed -i "s#<<WG_ADDRESS>>#172.16.100.2/24#" /etc/wireguard/lab_1-4.conf
# On Peer 3
sed -i "s#<<WG_ADDRESS>>#172.16.100.3/24#" /etc/wireguard/lab_1-4.conf

Peer Data

Now comes the finicky part. You’ll likely want to have a text editor open on each terminal pane with the configurations open.

With the keys and addresses filled in for each Peer, we need to fill in the respective addresses across the configs. Here’s the breakdown of what keys and addresses need to go where.

  • Peer 1: Peer 2 and 3
  • Peer 2: Peer 1 and 3
  • Peer 3: Peer 1 and 2

Fill in the public keys from each to each. Now let’s talk about AllowedIPs.

Note

Zellij will try its best to copy highlighted text in any pane to your system clipboard. See if it works!

AllowedIPs

This is the most misunderstood entry in the config file. That’s because it means something slightly different depending on whether you are sending Wireguard data or receiving it. For senders, AllowedIPs serves as a routing rule: any address that matches the subnet listed there will be sent over that Wireguard tunnel. But for receivers, it means that only traffic from the subnets/addresses listed will be accepted.

In this case, that’s a distinction without a difference because we’re making point-to-point connections between Peers. But in networks where a single Peer handles incoming traffic for multiple other Peers, this difference matters greatly, and can lead to confusion.

Anyway, taking our addresses above as an example, here is how the AllowedIPs settings should look on each Peer:

  • Peer 1:
    • Peer 2: 172.16.100.2/32
    • Peer 3: 172.16.100.3/32
  • Peer 2:
    • Peer 1: 172.16.100.1/32
    • Peer 3: 172.16.100.3/32
  • Peer 3:
    • Peer 1: 172.16.100.1/32
    • Peer 2: 172.16.100.2/32

Get those set up and we’re nearly finished.

Endpoint

Endpoint defines the non-Wireguard address at which to attempt to contact a given Peer. We’ve predetermined the listening port to be 51820, but we still need to know the IP address (or the hostname) for each Peer.

We can of course run ip address show on each Peer to get their current network IP address, and use those in our configs, but Podman offers another option. Because each Peer can resolve the hostnames of other containers in the ad-hoc network for the deployment, we can use the hostname alone for this trick.

Prove for yourself that hostnames resolve. From Peer 1, run:

ping -c 4 lab_1-4_peer-2

See replies? Et voilà, we have hostname resolution! So for each Peer entry, the Endpoint should be like: lab_1-4-peer-N, where N is the peer number.

Putting it all together, here’s what a complete config would look like for Peer 1. Keep in mind the keys for your configuration will be completely different.

# Our Interface
[Interface]
#PublicKey = SU5dSM/fPQFirdIkGr9CfTwY/6Il9o3rLoblhuZzQ1U=
PrivateKey = EAMBHm/P48+4EDsQ8d4YnOJarNFFxthagDqc/y2I2Fs=
Address = 172.16.100.1/24
ListenPort = 51820

# Fill these in with detail from the other two peers!

[Peer]
PublicKey = mpl+K0M9U04nlrlVNAPeiggVPi19PZaG6A47KhKHckw=
AllowedIPs = 172.16.100.2/32
Endpoint = lab_1-4_peer-2:51820
PersistentKeepalive = 25

[Peer]
PublicKey = +4h5gepZN/o+N6BNrcKcTUHNvi9U21IJUDYgAvl++R8=
AllowedIPs = 172.16.100.3/32
Endpoint = lab_1-4_peer-3:51820
PersistentKeepalive = 25

Save your configs on each Peer. Now we’re ready to bring the Wireguard network up!

Launching the Network

The wg-quick command allows us to quickly spin Wireguard interfaces up and down on a system. It takes two arguments: an interface action (up, down), and the name of either an existing Wireguard interface, or the name of a config file in the config folder—in our case, /etc/wireguard.

On each Peer, run:

wg-quick up lab_1-4

The output should look like:

[#] ip link add dev lab_1-4 type wireguard
[#] wg setconf lab_1-4 /dev/fd/63
[#] ip -4 address add 172.16.100.1/24 dev lab_1-4
[#] ip link set mtu 65440 up dev lab_1-4 

If you receive an error and an ip link delete command, check your config files for mistakes.

If the network came up, you can run wg show on any Peer and you should see the status of the Wireguard connection, including data sent between nodes. Which probably won’t look like much yet. That’s okay though; we’re just confirming the network functions. To send some data over the Wireguard tunnel, try this from Peer 1:

ping -c 4 172.16.100.2
ping -c 4 172.16.100.3

When you’re ready, run this to bring the lab down:

just down 1-4

Congratulations! You’ve made your first Wireguard network! We did everything the hard way in this lab to get familiar with the basic components of a Wireguard config. Some of this will be automated as we move forward.

2-1: The Lighthouse

In the previous Unit, we explored the basics of connecting Wireguard nodes. However, the network we built only works when every node can directly connect to every other node. Think of this as “line of sight.” If all the nodes are in the same subnet, or accessible behind the same router, our setup works great.

flowchart
  A[Peer A] --> B[Peer B]
  B --> C[Peer C]
  A --> C

But what happens when the nodes can’t all see each other? Imagine I want to create a Wireguard network with my family to securely share photos. Nodes in my house can see one another just fine, but family members who don’t live with me can’t see those devices!

The two homes’ networks are isolated from the general internet, and therefore from each other. In order to send messages from one to another, we need a relay somewhere on the internet that’s visible from both home networks.

We’ll call that relay a “lighthouse.” Why? Mostly because it sounds cooler than relay, and it connotes visibility.

In a Lighthouse model, the lighthouse is the single [Peer] entry in every other Peer’s config. The lighthouse is then responsible for properly routing the traffic to the right connected Peer.

In essence, the lighthouse is a router, but only for Wireguard traffic.

flowchart
  subgraph internet [Internet]
  C[Peer C] <--> L[Lighthouse]
  end
  subgraph home1 [Home Network A]
  A[Peer A] <--> L
  end
  subgraph home2 [Home Network B]
  B[Peer B] <--> L
  end

  style internet fill:blue
  style home1 fill:purple
  style home2 fill:maroon

Lighthouse Configuration

There is exactly one new change for our Lighthouse Peer to serve its role: it needs IP packet forwarding enabled. There are multiple methods to enable this setting on Linux systems. However, because we are running inside of a container, we will set this configuration within the container specification in podman-compose.yml.

Peers

The Lighthouse will have listed Peers in its configuration, just as before. But the Lighthouse Peer entries will not have Endpoint directives. That’s because the Lighthouse won’t be making outgoing handshakes! It is acting as a server, receiving and routing communications rather than initiating connections.

Peer Configuration

In the last network, each Peer needed [Peer] listings for every other member of the Wireguard network. Not only does that scale very poorly (imagine adding 100+ Peer entries to every new network member), but in the case of a Lighthouse model, it’s not necessary. Because we are providing the Lighthouse, with a guaranteed (to the best of our ability) line-of-sight from all Peers, the only [Peer] entry everyone else needs is the Lighthouse itself. Only the Lighthouse needs an entry for every known Peer.

Okay, enough theory. Let’s light the beacon!

Check For Understanding

  • Why can’t nodes on separate networks connect directly to each other?

2-2: LAB | A Lighthouse Network

Lab Overview

Lab Mechanics

Under the hood, this lab is a bit trickier than the last one. I wanted to make clear what’s going on for transparency and to avoid confusion.

The files discussed here are all in the labs/2-2 directory in the course repo.

First, you’ll notice a up.sh shell script. This is a helper script used by just to coordinate the building of Podman images necessary for the lab. The script also kicks off a second script, setup.sh, inside of each container that it runs with podman compose up.

That script, located in labs/2-2/peer/ automates the Wireguard key generation we performed manually last time. So when the containers pop up, you’ll find pre-filled keypairs in the Wireguard config files.

You’ll also notice a lighthouse directory in labs/2-2. The lighthouse container is slightly different from a regular Peer, and this folder contains the modified config we need. It will build a container image called wg-lighthouse:2-2 to go along with wg-peer:2-2.

Network Design

Take a moment to review the podman-compose.yml file in labs/2-2. Even if you’re not familiar with compose files (wanna be?), you should be able to suss out what’s going on broadly. We have 3 “services” we’re standing up: a “home” Peer, a “roaming” Peer, and the Lighthouse. Note that each has a networks section, with both a named network and a specific IP address.

Note

For simplicity’s sake, we’re reducing the non-home networks to a single “roaming” subnet. In reality, the local IP of the roaming device would be a private address, much like the home one. But the separation from the home device is the real purpose of this lab, so we’ll accept one fewer network.

You might also notice that in the Lighthouse service, there is a unique sysctls entry. This is how we enable IP forwarding for the Lighthouse, enabling the routing of all Wireguard traffic through the Lighthouse.

Running The Lab

Bring the lab up same as last time with:

just up 2-2

Once again we’re looking at a Zellij session, but this time there are two separate tabs:

Zellij tabs with names os-shell and lab_2-2

The tabs are clickable, but you can also use Ctrl+t to navigate between them. Note the bottom of the Zellij window for instructions.

In the lab_2-2 tab you’ll see three panes, each with a separate container. The large one on the left is the Lighthouse Peer. Top right is the “home” Peer, and bottom right is the “roaming” Peer.

Lighthouse Configuration

We’ll start with the Lighthouse. Using the text editor of your choice, open the config file at /etc/wireguard/lab_2-2.conf. You’ll notice that the [Interface] section has already been populated with a keypair. It has not yet been populated with a Wireguard address. If you’re following the same pattern we used in the last lab, make this 172.16.100.1/24.

Note

Why not pre-fill this in? Mostly to get you the muscle memory of making this setting manually. In later labs, this is automated for efficiency.

There is a single [Peer] placeholder that’s waiting for a public key and an address. We’ll need two such entries for our home and roaming devices. Let’s head over to home and roaming and fill out those configs—but before we leave the Lighthouse, make sure to copy the public key. We’ll need it for both other Peers.

Peer Configuration

Just as with the Lighthouse, the keypairs for the home and roaming Peers are pre-populated. The addresses are not. Following our pattern, assign these:

  • Home: 172.16.100.2/24
  • Roaming: 172.16.100.3/24

Now for the [Peer] entries. Both home and roaming need only a single entry, pointing to the Lighthouse’s IP. That would be 10.1.99.10:51820, as the compose file shows.

Note

In reality, the Lighthouse is going to probably have a domain name attached to it. And in fact, the roaming Peer can use lab_2-2_lighthouse instead of the IP address because they technically share a network. But DNS in containers is a pain, so we’ll stick to IPs for this lab.

AllowedIPs is going to change a little. Remember that for “clients,” AllowedIPs determines what IPs are routed over the Wireguard tunnel. In our last network, this was a Peer-to-Peer connection with direct addressing for each other Peer. In this case, every Peer is routing Wireguard traffic through the Lighthouse, regardless of specific IP in the network. That means that AllowedIPs is not a /32, but the entire /24 subnet we’re using for Wireguard. More succinctly:

AllowedIPs = 172.16.100.0/24

Because both home and roaming are connecting to the same endpoint, entire Peer entry for the Lighthouse will look identical between them. All together, it will be something like:

[Peer]
PublicKey =  ybFybA88hvso4fxo8dWCYXRnKmPw6+sy+YwtqisOgBc=
AllowedIPs = 172.16.100.0/24
# For Peer->Server
Endpoint = lab_2-2_lighthouse:51820
PersistentKeepalive = 25

With your own Lighthouse’s public key, of course.

Adding Peers to the Lighthouse

Almost done! Back on the Lighthouse, we need a [Peer] entry for both home and roaming. Take the respective public keys and fill them in, along with the /32 version of their address.

Here’s what that looks like:

# Home
[Peer]
PublicKey = k7uDnn9nV+NlSzJsVRr4sFf98uyvlpvbFHla5foS4iA=
AllowedIPs = 172.16.100.2/32

# Roaming
[Peer]
PublicKey = 9iC0kFu2y2O7A5kn8XNVXso1zjphNf8pu5hvXnZ7Y38=
AllowedIPs = 172.16.100.3/32

Testing the Connection

Can the roaming host make contact with with the home machine? Let’s try an experiment. We’ll set up a tiny “server” on the home Peer for roaming to reach. On home, run:

echo "Hi" | nc -nvlp 8000

This sets up a netcat connection on port 8000 which will send Hi as the buffer to any incoming connection. Then, on roaming:

nc 172.16.100.2:8000

You should get a greeting on the roaming Peer! Use Ctrl+c to kill the connection. Keep experimenting! When you’re done, click over to the os-shell Zellij tab and run just down 2-2 to shut down the lab.

2-3: Accessing Networks

Our Lighthouse server allowed Peers to connect across network boundaries, so long as all Peers had line-of-sight to the Lighthouse. We accomplished this with IP forwarding on the Lighthouse, so that traffic intended for Peers connected to the Lighthouse could reach their destination.

But IP forwarding can be used for more than just Wireguard traffic. This same technique can allow a Wireguard Peer to provide access to an entire subnet.

It works like this:

flowchart
  subgraph internet [Internet]
  C[Peer C] <--> L[Lighthouse]
  end
  subgraph home1 [Home Network]
  A[Peer A] <--> L
  S[Webserver] <--> A
  end
  subgraph roaming [Roaming Network]
  B[Peer B] <--> L
  end

  style internet fill:blue
  style home1 fill:purple
  style roaming fill:maroon

Here we have the Lighthouse, same as before, as well as a home Peer. In the home network, we also have a web server that we’d like to access remotely. If the home Peer is IP forwarding just like the Lighthouse, and we configure the AllowedIPs for all Peers correctly, all nodes will be able to access IPs in that home subnet from anywhere, while connected to Wireguard. Tailscale calls this kind of Peer a “subnet router,” and so will we. But we don’t need Tailscale to pull this off.

The Magic of AllowedIPs

Recall that AllowedIPs determines both what traffic is allowed in from a Wireguard session, and what traffic is sent out over that session. It’s the latter we take advantage of here to provide subnet access.

The home “subnet router” doesn’t need to change anything in its AllowedIPs configuration, surprisingly. If we’re following a Lighthouse model, even the subnet router will have only a single Peer entry: the Lighthouse. And because we set that AllowedIPs value to the entire Wireguard subnet (so the Peer can access all other Wireguard Peers), we have also stated that traffic from the whole subnet may come over this connection.

The other Peers, on the other hand, need to add each subnet they want handled by the subnet router to their AllowedIPs entry. Subnets are separated by commas. So let’s imagine our home network is the 192.168.1.0/24 subnet, and the Wireguard network itself is 172.16.100.0/24. In order to send traffic for the home network over the Wireguard session, a Peer’s AllowedIPs entry would need to look like:

AllowedIPs = 172.16.100.0/24, 192.168.1.0/24

When the Wireguard session is up, packets destined for either subnet will be sent over the session. In the case of roaming Peers, that means the Lighthouse receives the packets first.

The Lighthouse’s AllowedIPs entry also needs to change. In order to send traffic from other Peers intended for the home network to the subnet router (and to allow replies from those IP addresses), the home subnet needs to be added to the list. So the Lighthouse AllowedIPs for the subnet router looks like:

AllowedIPs = 172.16.100.2/32, 192.168.1.0/24

Note the subtle difference between the Peer and Lighthouse. We’re only allowing a single Wireguard IP address (/32) to/from the subnet router, whereas other Peers allow/route the entire Wireguard subnet (/24). But in both cases, the entire home subnet is allowed.

Warning

This setting could very easily break other local network connections if they use the same subnet as home. If, say, a hotel LAN uses the same IP space as your home network, this configuration will break your hotel LAN connection.

But once the Wireguard traffic from other Peers has reached the subnet router, we have another problem: how to forward the traffic on in such a way that the traffic comes back to the router, and then back to the original source.

For that, we need firewall rules.

PostUp and NFTables

The Lighthouse’s “routing” responsibilities are limited to the Wireguard network, but the subnet router has a harder job to do.

Let’s imagine there’s a web server on my home network—maybe a Jellyfin server or similar service—that I want to access remotely. I know the IP of the server on my home network, so I make a request to that server from a roaming device, connected to the Wireguard network via the Lighthouse. The Lighthouse, configured via its AllowedIPs entry for the subnet router, sends the traffic along to that Peer.

Now the subnet router has the packet, which has a destination intended for the home server. If IP forwarding is enabled on the subnet router, then the packet can be sent along to that server. However, once the server receives it, it will not have any way to reply—the source IP is in the Wireguard subnet, which the home server has no route for.

To solve this problem, the subnet router needs to perform a type of network address translation known as “masquerading.” The router needs to present the packet with itself as the source. Then, upon receiving the reply from the internal server, send the response back to the true source.

Luckily, the ability to do this is built-in to most Linux distributions, via the older iptables or the newer nftables. We’ll be using the latter.

Nftables firewall rules are configured with the nft command line tool.

Here’s the full set of commands we need to configure a masquerading, NAT router in our home subnet.

nft add table ip filter
nft add table ip nat
nft add chain ip filter forward '{type filter hook forward priority filter; policy accept;}'
nft add rule ip filter forward iifname %i counter accept
nft add chain ip nat postrouting '{type nat hook postrouting priority srcnat; policy accept;}'
nft add rule ip nat postrouting oifname <<HOME_INTERFACE>> counter masquerade

That is…quite a lot, I know. Let’s go line by line.

nft add table ip filter: Firewall rules are stored in tables. Tables have multiple types. Here we make a new rules table for IPv4 addresses called filter.

nft add table ip nat: And we make another called nat. Next up, we build rule chains.

nft add chain ip filter forward '{type filter hook forward priority filter; policy accept;}': This monster creates a new chain—a ruleset—called forward to the filter table. The definition inside the curly braces describes the type of rules. These are filter rules (naturally), to be applied on the forward hook. This matches packets that are intended for other hosts. The priority is the standard filter, and and packets not matched by any rules in the chain will be accepted.

nft add rule ip filter forward iifname %i counter accept: Here we add a rule to the chain we just made. This rule applies to any packet coming in on interface %i, which is a variable used by Wireguard to refer to the specific Wireguard interface created by the config. counter means that hits on this rule will be counted, and the packets will be accepted.

nft add chain ip nat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }': Another chain, this one for NAT called postrouting. This is how we’ll send the packets on.

nft add rule ip nat postrouting oifname <<HOME_INTERFACE>> counter masquerade: Just as before, we’re adding a new rule. Before we were catching packets inbound on the Wireguard interface. Now we’re catching packets on the way out on the home network interface, counting them and masquerading the packets. In this way, the packet will be able to find its way back to the router. We’ll need to fill in the <<HOME_INTERFACE>> with a real one, once we know its name.

Put it all together, and we have a way to send packets into the subnet and get them back.

So where do all these commands live? If we want them to be linked to the Wireguard tunnel itself—and we do—we can take advantage of the PostUp directives in the [Interface] config.

The PreUp, PostUp, PreDown, and PostDown directives allow us to run arbitrary commands before and after bringing up and down the Wireguard interface, respectively. We can use the directive as many times as we want for multiple commands. In this way, we can contain all the above nft commands in the Wireguard config and have Wireguard itself set up the firewall rules.

Oh, and remember that %i up there for the interface name? Wireguard will fill in its own when presented with that variable.

We can use PostDown to clean up when we’re done with Wireguard, to keep our firewall rules clean.

For that, we’ll use only two commands:

nft delete chain ip filter forward
nft delete chain ip nat postrouting

So if we put that all together, it looks like this:

PostUp = nft add table ip filter
PostUp = nft add table ip nat
PostUp = nft add chain ip filter forward '{type filter hook forward priority filter; policy accept;}'
PostUp = nft add rule ip filter forward iifname %i counter accept
PostUp = nft add chain ip nat postrouting '{type nat hook postrouting priority srcnat; policy accept;}'
PostUp = nft add rule ip nat postrouting oifname <<HOME_INTERFACE>> counter masquerade
PostDown = nft delete chain ip filter forward
PostDown = nft delete chain ip nat postrouting

That lives up top with the rest of our interface configuration. Is it a bit clunky? It is, but it’s the cleanest way to integrate nftables with Wireguard.

Note

nft can be used as a scripting language in dedicated files. The reason we’re not putting all this into separate files is twofold. First, we’d still need to delete the rules when bringing down the interface; and second, then we’d have to sed two separate files to fill on variables rather than one. I’d prefer fewer files to wrangle.

I know this is a lot, but luckily it’s one of those things that, once you have it set up, you can mostly ignore it. And if you ever need to make another subnet router, the steps will nearly identical. But it’s practice that will cement your memory, so let’s bring up our subnet router lab!

Check For Understanding

  • Explain the difference in how the Lighthouse, home router, and other Peers use AllowedIPs.
  • Take each PostUp nft command in turn. Explain what each one does and why it matters for our subnet router.

2-4: LAB | A Subnet Router

Lab Setup

As before, let’s understand what we’re launching by examining the podman-compose.yml file in the labs/2-4 directory. Structurally, the big difference in this lab is the addition of a new machine: the home webserver. It’s on the home network, but it will not have Wireguard installed. Instead, we need to configure the home-router device to route traffic to it (and anything else in the subnet we want).

Incidentally, pay close attention to the IP addresses assigned in the compose file. You’ll need them later, and having them handy saves a few ip a s calls.

You will see in the home-router/home-router.conf that the nft commands we discussed in the last section have been added.

Let’s get the lab up with:

just up 2-4

Once again we have a new Zellij session. This one has two tabs: os-shell for host OS commands, and lab_2-4 which has our lab machines. We’ll live in the second tab for now. Remember you can click on it, or use Ctrl+t to navigate tabs.

This time, we have four machines. Clockwise from the top left, we have:

  1. Lighthouse
  2. Roaming Peer
  3. Home router (subnet router)
  4. Home webserver

The only one without Wireguard is the home webserver, since we’re trying to expose it via the home router.

With each successive lab, we automate what we’ve come to understand manually in previous exercises. For this one, you don’t have to worry about adding keys to each config; the setup scripts for the lab have handled that. See for yourself by running cat /etc/wireguard/lab_2-4.conf in each of the Wireguard-using containers. Magically, the keys are already populated!

Note

It isn’t magic, but any sufficiently advanced Bash is indistinguishable from magic.

Now as for the home webserver. There’s a lightweight web service running on this thing. You can confirm it’s working by running this on the home router:

curl http://192.168.99.10
# OR
curl http://lab_2-4_webserver

Either hostname works thanks to container networking. Remote Peers will have to use the IP address (for now).

On the home router, have a look at the Wireguard config.

cat /etc/wireguard/lab_2-4.conf

You’ll see exactly the NFTables commands we’ve discussed for getting tables and chains set up for port forwarding and masquerading in PostUp commands. And you’ll see a small set of PostDown commands to remove those entries when the interface is destroyed. The other Peer configs look more familiar, and as mentioned, they’ve already been filled in with the necessary key and address values.

Running the Lab

On all but the webserver, bring up Wireguard.

wg-quick up lab_2-4

This is pretty simple, ultimately. From the roaming Peer, try to access the local webserver.

curl http://192.168.99.10

You should see a response! The Lighthouse->Home Router path is working! Or is it??

Podman does all sorts of weird backplane networking between containers.

Even the roaming Peer can see the webserver!

Running the Lab

On all but the webserver, bring up Wireguard.

wg-quick up lab_2-4

This is pretty simple, ultimately. From the roaming Peer, try to access the local webserver.

curl http://192.168.99.10

You should see a response! The Lighthouse->Home Router path is working! Or is it??

Podman does all sorts of weird backplane networking between containers. How do we know for sure Wireguard is doing the routing?

If you look closely at the Containerfile for this lab’s Peer images, you’ll note that we now have iputils-tracepath installed. You can use this to confirm how your packets are getting to the webserver.

tracepath 192.168.99.10

Your output should look like:

 1?: [LOCALHOST]                      pmtu 65440
 1:  172.16.100.1                                          0.328ms
 1:  172.16.100.1                                          0.410ms
 2:  172.16.100.2                                          0.780ms
 3:  192.168.99.10                                         0.853ms reached
     Resume: pmtu 65440 hops 3 back 3

The packets travel through the Wireguard subnet, demonstrating that our network is up and routing is correct.

Now, just to demonstrate that this is all self-contained in Wireguard, run the Wireguard shutdown command on all but the webserver.

wg-quick down lab_2-4

You’ll see the home router run the nft delete command that removes the port forwarding and masquerading rules. The operations only work when Wireguard is up—actually, when that specific Wireguard configuration is up.

You can also re-run the tracepath command above. Yes, Podman is still routing the traffic, but you’ll note that it’s happening over the Podman router rather than Wireguard.

Stopping the Lab

As usual, use the os-shell tab to run just down 2-4 to bring down the lab.

2-5: DNS

At some point, you’re going to get pretty tired of using IP addresses all over the place to refer to your Wireguard Peers. Or, you’ll want to use your existing private network’s internal domain while roaming. Both of these are possible!

Wireguard offers multiple ways to route DNS over the tunnel. We’ll start with the lame easy way.

The DNS Directive

Turns out, there’s a DNS directive available in the [Interface] configuration! Setting that address will send all DNS requests to that destination while the tunnel is up.

DNS = 172.16.100.10

You can also add multiple addresses, just like other DNS configurations.

DNS = 172.16.100.10, 172.16.100.11

You can even add a search domain.

DNS = 172.16.100.10, mydomain.local

As long as the IP address provided is reachable by your Peer, DNS will be routed through the tunnel. This prevents DNS Leaks while on a “private” network.

Systemd-Resolved

On Linux systems using systemd-resolved for their DNS management, there is also the option of specifically routing DNS requests for known domains on a specific network interface. If you want all DNS traffic routed through the Wireguard tunnel, this is unnecessary; you can use the DNS directive described above. But in some cases, you do want only certain domains to be handled by the DNS server on the tunnel. For example, suppose you want to route an intern lab’s domain name via the tunnel, but leave the rest of your DNS as your default servers.

Here’s what we need. Assuming a DNS server address of 172.16.100.3 and a domain name of home.lab:

PostUp = resolvectl dns %i 172.16.100.3
PostUp = resolvectl domain %i home.lab ~home.lab
PreDown = resolvectl revert %i 

The first PostUp configures a new DNS server on our Wireguard network interface at the address listed. Remember, this address doesn’t have to be on the same subnet as your Wireguard hosts; it only has to be accessible over the Wireguard tunnel. So if you have a DNS server in your lab that’s available via NAT/Masquerading like we set up in the last lab, you can use an IP address on a subnet to which you’re forwarding.

Now, if that’s all we did, the DNS resolution would still work, but a bit inelegantly. Systemd-resolved would attempt DNS queries on all configured servers at once, and first responder wins. We don’t need to bother with querying a DNS server we know won’t have the answer we want. That’s what the second PostUp directive does. We’re telling resolved to send DNS queries for the home.lab domain to the configured server on our Wireguard interface. We’re also telling it to search that domain, so that if we request webserver without a domain, resolved will attempt webserver.sec.lab on that DNS server.

Finally, a PreDown directive. Why PreDown and not PostDown? We can’t make changes that reference the Wireguard interface once it’s been deleted, so we need to do that before it goes away. revert removes any added config for the interface and sets it back to a default state.

You can add as many domains this way as you like with additional PostUp directives.

Again, this is fully optional. But having custom DNS really is quite a valuable tool in a private network.

Let’s practice setting this up.

2-6: LAB | Custom DNS

This one won’t take long, but it might be useful to see custom DNS at work in a Wireguard deployment.

Lab Setup

We’ve automated away what we need for the Wireguard keys at this point, but there is a new component to this lab: the DNS server! We’re using Unbound as our DNS server, because it’s easy and lightweight.

Have a look at labs/2-6/home-dns/unbound.conf to see what the Unbound configuration looks like. We won’t spend time on it here, since DNS is not our focus. But do note the access-control entries that allow queries from both LAN and Wireguard subnets.

Okay, let’s fire up the lab.

Running the Lab

just up 2-6

Our layout is the same as lab 2-4, with two Zellij panes—the host OS, and the lab containers. In the lab tab, you won’t see the DNS server because no commands need to be run on it; we only need to see its effects.

On the lighthouse, roaming, and home router containers, start the Wireguard tunnel:

wg-quick up lab_2-6

[!INFORMATION] You can use Ctrl + T S to enable “Sync mode” in the Zellij tab and send this command to all four panes/containers at once. The webserver will produce an error because it doesn’t have Wireguard, but that’s fine. Don’t forget to repeat the command to exit Sync.

The purpose of this lab is to demonstrate the DNS configuration for roaming hosts, so let’s focus on our roaming container in the top right.

Start by confirming the DNS configuration. Our Alpine machine has no systemd, so we won’t be doing the resolvectl version. We can still view our nameservers by looking at /etc/resolv.conf.

cat /etc/resolv.conf

You should see 192.168.99.3 as the nameserver! Now bring the Wireguard tunnel down temporarily.

wg-quick down lab_2-6

And re-run the cat command. See? Wireguard is modifying our DNS settings. Bring the tunnel back up.

wg-quick up lab_2-6

Our DNS config should be back to our home DNS server at 192.168.99.3. Let’s put it to the test by accessing our home webserver by domain name.

curl http://webserver.home.lab

If you see a message from your home webserver, we’ve just demonstrated Wireguard-controlled DNS configuration!

Stopping the Lab

Move back to the os-shell tab and run just down 2-6.

2-7: Preshared Keys

Wireguard uses asymmetric encryption to protect communications. Asymmetric encryption, as it turns out, may be vulnerable to decryption via quantum computers. This is likely a long ways off, but preparing for post-quantum security now is both simple and prudent. By enriching the asymmetric encryption with a symmetric secret shared between Peers, we can future-proof our Wireguard configuration with minimal effort.

You can create such a secret with the built-in genpsk command.

wg genpsk

That value goes in a PresharedKey directive in the Peer section.

[Peer]
# Other Peer directives...
PresharedKey = 5O9qHryfXWOegiNi6s3tlFjTie7sn1fuPYDtiNo8S5U=

This key must match on complementary Peer entries. Not all members of a network need to (or should) share the same secret, but any two connecting Peers must have matching keys.

These keys should be treated like any other credential, and handled with care. I recommend using a password manager like Bitwarden or Vaultwarden to store and share keys with others.

That’s it! No lab necessary for this small feature, but I wanted to call it out as an additional security measure when setting up Wireguard networks manually.

But we’re now done setting things up manually. In the next section, we’ll use tools built on top of Wireguard to more easily manage our networks and network members.

3-1: Wireguard-Powered Tools

It’s time to graduate from our isolated Podman environments and start building something in the real world. If all goes according to plan, what we build can be the beginning of your new Wireguard-powered private network. Therefore, we want to make it as easy as possible to manage this network. While you can manage keys and configurations manually, you don’t have to. Wireguard forms the foundation of several well-built, well-maintained open source networking solutions which have both paid/managed and self-hosted options.

We’re going to use Netbird for our build, but it’s worth reviewing some of the big names in Wireguard tools, so you know what the options are.

Why Managed Tools?

Think back to the manual drudgery of key generation in the early labs. While we scripted away this complexity for later labs to focus on new concepts, you may not have that luxury in the real world. Wireguard has some creature comforts, like QR codes, to add configuration to mobile devices, but there is still a tremendous amount of manual effort.

Take the Lighthouse server. Our small network required the addition of just a few public keys. Imagine a network of a dozen Peers—or more. Some automation makes this a much saner process.

Managed tools also come with some very handy networking tricks that only a dynamic service that’s changing configuration and routing in real-time can pull off. For example, most managed Wireguard-based tools use a management server for setup, but after setup, most traffic between Peers is direct, or peer-to-peer. You might think that’s impossible for two Peers behind NATed firewalls, but these tools find a way.

Another benefit: automatic DNS. Without any additional work, most of these tools resolve Peer hostnames. You can still maintain custom DNS servers if you like, or you can use those hostnames directly.

Ending on the biggie, access control. Raw Wireguard has only AllowedIPs as a tool for managing access to network resources. As your network grows more complex, you may find yourself wishing for more options. Identity-based, Peer-based, or group-based access control policies make mesh networking much more convenient at scale.

Imagine running a small remote business. You could set up central assets in one location, accessed behind a traditional firewall and VPN. Or you could use an “edgeless” Wireguard-based mesh network, placing assets where it makes sense to place them, and governing access via groups or identity. I dunno about you, but I find the latter much more appealing, especially in the age of ever-increasing 0-days against edge devices.

Some Options

We’re going to use Netbird for our buildout. It’s a comprehensive network manager built on Wireguard. I like Netbird for its ergonomics and licensing model. But there are other tools you might want to explore.

Tailscale

Tailscale is by far the most popular Wireguard-based networking platform, and for good reason: excellent free-tier support, even better paid options for businesses. There’s a lot to like about Tailscale, and I’ve used it in the past. Why not for this course, then?

I wanted to give you a fully self-hostable solution. While Headscale seeks to reproduce some of Tailscale’s capabilities, it’s far more limited, and harder to manage. Plus, it still requires Tailscale’s closed-source client. Netbird’s entire stack is open source.

Netmaker

Netmaker is another open-source-with-enterprise-options offering. It is, in my opinion, the most complex of the popular Wireguard products, although its Kubernetes deployment options may be appealing for some organizations. Have a look! It may be preferable to Netbird for your needs. I’m confident that the skills we practice with Netbird will translate across.

Alright, let’s go build a network.

Check For Understanding

  • What are the advantages of using tools built on Wireguard rather than just Wireguard? What are some potential drawbacks?

3-1: Setting up a Cloud VM

For this next and final section, we’re moving on from the Podman environments. There are limits to what we can do in a fully containerized world. It’s time to use what we’ve learned to build something real.

You’ll need a cloud VM.

Providers

There are multiple options for choosing a Virtual Private Server (VPS). AWS has a free tier that can suffice for this project. Microsoft Azure has a signup bonus of $200, which also works. I use Digital Ocean for my personal servers, which does not have a free tier.

Since I want this lab to be accessible to all, I will use AWS for these examples. The steps will be similar for other providers, with some platform-specific modifications.

That said, this is not a course on AWS operations. I will not be going through VM creation in every single detail; instead I will explain the necessary configurations for our purposes.

System Requirements

Our requirements are modest for our cloud VM. Keeping within the free tier for AWS, we’ll want a system with:

  • 2 vCPUs
  • 2 GB RAM
  • 30 GB root volume

Operating System

I’m going to use Ubuntu for commonality. If you choose another Linux distribution, there may be some differences in the setup commands.

Networking

We will require a public IP address as well. For the firewall rules, you’ll want to configure the following:

  • SSH allowed from your IP address, not the whole internet
  • HTTP/HTTPS allowed from everywhere
  • UDP 3478 allowed from everywhere—this may have to be configured after initial setup.

Access

Since we’re allowing SSH from our port, create an (or add an existing) SSH key to the VM.

Once the VM is set up and you’re connected, we’ll begin building a network with a more sophisticated Wireguard tool.

3-4: Netbird

Once you’re connected to your cloud VM, we’re ready to set up Netbird.

DNS

Warning

This part will cost some money. Even if you’re using free tier cloud servers, acquiring a domain name requires a purchase.

But actually, the first thing we’re going to do is not in the terminal at all. We’ll need a domain name to use with our management server. You can purchase a domain from several locations. I use Namecheap, but there are others. Pick a domain that you like, and you’re willing to keep for a long time if this network is intended for continued use.

You can also use a subdomain of a domain you already own.

Either way, you’ll want to create an A record for the domain that points to your cloud VM’s public IP address. For the root domain, the “host” value is @.

Give it like an hour for DNS records to propagate, then head back to your SSH session on the cloud VM.

Installation/Setup

Let’s do a general overview of the architecture.

architecture-beta
    service internet(internet)[Internet]
    group netbird[Netbird Server]
    group docker(server)[Docker] in netbird

    service caddy(cloud)[Caddy] in netbird
    service netbirdserver(server)[Netbird Wireguard] in docker
    service netbirddashboard(server)[Netbird Dashboard] in docker

    internet:R -- L:caddy
    caddy:B -- L:netbirddashboard
    internet:B -- L:netbirdserver
    netbirdserver:R -- L:netbirddashboard 

The Netbird application is a containerized deployment, so we’ll be installing Docker. The application comprises two services: the Wireguard server and the Dashboard UI. The server is exposed on the UDP port we opened up, and the Dashboard will be served over HTTPS via Caddy.

Caddy will handle the acquisition and renewal of our TLS certificate for the domain, and provide the dashboard via reverse proxy.

Caddy

Ubuntu’s repository version of Caddy is woefully out-of-date. Instead, we’ll grab the latest version from Github. Head to the latest release, and find the amd64.deb. You’ll probably have to “show all assets” to find it—Caddy builds for a lot of platforms.

Here’s an example with version 2.11.3, the latest as of this writing.

wget https://github.com/caddyserver/caddy/releases/download/v2.11.3/caddy_2.11.3_linux_amd64.deb.pem
sudo dpkg -i caddy_2.11.3_linux_amd64.deb.pem
rm caddy_2.11.3_linux_amd64.deb.pem

Caddy will install itself as a service, so we’re pretty much good to go.

Docker

Follow Docker’s install instructions for Ubuntu, or whatever distribution you’ve chosen.

Install Script

Once Docker is installed, we can use the Netbird one-liner install script to begin configuring Netbird itself. Except, it’s kind of funky with Caddy.

mkdir netbird
cd netbird
curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash

We make a netbird folder because the script generates several files, and I just think it’s messy to have them in your home folder root.

You’ll be prompted for a domain name for Netbird. Enter whatever you configured in your DNS console.

Choose 4 (Caddy) when asked about reverse proxies.

Choose Y for binding container ports to localhost only.

Leave the Docker network for Caddy empty, as it’s running on the host.

At this point, the script will hang because it instructs you to configure Caddy—which you can’t do while it’s hanging. Does it expect a terminal multiplexer? Maybe! But we won’t bother. Press Ctrl+C to exit the installer.

If you ls, you’ll see several files were created:

  • caddyfile-netbird.txt: Config snippet to add to /etc/caddy/Caddyfile
  • config.yaml: The Netbird config file. This gets mounted into the server container
  • dashboard.env: The dashboard config. This gets mounted into the dashboard container
  • docker-compose.yml: The Compose file that defines the services.

The caddyfile-netbird.txt can be copied over to /etc/caddy/Caddyfile in its entirety, unless you’re using Caddy for something else—in which case, simply add the snippet to the existing file. But if this server is dedicated to Netbird:

sudo cp caddyfile-netbird.txt /etc/caddy/Caddyfile
sudo chown caddy: /etc/caddy/Caddyfile
sudo systemctl daemon-reload
sudo systemctl restart caddy

That’s Caddy sorted! It just went and grabbed a certificate for your chosen domain.

Let’s bring Netbird up with Docker compose.

sudo docker compose pull
sudo docker compose up -d

Now, AS FAST AS POSSIBLE, visit your domain. You’ll be taken to a setup page to create your admin user/password. Once that’s sorted, you have yourself a brand new Netbird instance to play with. Congratulations!

Feel free to skip the “Get started” questionnaire. We’ll build it all out together.

Peers

Our network is made of Peers, so we’ll want to add some. There’s a bit of a catch, though: Netbird authentication sessions expire by default. That is, unless you sign in with a Setup Key—basically preauthorization for the Peer. When we set up Netbird with one of these keys, sessions last indefinitely. That might not be what you want, but in general I don’t want to have to reauthenticate my trusted devices to my network.

So let’s go make some setup keys before adding Peers.

Setup Keys

Click on “Setup Keys” in the sidebar or navigate to /setup-keys in the Netbird Dashboard.

You’ll usually want to create a setup key for a given user or set of devices. Make the key reusable to enroll multiple devices. Keys can expire or not, depending on the “Expires In” setting. You can also set a maximum number of uses.

When providing setup keys to other users, consider using one-off or limited-use keys so they can’t be abused. Expirations would also be a good idea.

You can also add users of a given setup key to a group upon enrollment. Groups allow you to scope permissions and configurations. You can create new groups right from the add dialog.

When you create a key, it will be shown one time, so copy it down (maybe into a password manager?). You can also click “Setup Netbird” right from the creation popup and receive installation/setup instructions for how to use netbird up with the setup key.

Networks

Netbird has kind of two ways to think about network resources. The older “Network Routes,” which closely mirrors the “subnet router” model we’ve already explored, and the new “Networks” model. We’ll try to use the new one.

A “Network” in Netbird is just a container for many “Resources,” or network destinations. A “Resource” might be an IP address, a domain name, or even a full subnet.

Here I’m adding a “Home Subnet” resource.

A resource is paired with one or more access policies, allowing individual Peers or Groups access. You can refine policies by port/protocol if you wish.

Once you add your first Resource, you’ll be asked to add a Routing Peer to the Network.

Routing Peers are similar to “subnet routers” in Tailscale and our own Lighthouse buildout. They are any Peer that can offer access to resource in the defined Network. You’ll want to add Peers with unique visibility to resources nothing else can see. For example, if you have a jumpbox in your homelab, add all your homelab subnets as Resources and the jumpbox as a Routing Peer.

DNS

Netbird provides two ways to manage DNS. You can add DNS servers to be used by Peers in a network.

Nameservers

With Nameservers, you can configure well-known DNS resolvers like Google, Cloudflare, or Quad9. Alternatively, you can add custom servers. They don’t have to be in private networks, but they can be. This might be useful if you have a homelab DNS server you want Peers to use when connected. You could also use this to prevent the DNS leakage we’ve discussed previously.

Zones

If you don’t want to configure a separate server, but you do want to provide in-network DNS resolution of certain domains/domain names, you can configure Zones in Netbird. These are provided to Peers in a given group, and allow a sort of ad-hoc DNS resolution. Here I’m creating a home.lab Zone and adding a jumpbox record to it.

Built-In DNS

Netbird will also provide hostname resolution for a built-in domain for all Peers. You can configure the default network domain (and IP range) in Settings -> Networks

Users

If you want to share your network with others, you’ll need separate accounts for authentication. This is done in Team -> Users.

You can either create users directly, or create an invitation link to send to users via external communication. Either way, you have the option to automatically add the user’s Peers to specific groups upon creation.

MFA

This is optional, but strongly recommended: In Settings -> Users, you can require users created with local accounts to configure a second factor (TOTP/Authenticator app) for login.

And…that’s it! Your Netbird server is up and running and ready for use. You can now build your private network however you wish. Doing so will be your Exhibition of Mastery.

Check For Understanding

  • How are Networks, Resources, and Routing Peers related?
  • Why might you want to define multiple access policies?
  • Why would you want to consider configuring a custom DNS server in your network?

4-1: Exhibition of Mastery

Congratulations! You’ve made it to the end of the course. It’s time to prove what you’ve learned with what we call an Exhibition of Mastery. This is a holistic project that pulls together all the topics we’ve covered in the course.

For this course, your EoM is to build out your Wireguard network. Whether using Netbird, Tailscale, or raw Wireguard, your task is to create a secure network for you and your community. That could be a simple family network for sharing resources while roaming. It could be a network for a local volunteer group or nonprofit for safer access to resources. It could even be a secure network for an activist group. Whatever your community needs, build it using Wireguard and Wireguard-based tools.

Share It!

Whatever you make, come share it with us in our learning community!

Thank You

Thank you for taking the time to complete this course. I hope you’ve found it valuable. And if you enjoy the material at TTI, please consider subscribing to support the community and future course development.