A minimalist 3D illustration of a secure, self-hosted VPN server standing on an isolated digital rock, protected by a glowing wireframe dome representing OpenBSD security.

1. Introduction: The OpenBSD Fortress (WireGuard VPN)

The Premise: Commercial VPNs are fine if you easily give away your trust to companies, but for true sovereignty, I don’t trust my traffic to a black-box company. I need a node I own, running an OS that prioritizes security over features.

Why OpenBSD? In my setup, I use OpenBSD on a number of my edge nodes because of pf (Packet Filter) and its “secure by default” philosophy. It is lightweight, rock-solid, and has WireGuard support built directly into the kernel.

Why WireGuard? I used to work exclusively with OpenVPN. While it was the industry standard for my clients, it was often slow and a nightmare to maintain especially the certificate management.

When I looked for a replacement, I found WireGuard. It is a breath of fresh air: only ~4,000 lines of code, and it follows the Unix philosophy of doing one thing and doing it well. The latency is minimal, the cryptographic choices are modern, and it is deeply integrated into both the Linux and OpenBSD kernels.

That last point is crucial. For a security project to be accepted into the OpenBSD kernel base is the ultimate seal of quality.

Watch the video for the philosophy behind this setup and a quick technical walkthrough.

Here is the exact configuration I use to deploy a VPN node.

2. The Prerequisites

2.1 The Architecture of Choice: Where Does Your Node Live?

Before we touch the terminal, you have to decide where your “Fortress” will physically reside. In my experience, this choice depends entirely on your local infrastructure and your need for global availability.

Option A: The Home Lab (Maximum Sovereignty)

If you have a static IP or a reliable Dynamic DNS setup, you can run this on a low-power machine at home.

  • The Choice: You own the hardware. No one can pull the plug but you.
  • The Trade-off: Your VPN speed is limited by your home’s upload speed. Availability varies.

Option B: The Cloud Gateway (Maximum Availability)

If your home internet is unreliable or you need a “neutral” presence in a specific region, a VPS or Dedicated Server is the way to go.

  • Hetzner (Best for EU/Global Value): I have used Hetzner for years. Their value-to-performance ratio is unmatched in the EU. This tutorial and the accompanying video were filmed on a Hetzner ARM64 instance.
  • Vultr (Best for US/Global Reach): If you need nodes in the US or Asia, Vultr offers a massive footprint and high-performance NVMe instances. They are slightly more expensive than Hetzner but offer a broader geographic spread.

While there are countless hosting providers in the market, I have chosen to highlight Hetzner and Vultr because I have used them both extensively with OpenBSD over the years.

2.2 Hardware & Software Requirements

While you can technically “squeeze” this setup onto a bottom-tier VPS with 1 vCPU and 2GB of RAM, I personally recommend 2 vCPUs and 4GB of RAM if you plan on pushing high-speed traffic or managing multiple concurrent peers. This overhead ensures that your pf state tables and kernel buffers have room to breathe.

  • Operating System: We are using OpenBSD 7.8 (the latest release at the time of writing). Its hardware support especially for modern ARM64 and NVMe drives found on Hetzner and Vultr is at its peak in this version.
  • Storage Architecture: In my setup, security isn’t an add-on; it’s the foundation. Installing with Full Disk Encryption (FDE) is mandatory.

Why FDE is Non-Negotiable

If someone physically pulls a drive from a server rack or if your provider’s snapshot is ever leaked/compromised, unencrypted keys are “game over.” Without FDE, your WireGuard private keys, sensitive logs and any other data stored are exposed.

This way we ensure that even if the physical “box” is stolen, the data remains an unreadable block of noise.

2.3 The Skills: Terminal Proficiency

This is a “Headless” setup. There is no graphical interface, no mouse, and no “Undo” button. You need to be comfortable with the following:

1. SSH (Secure Shell) You must know how to initiate a remote session from your local terminal.

  • The Command: ssh root@<YOUR_IP_ADDRESS>
  • The Concept: You are not just “logging in”; you are opening a secure tunnel to send text commands. If you misconfigure the firewall without keeping a session open, you will lock yourself out permanently.

2. Text Editing (vi / nano) OpenBSD doesn’t come with VS Code. You will be editing configuration files directly on the server.

  • The Challenge: You need to know how to open, edit, and save files in a terminal editor.
  • The Tool: OpenBSD comes with vi and mg (Emacs-like) pre-installed. If you aren’t a vi master, I recommend installing nano (pkg_add nano) immediately after logging in for a friendlier experience.

