Skip to content
Ivan Dankov
← Back to Projects

Self-Hosted Home VPN

Problem:

I wanted a secure connection back home while travelling, but commercial VPNs replace trust in the local network with trust in someone else's infrastructure.

Solution:

A small Linux gateway at home and a dedicated travel router create an encrypted, kill-switched WireGuard path for every device I carry.

wireguardlinuxraspberry-pinetworkingsecurityself-hosted

The Problem

When I travel, my laptop, phone, and other devices connect through networks I do not control. A commercial VPN can encrypt that first hop, but it also asks me to trust a new company with my traffic, its infrastructure, its logging claims, and its account security.

There is a practical annoyance too: commercial VPN exit addresses are shared by thousands of people. Half the time I am praying a website works and does not decide I am a bot, then switching servers until I find a less busy endpoint with a cleaner reputation. CAPTCHAs and arbitrary blocks are a frustrating price to pay for traffic that is otherwise completely ordinary.

I already have a trusted internet connection at home. I wanted a small, auditable system that would let my devices use it remotely without paying a VPN provider or moving that trust to another third party.

How It Works

A Raspberry Pi on the home network runs WireGuard. The home router forwards a single UDP port to it, while dynamic DNS keeps the endpoint reachable if the residential address changes.

A compact travel router acts as the WireGuard client. Everything connected to its Wi-Fi uses the tunnel automatically, including devices that cannot run a VPN application themselves.

GL.iNet routers are particularly good for this job. They run an OpenWrt-based firmware, support WireGuard natively, and put practical controls such as VPN policies, a kill switch, DNS configuration, and IPv6 settings behind a usable web interface. I get the flexibility of an open Linux networking platform without needing to build a travel router from scratch.

The traffic path is:

device -> travel router -> encrypted WireGuard tunnel -> home gateway -> internet

Each client has its own keypair and tunnel address. Losing one device means I can revoke that peer without rotating credentials for every other client.

Building It

The examples below use documentation-only values. Replace the interface names and networks to match your environment:

  • Home uplink interface: eth0
  • Home LAN: <HOME_LAN_CIDR>
  • Example VPN network: 10.90.0.0/24
  • VPN server address: 10.90.0.1
  • Travel-router address: 10.90.0.2
  • Public endpoint: vpn.example.com

Do not copy an example firewall over an existing ruleset without reviewing how it composes with the rules already on the host. Keep a local recovery path when changing routing or firewall configuration remotely.

Install WireGuard

On a Debian-based Raspberry Pi:

sudo apt update
sudo apt install wireguard-tools nftables
sudo install -d -m 700 /etc/wireguard/clients

Modern Raspberry Pi OS and Debian kernels already include WireGuard support; the package provides the wg and wg-quick tools.

Generate Separate Keys

Create one identity for the server and one for each client. umask 077 makes new private-key files readable only by root:

sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server.key'
sudo sh -c 'wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub'

sudo sh -c 'umask 077; wg genkey > /etc/wireguard/clients/travel-router.key'
sudo sh -c 'wg pubkey < /etc/wireguard/clients/travel-router.key > /etc/wireguard/clients/travel-router.pub'

sudo chmod 600 /etc/wireguard/server.key /etc/wireguard/clients/travel-router.key
sudo chmod 644 /etc/wireguard/server.pub /etc/wireguard/clients/travel-router.pub

Only public keys need to move between peers. Never paste a private key into a ticket, chat message, shell history, screenshot, or public repository.

Configure the Server

Read the two public keys locally:

sudo cat /etc/wireguard/server.pub
sudo cat /etc/wireguard/clients/travel-router.pub

Create /etc/wireguard/wg0.conf, inserting the server’s private key and the travel router’s public key:

[Interface]
Address = 10.90.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>

[Peer]
PublicKey = <TRAVEL_ROUTER_PUBLIC_KEY>
AllowedIPs = 10.90.0.2/32

Protect and start it:

sudo chmod 600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
sudo wg show
ip -br address show wg0

Enable IPv4 Forwarding

printf '%s\n' 'net.ipv4.ip_forward=1' \
  | sudo tee /etc/sysctl.d/99-wireguard-forward.conf >/dev/null
sudo sysctl -p /etc/sysctl.d/99-wireguard-forward.conf

Add Isolation and NAT with nftables

This table limits only traffic entering or leaving wg0, so it can be merged into a wider host policy. It lets VPN clients reach the internet, blocks them from the home LAN and Pi services, rejects unsolicited traffic towards VPN clients, and translates their private addresses on the way out:

