Set up a personal WireGuard VPN on a VPS paid with Bitcoin

· 10 min read

Build a personal WireGuard VPN on an Ubuntu 24.04 VPS: keys, wg0.conf with nftables NAT, IP forwarding, client QR code, DNS, and what a VPN does not hide.

Why run your own VPN, and why pay for it anonymously

A commercial VPN asks you to trust a company you cannot audit, shares an exit IP with thousands of strangers (hello, captchas), and ties your subscription to a payment identity. A WireGuard server on a VPS you control gives you a dedicated exit IP, a kernel-level tunnel with a tiny codebase, and a server whose logs you can see are off because you turned them off.

Paying for that VPS in Bitcoin with no account means the exit IP is not linked to your name through a billing record. Be precise about what that buys: the hosting provider (BitVPS, and Hetzner underneath) can still see that the server exists, how much traffic it moves and when, and the destination IPs of that traffic. A single-hop VPN moves trust from your ISP to your host; it is not Tor and offers no anonymity against a party that can watch both ends. It is excellent for untrusted Wi-Fi, geo-flexibility, and getting your home IP out of every service’s logs.

Prepare the server

Everything below assumes a fresh Ubuntu 24.04 image and a root shell (prefix sudo if you created a user). Patch first, then lock down SSH and the firewall before anything else is listening.

apt update && apt full-upgrade -y
apt install -y ufw unattended-upgrades curl gnupg

# SSH: keys only. Put your public key in /root/.ssh/authorized_keys first and
# confirm you can log in with it before disabling passwords.
cat > /etc/ssh/sshd_config.d/00-keys-only.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl restart ssh

# Firewall: deny everything inbound except SSH for now.
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enable

Install WireGuard and generate server keys

apt install -y wireguard qrencode

# Find the public interface name (eth0 on most Hetzner-based images).
ip route get 1.1.1.1 | awk '{print $5; exit}'

umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
cat /etc/wireguard/server.pub

umask 077 makes the key files readable by root only. Keep the private key on the server; only the public key goes into client configs.

Enable IP forwarding

cat > /etc/sysctl.d/99-wireguard.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl --system

Enable IPv6 forwarding even if you only route IPv4 today. Including ::/0 in the client’s AllowedIPs later prevents IPv6 traffic from bypassing the tunnel on dual-stack networks.

Server configuration: wg0.conf with NAT

Clients get addresses in a private range and the server masquerades them out of its public interface. Ubuntu 24.04 uses nftables natively; the PostUp/PostDown hooks below create and remove a dedicated table so nothing collides with ufw’s rules. Replace eth0 if your interface differs and paste the server private key.

# /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/24, fd00:8::1/64
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>

PostUp = nft add table inet wg; nft add chain inet wg postrouting '{ type nat hook postrouting priority 100; }'; nft add rule inet wg postrouting oifname "eth0" masquerade
PostDown = nft delete table inet wg

# Peers are appended below.
chmod 600 /etc/wireguard/wg0.conf

# Firewall: open the WireGuard port and allow forwarding from wg0 out to eth0.
# ufw's default FORWARD policy is DROP, so the route rule is required.
ufw allow 51820/udp
ufw route allow in on wg0 out on eth0

systemctl enable --now wg-quick@wg0
wg show

Add a client and generate its config plus QR code

Each client has its own keypair and, ideally, a preshared key for post-quantum hedging. Generate the client keys on the server for convenience, build the config, display it as a QR code for phones, and then delete the client’s private key from the server.

cd /etc/wireguard
umask 077
wg genkey | tee phone.key | wg pubkey > phone.pub
wg genpsk > phone.psk

SERVER_IP=$(curl -4 -s https://ifconfig.me)
cat > phone.conf <<EOF
[Interface]
PrivateKey = $(cat phone.key)
Address = 10.8.0.2/32, fd00:8::2/128
DNS = 10.8.0.1

[Peer]
PublicKey = $(cat server.pub)
PresharedKey = $(cat phone.psk)
Endpoint = $SERVER_IP:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF

# Register the peer on the server side.
cat >> wg0.conf <<EOF

[Peer]
# phone
PublicKey = $(cat phone.pub)
PresharedKey = $(cat phone.psk)
AllowedIPs = 10.8.0.2/32, fd00:8::2/128
EOF

# Apply without dropping existing sessions.
wg syncconf wg0 <(wg-quick strip wg0)

# Scan from the WireGuard app, or copy phone.conf to a laptop.
qrencode -t ansiutf8 < phone.conf

# Once imported, remove the client's private material from the server.
shred -u phone.key phone.psk phone.conf

For the next device, use 10.8.0.3/32 and fd00:8::3/128 and a new name. AllowedIPs on the server is a routing table (which tunnel addresses belong to that peer); on the client it is what gets sent into the tunnel, and 0.0.0.0/0, ::/0 means everything.

DNS: do not leak queries around the tunnel

The client config sets DNS to 10.8.0.1, so queries go through the tunnel instead of to whatever resolver the coffee shop handed out. Something has to answer on 10.8.0.1. The simplest option is a recursive resolver on the server itself so your queries do not go to a third party at all.

apt install -y unbound
cat > /etc/unbound/unbound.conf.d/wg.conf <<'EOF'
server:
    interface: 10.8.0.1
    interface: 127.0.0.1
    access-control: 10.8.0.0/24 allow
    access-control: fd00:8::/64 allow
    access-control: 127.0.0.0/8 allow
    hide-identity: yes
    hide-version: yes
    qname-minimisation: yes
    prefetch: yes
EOF
unbound-checkconf && systemctl restart unbound
ufw allow in on wg0 to any port 53

Ubuntu’s systemd-resolved also listens on 127.0.0.53, which does not conflict. If you would rather forward to a public resolver, set DNS = 9.9.9.9 or similar in the client config and skip unbound; the queries still travel inside the tunnel, the resolver just sees your VPS IP instead of your real one.

Verify and troubleshoot

wg show                                   # latest handshake and transfer per peer
nft list table inet wg                    # masquerade rule present
sysctl net.ipv4.ip_forward                # = 1
journalctl -u wg-quick@wg0 --no-pager | tail -20
  • Handshake but no traffic: forwarding is off or ufw is dropping it. Re-run ufw route allow and check sysctl.
  • No handshake: 51820/udp is blocked, the endpoint IP is wrong, or the keys are mismatched. wg show on both ends should display the other side’s public key.
  • Works on Wi-Fi, fails on mobile: some carriers are IPv6-only; that is fine with the dual-stack config above, but make sure the Endpoint uses an address the phone can reach.
  • Check for leaks from the client with a site that shows your IP and DNS resolver; both should point at the VPS.

Which plan to pick

WireGuard is light: the smallest plan (2 vCPU, 4 GB RAM from about $10/mo) will saturate a typical home connection with CPU to spare, and the generous traffic allowance covers everyday use for a handful of devices. Choose a region near where you will be using it (Ashburn or Hillsboro for North America, Falkenstein, Nuremberg or Helsinki for Europe, Singapore for Asia) to keep latency low. Configure a server, pay in Bitcoin, and the magic link with root access shows up in your inbox.

Ready to run this yourself?

Linux KVM servers with NVMe storage in the EU, US and Singapore. Root access arrives by magic link; no account, no KYC.

Configure a server — pay with Bitcoin, no account needed