3. Root / Superuser Concepts We are modifying kernel parameters and system interfaces.

  • The Risk: You will be operating as root (the system administrator). In the Unix world, root is God-mode. The system will not ask “Are you sure?” before deleting a file or killing a network interface.
  • The OpenBSD Way: While Linux uses sudo, OpenBSD prefers doas. For this initial setup, we will execute commands directly as root, but you must understand the gravity of the commands you type.

3. Installation

3.1 The Foundation: OS Installation & Encryption

The OpenBSD installer is famous for being simple, for 90% of the prompts, you can just press Enter to accept the defaults (which are enough for our case).

However, there is one critical prompt you cannot miss.

hard drive wrapped in digital chains and a padlock with the OpenBSD logo, symbolizing the security of Full Disk Encryption and Bioctl.

The Encryption Step (Bioctl)

When the installer asks which disk you want to use for the root disk (usually sd0 or wd0), it will immediately ask if you want to encrypt it.

The default is “no”. You must type yes.

⚠️ The “Remote Boot” Warning

Full Disk Encryption (FDE) wraps your entire OS in a cryptographic shell. This gives you Physical Sovereignty even if the VPS provider pulls the plug and takes your drive, they cannot read your keys.

But there is a catch: Because the disk is locked, the operating system cannot boot until you type the password.

  1. You cannot reboot via SSH alone. If you type reboot, the server will go down and wait at the password prompt. SSH will never come back up because the OS hasn’t loaded.
  2. You MUST have Console Access. Before you encrypt, ensure your VPS/Server provider (Vultr, Hetzner, etc.) offers a “Web Console” that lets you see the screen before the OS loads.
  3. The Process: Every time you restart the server, you must log into your VPS/Server dashboard, open the Web Console, and manually type your decryption password.

The Passphrase

Do not use “password123”. Use a sentence or a string with high entropy.

  • Good: correct-horse-battery-staple-on-fire
  • Bad: myvpndiskpassword
  • Paranoid: c0rrect-Hors3-battery-st4ple-0n-f!re

If you lose this password, your data is mathematically unrecoverable. There is no “Forgot Password” button here.

3.2 The First Boot Protocol (Update & Patch)

Once you log in for the first time, do not start installing tools yet. The ISO image you installed from might be weeks old, and in the security world, that is a lifetime.

OpenBSD has a tool called syspatch that downloads and verifies binary patches for the base system (kernel and userland) cryptographically.

Apply System Patches

Run the patch tool:

syspatch

If the system returns silently, you are up to date. If you see a list of files being downloaded (especially generic kernel names like SHA256.sig or bsd.rd), it means critical security fixes are being applied.

The Mandatory Reboot

If syspatch installed anything, you must reboot. OpenBSD patches the kernel binary on disk, but the running kernel is still the old one.

reboot

Wait 30-60 seconds, then log back in. Now your foundation is solid.

3.2 Installing the Tools

Once your server is patched and rebooted, we need to equip the system.

Because OpenBSD includes the WireGuard protocol (wg) directly in the kernel as a driver, we only need the userspace tools to help us generate keys and debug the connection.

Run this single command:

pkg_add wireguard-tools

Note: We are going to configure the interface the “Native OpenBSD Way” using hostname.if files. This ensures your VPN respects the system’s boot process properly and doesn’t rely on 3rd party scripts.

4. Hardening Access (The Digital Lock)

You cannot call this server a “Fortress” if a bot can brute-force the password. We are going to disable SSH password authentication entirely. From this point forward, the server will only accept a cryptographic key that exists on your specific machine.

4.1 Generate an Ed25519 Key (On your PC/Laptop)

Do not run this on the server. Open a terminal on your local computer. If you already have an SSH key, skip this. If not, generate a modern Ed25519 key:

ssh-keygen -t ed25519 -C "your-email@example.com"

The Passphrase Prompt: The system will ask: Enter passphrase (empty for no passphrase): DO NOT PRESS ENTER. You must set a passphrase. This encrypts the key file on your disk.

  • Why? If your laptop is stolen or compromised, the hacker gets your private key file. If it has no passphrase, they instantly own your server. If it is encrypted, the file is useless to them.

4.2 Transfer the Key

Send your public identity to the server.

ssh-copy-id root@<YOUR_REMOTE_SERVER_IP>

Note: You will be asked for your root password one last time. If ssh-copy-id isn’t available on your system, you can manually paste the content of your id_ed25519.pub into ~/.ssh/authorized_keys on the server.

Test it: Open a new terminal tab and try to ssh in. You should log in instantly without typing a password. Do not proceed until this works.