table inet home_vpn_filter {
    chain input {
        type filter hook input priority filter; policy accept;
        iifname "wg0" ip protocol icmp accept
        iifname "wg0" drop
    }

    chain forward {
        type filter hook forward priority filter; policy accept;
        iifname "wg0" oifname "eth0" ip saddr 10.90.0.0/24 ip daddr <HOME_LAN_CIDR> drop
        iifname "wg0" oifname "eth0" ip saddr 10.90.0.0/24 accept
        iifname "wg0" drop
        oifname "wg0" ct state established,related accept
        oifname "wg0" drop
    }
}

table ip home_vpn_nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "eth0" ip saddr 10.90.0.0/24 masquerade
    }
}

Save the reviewed rules in a file included by your nftables configuration. Always syntax-check the complete ruleset before applying it:

sudo nft -c -f /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo systemctl enable nftables
sudo nft list ruleset

The example deliberately does not use flush ruleset; erasing an existing firewall is not a safe installation step.

Make the Server Reachable

On the home router:

  1. Reserve a stable LAN address for the Raspberry Pi.
  2. Forward one UDP port to the Pi’s WireGuard listening port.
  3. Configure dynamic DNS if the home public address can change.
  4. Do not enable a DMZ, expose router administration, or forward SSH as a shortcut.

If the ISP uses carrier-grade NAT, an unsolicited inbound port-forward will not work without another rendezvous or relay design.

Create the Travel-Router Profile

The client needs its own private key and the server’s public key:

[Interface]
PrivateKey = <TRAVEL_ROUTER_PRIVATE_KEY>
Address = 10.90.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

Import the complete client profile into the travel router over a trusted local connection, then delete any temporary copy. Configure all connected clients and all destinations to use this tunnel, enable the kill switch, disable WAN fallback, and disable IPv6 unless the server provides a tested IPv6 path.

Check the Server

sysctl net.ipv4.ip_forward
sudo systemctl status wg-quick@wg0 --no-pager
sudo wg show
sudo nft list ruleset

After the client connects, sudo wg show should report a recent handshake and increasing sent/received counters.

Security Model

The useful part was not merely getting a successful handshake. It was making failure safe.

  • Default-deny forwarding: VPN clients can reach the internet, but not my private home network.
  • Minimal exposure: one UDP service is forwarded; remote administration, SSH, and a router DMZ remain closed.
  • Separate client identities: every client can be revoked independently.
  • Kill switch: the travel router blocks non-VPN traffic instead of falling back to the hotel, café, or mobile connection.
  • IPv6 disabled at the travel edge: prevents an untunnelled IPv6 route while the home exit is IPv4-only.
  • Client DNS through the tunnel: the WireGuard profile assigns a public resolver, and the full-tunnel policy carries those client queries through the home connection instead of handing them to the local access network.
  • Restricted key handling: private keys stay in protected configuration files and are never placed in chat, screenshots, or documentation.

The Raspberry Pi firewall performs both forwarding and address translation. Traffic from the VPN subnet is allowed out to the internet, return traffic is statefully accepted, and attempts to reach the home LAN or services on the Pi are dropped.

Verification

Testing from a device behind the travel router confirmed:

  • IPify showed that the public exit matched the home broadband connection.
  • The extended test at DNS Leak Test returned only the resolver configured in the WireGuard profile, not the local mobile or Wi-Fi provider. Because all client destinations use the tunnel, those DNS queries also exited through the home connection. The separate option that controls traffic generated by the router itself remained disabled; it does not exempt connected clients from the VPN policy.
  • Test IPv6 confirmed that no public IPv6 route was available, removing an untunnelled IPv6 path.
  • I verified the kill switch with a failed handshake, not by manually clicking “disconnect”, which can disable the VPN policy as well as the tunnel. With the policy and kill switch still active, I temporarily pointed WireGuard at an unused endpoint port. The tunnel remained selected but could not connect, and clients lost internet access entirely instead of falling back to the upstream network. Restoring the endpoint brought the VPN connection back.
  • The travel router’s local administration page remained available during the simulated outage, so the configuration could be repaired safely.

Tech Stack

  • VPN: WireGuard
  • Gateway: Raspberry Pi running Linux
  • Travel edge: GL.iNet travel router
  • Firewall/NAT: nftables
  • Endpoint discovery: Dynamic DNS
  • Client DNS: Public resolver reached through the tunnel

Trade-offs

Self-hosting removes the commercial VPN provider from the trust chain, but it does not remove trust altogether. The home ISP still carries the exit traffic, and the setup depends on home power, broadband availability, and the security of the gateway itself.

It also has a very different privacy property from a shared commercial VPN: traffic exits through my normal residential connection. That is exactly what I wanted for this project: continuity and control, not anonymity.