4.3 Disable Password Authentication

Now we cut the safety line. Back on the server, edit the SSH daemon configuration:

vi /etc/ssh/sshd_config

Find the following parameters and change them to match this configuration. This explicitly denies passwords and ensures root can only log in with keys.

# ... inside /etc/ssh/sshd_config ...

# Disable legacy password login
PasswordAuthentication no

# Disable challenge-response (often used for passwords)
ChallengeResponseAuthentication no

# Allow Root login, but ONLY via SSH Keys
PermitRootLogin prohibit-password

4.4 Apply the Changes

Reload the SSH daemon to apply the new rules.

rcctl reload sshd

DO NOT CLOSE YOUR CURRENT TERMINAL WINDOW. Your current session is your lifeline. If you made a typo in the config, you are still logged in and can fix it.

  1. Keep the current window open.
  2. Open a new terminal window.
  3. Attempt to connect: ssh root@<YOUR_VPS_IP>

If you get in? Success. The door is locked to the world, and you have the only key. If you get “Permission denied”? Go back to the first window and fix /etc/ssh/sshd_config.

🔒 OpSec Sidebar: Beyond the Basics

This guide permits root login via keys for simplicity, but in a mission-critical environment, we can go further. Here are four ways to tighten the noose around your SSH port that are outside the scope of this tutorial but worth exploring:

1. The “Root” Debate (doas) Direct root login is generally frowned upon in enterprise environments. The common solution is to create a standard user account, disable root login entirely, and use doas (OpenBSD’s simpler sudo alternative) to escalate privileges only when needed.

2. IP Allow-Listing (The White List) If you are lucky enough to have a Static IP at your home or office, you can configure pf to drop all packets to port 22 unless they originate from your specific IP. This makes your SSH port invisible to the rest of the world.

3. SSH via VPN (Inception) Once this WireGuard setup is active, the ultimate move is to bind SSH only to the internal VPN IP (e.g., 10.0.0.1) and block port 22 on the public internet interface entirely. You would connect to the VPN first, then SSH into the server “locally.”

4. The “Console Only” Option For the truly paranoid, you can disable the SSH daemon entirely and rely 100% on the VPS/Server provider’s Web Console for maintenance. It’s painful to use, but it physically removes the network attack vector.

5. Port Knocking A classic obfuscation technique where the firewall keeps port 22 closed until a specific sequence of “knocks” (packets sent to other closed ports) is received.

For this guide, Key-Based Root Authentication is the balance between high security and usability.

5. Key Generation (The Digital Handshake)

WireGuard does not use username/password accounts. It uses Public Key Cryptography.

Think of it like a strict VIP club:

  1. The Private Key: This is your ID badge. It never leaves your pocket (your device).
  2. The Public Key: This is your name on the guest list. You give this to the bouncer (the other peer).

For a connection to happen, both sides need to have the other’s Public Key on their “Allowed” list.

5.1 Generate the Server Keys

We are already logged into the OpenBSD server, so let’s create its identity first.

Create a Secure Vault: We create a directory locked down so only root can read it.

mkdir -p /etc/wireguard
chmod 700 /etc/wireguard
cd /etc/wireguard

Generate the Pair: We generate the private key and immediately derive the public key from it.

wg genkey > server_private.key && wg pubkey < server_private.key > server_public.key

Reveal the Public Key: You will need to copy this string into your notes for the configuration later.

cat server_public.key

(Example output: RWx...59s=)

Abstract digital art of two robotic hands exchanging a glowing cryptographic key, representing the generation of WireGuard public and private keys

5.2 Generate the Client Keys

Generate on CLI If you are connecting a Linux/Mac laptop or even your smartphone iPhone/Android you can generate the keys in your terminal (local machine) using the same tools:

wg genkey > client_private.key && wg pubkey < client_private.key > client_public.key

📝 The “Cheat Sheet”

Before moving to the next step, you should have five strings of text ready in a notepad:

  1. Server Private Key: (Stays on Server)
  2. Server Public Key: (Goes to Client)
  3. Client Private Key: (Stays on Client)
  4. Client Public Key: (Goes to Server)
  5. Server IP Address: (Goes to Client)

OpSec Rule: Never send Private Keys over the internet (email, Slack, Discord). If you are generating config files for a friend or a client, generate them locally and transfer them via a secure channel (encrypted USB or Signal), or better yet, let them generate their own keys and just send you the Public Key.

6. Configuring the Interface (The OpenBSD Way)

6.1 Create the Interface File

We will create the configuration for interface wg0.

vi /etc/hostname.wg0

Paste the following configuration. You must replace the placeholders.

inet 10.0.0.1 255.255.255.0
wgkey [YOUR_SERVER_PRIVATE_KEY]
wgport 51820

# Client 1: My Desktop
# wgaip = WireGuard Allowed IPs (The internal IP this client gets)
wgpeer [YOUR_CLIENT_PUBLIC_KEY] wgaip 10.0.0.2/32

# Client 2: My iPhone (Optional)
wgpeer [YOUR_IPHONE_PUBLIC_KEY] wgaip 10.0.0.3/32

up

6.2 Lockdown Permissions

This file contains your Server Private Key. If a hacker reads this file, they can decrypt your traffic. Set the permissions so only root can read it.

chmod 600 /etc/hostname.wg0

6.3 Bring it online

sh /etc/netstart wg0

Verification: Run ifconfig wg0. You should see the interface marked as UP and RUNNING, with your IP (10.0.0.1) assigned.

7. The Firewall (PF Configuration)

A visualization of an OpenBSD PF firewall blocking chaotic red traffic while allowing a single secure green VPN tunnel to pass through.

This is the “Fortress” part of the guide. We need to do two things:

  1. Open the Port: Allow encrypted UDP traffic to hit port 51820.
  2. Enable NAT: When your client(s) sends a request like “https://www.google.com/search?q=google.com” inside the VPN, the server needs to “translate” that packet so it looks like it came from the server’s public IP.

Open the main firewall config:

vi /etc/pf.conf

You will likely see a default configuration. You can delete it or comment it out (#). We are going to replace it with this clean, focused ruleset:

# 1. Macros (Define your variables)
ext_if = "vio0"           # CHANGE THIS: Check your interface with 'ifconfig' (could be em0, vio0)
vpn_if = "wg0"            # Your VPN interface
vpn_net = "10.0.0.0/24"   # The internal VPN network

# 2. Options
set skip on lo            # Ignore loopback traffic

# 3. NAT (The "Magic" Routing)
# Translate traffic coming FROM the VPN, going OUT to the world
match out on $ext_if from $vpn_net to any nat-to ($ext_if)

# 4. Rules
block return              # Default Deny Policy (Block everything unless allowed)
pass out                  # Pass Outbound (Simplify setup)

# Allow SSH (So you don't lock yourself out!)
pass in on $ext_if proto tcp to ($ext_if) port 22

# Allow WireGuard Traffic (The Handshake)
pass in on $ext_if proto udp to ($ext_if) port 51820

# Allow Traffic INSIDE the Tunnel
pass on $vpn_if

Apply the Rules

We need to load this file into the kernel.

pfctl -f /etc/pf.conf

Run the Verification Command

Run the following command:

pfctl -s rules

8. The “Router Switch” (Sysctl)

By default, OpenBSD is a secure workstation, it does not forward packets for other people. If you try to use the VPN now, you will connect to the server, but you won’t be able to reach the internet. We need to turn the server into a router.

Enable Forwarding (Temporary/Immediate)

This turns the feature on right now.

sysctl net.inet.ip.forwarding=1

Enable Forwarding (Permanent)

This ensures the setting survives a reboot.

echo "net.inet.ip.forwarding=1" >> /etc/sysctl.conf

Verification

To confirm the switch is actually “On,” I run:

sysctl net.inet.ip.forwarding

I look for the output: net.inet.ip.forwarding=1

Operator Note (Crucial for the Guide)

In the pf.conf section, the variable ext_if = "vio0" is the #1 point of failure for copy-paste users.

  • Hetzner usually uses vio0.
  • Vultr usually uses vio0.
  • Dedicated Hardware might use em0, ix0, or re0 depending on the driver used.

9. Client Setup (The Linux Desktop)

Now that the server is listening, we need to teach the client how to talk to it. For Linux, I use wg-quick.

Make sure the wireguard-tools package is installed, on Artix or any Arch based distribution you can install it with the following command:

sudo pacman -S wireguard-tools

Think of wg-quick as the Autopilot: Instead of manually typing routing commands every time (flying manually), we hand a “flight plan” (the config file) to wg-quick, and it handles the takeoff and landing automatically.

The “Flight Plan” (wg0.conf) has two distinct sections:

  • [Interface]: The Pilot (You). This defines your identity (IP) and your credentials (Private Key).
  • [Peer]: The Tower (The Server). This defines who you are calling, where they are located on the internet, and what traffic you are sending them.

9.1 Create the Flight Plan

We create the configuration file at /etc/wireguard/wg0.conf and paste in the details. You must replace the placeholders.

[Interface]
# Client Identity
Address = 10.0.0.2/32
PrivateKey = [YOUR_CLIENT_PRIVATE_KEY]
DNS = 9.9.9.9 # We force Quad9's DNS

[Peer]
# The Gateway
PublicKey = [YOUR_SERVER_PUBLIC_KEY]
Endpoint = [YOUR_VPN_SERVER_IP]:51820

# The Traffic Valve
# 0.0.0.0/0 = Send EVERYTHING through the tunnel (Full VPN).
# 10.0.0.0/24 = Send ONLY home traffic through (Split Tunnel).
AllowedIPs = 0.0.0.0/0

# The Heartbeat
# Keeps the connection alive behind NAT (firewalls/routers).
PersistentKeepalive = 25

9.2 Engage Autopilot

With the file saved, we don’t need to mess with complex IP commands. We simply bring the interface up:

sudo wg-quick up wg0

To confirm we are actually airborne and the keys were accepted:

sudo wg show

We look for the “latest handshake”. If it says something like “1 minute, 20 seconds ago,” the digital handshake succeeded. If it says nothing, the bouncer (server) rejected our ID.

10. Verification (The Reality Check)

Everything looks green on the terminal, but I never assume it works until I see the data. The goal is simple: The Mask. When I am connected, the internet should stop seeing my home IP address and start seeing the OpenBSD server’s IP address instead.

I perform a simple “Before and After” test.

A digital figure wearing a blank mask standing in a light tunnel, representing the IP masking and anonymity achieved by connecting to a self-hosted VPN

The Control Test (VPN OFF)

Before I turn anything on, I ask the internet who I am. If I am in the terminal, I use curl:

curl https://ifconfig.me

Result: I see my actual home ISP address (e.g., 85.x.x.x).

The Tunnel Test (VPN ON)

I engage the tunnel:

sudo wg-quick up wg0

Now, I ask the question again:

curl https://ifconfig.me

(Or I just open a browser and go to https://www.whatismyip.com)

The Success Criteria: If the IP returned matches my OpenBSD Server’s IP (e.g., 203.0.113.1), the tunnel is holding. My traffic is physically traveling to the server before exiting to the web. I am effectively browsing from that server’s location.

If I still see my home IP, the “Traffic Valve” (AllowedIPs) isn’t catching the packets, and I have a leak to fix.

11. Troubleshooting

TODO

12. Bonus: iPhone Client Setup (The Digital Boarding Pass)

Typing 44-character Base64 keys on a touchscreen is a recipe for typos and frustration. We don’t do it. Instead, we use the “Boarding Pass” method. We turn the text configuration into a QR code and simply scan it.

The Prerequisites:

  1. The App: We download the official WireGuard app from the Apple App Store.
  2. The Config: We create a new configuration file specifically for the iphone (e.g., iphone.conf) on our Linux machine.
    • Note: Every device needs its own unique “ID badge” (IP and Private Key). Do not reuse the config from your other VPN devices, or they will fight for the connection.
A smartphone scanning a QR code displayed on a Linux terminal screen to instantly configure the WireGuard VPN client for iOS.

12.1 Generate the Visual Pass

On the Linux machine, I use a tool called qrencode to convert the text file into a scannable image right inside the terminal.

First, I install it (if missing):

sudo pacman -S qrencode

Then, I display the config as a QR code on my terminal:

qrencode -t ansiutf8 < /etc/wireguard/iphone.conf

12.2 Scan and Connect

  1. Open the WireGuard App on iPhone.
  2. Tap the + button and select “Scan from QR code”.
  3. Point the camera at the Linux terminal screen.
  4. Name the tunnel (e.g., “VPN Home”) and hit Save.

That’s it. No typing, no errors. We toggle the switch, and the phone is instantly encrypted.


Fuel the Stack

I write these guides the same way I would help a friend in real life: we sit down, have a coffee, and just build the thing.

If you enjoyed the session and want to buy the next round, you can do so here:

Buy Me a Coffee at ko-fi.com

2 Signals

  • Puffy January 7, 2026 at 8:27 pm

    You can go without wireguard-tools on the openbsd server
    Just create the private-key with “openssl rand -base64 32”

    • The Lone Stack
      The Lone Stack January 8, 2026 at 5:44 am

      That is definitely the “pure” way to do it, keeps the dependency list at zero.

Transmit Signal

Your email address will not be published. Required fields are marked *