Configure a Zero-Trust WireGuard VPN with Per-User Access and DNS Blocking on Linux

A basic VPN is easy to set up, but a modern network needs more than “connect and hope for the best.” A Zero-Trust WireGuard VPN is a practical way to give remote users access to only what they need, while keeping everything else blocked by default. In this tutorial you will build a WireGuard server on Linux, create per-user profiles, restrict each user to specific internal subnets, and add DNS-based blocking to reduce malware and ads on connected devices.

Prerequisites

You will need: (1) a Linux server with a public IP (Ubuntu/Debian examples are used), (2) root or sudo access, (3) a domain name (optional but useful), and (4) one or more internal networks to protect (for example, 10.10.0.0/16). You should also know which UDP port you want for WireGuard (the default is 51820/UDP).

Step 1: Install WireGuard

Update packages and install WireGuard:

Ubuntu/Debian:

sudo apt update && sudo apt install -y wireguard iptables-persistent

Enable IP forwarding so the server can route traffic between VPN and internal networks:

sudo sysctl -w net.ipv4.ip_forward=1

Make it persistent:

echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf

Step 2: Generate Server Keys

WireGuard uses simple public/private keys. Create them with tight permissions:

umask 077

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

Read the public key (you will use it in client configs):

sudo cat /etc/wireguard/server.pub

Step 3: Create the WireGuard Interface (wg0)

Create /etc/wireguard/wg0.conf. In this example, the VPN network is 10.44.0.0/24 and the server is 10.44.0.1. Replace eth0 with your public interface name (check with ip a).

[Interface]
Address = 10.44.0.1/24
ListenPort = 51820
PrivateKey = (paste contents of /etc/wireguard/server.key)
SaveConfig = false

Now add firewall/NAT rules so VPN clients can reach internal networks (and optionally the internet). Add these lines under the interface section:

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Start and enable the service:

sudo systemctl enable --now wg-quick@wg0

Verify:

sudo wg

Step 4: Add Per-User Keys and “Allow Only What’s Needed”

For Zero-Trust behavior, you should avoid giving every user full access to your entire internal network. WireGuard supports this with AllowedIPs per peer. Generate a key pair for a user (example: user “alice”):

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

Decide what Alice is allowed to reach. Example: only a file server subnet 10.10.20.0/24 and the VPN IP for Alice (10.44.0.10/32). Add a peer block to /etc/wireguard/wg0.conf:

[Peer]
PublicKey = (paste contents of /etc/wireguard/alice.pub)
AllowedIPs = 10.44.0.10/32, 10.10.20.0/24
PersistentKeepalive = 25

Apply the changes by restarting WireGuard:

sudo systemctl restart wg-quick@wg0

This approach gives Alice an address on the VPN and only routes the required internal subnet through the tunnel. Everything else stays outside the VPN, reducing accidental exposure.

Step 5: Create a Secure Client Configuration

On the client side, create a config that uses Alice’s private key and the server’s public key. If your server public IP is 203.0.113.10 and the port is 51820, a minimal client config looks like this:

[Interface]
PrivateKey = (paste contents of /etc/wireguard/alice.key)
Address = 10.44.0.10/32
DNS = 10.44.0.1

[Peer]
PublicKey = (server public key)
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.10.20.0/24
PersistentKeepalive = 25

Notice that the client’s AllowedIPs includes only the internal subnet she needs. That keeps her internet traffic off the VPN and limits risk if the client device is compromised.

Step 6: Add DNS Blocking (Optional but Recommended)

A simple way to reduce malicious domains is to run a lightweight DNS resolver with blocklists, such as Unbound plus a blocklist, or dnsmasq. A practical option on a small server is dnsmasq:

sudo apt install -y dnsmasq

Bind DNS to the WireGuard interface IP by editing /etc/dnsmasq.conf and adding:

listen-address=10.44.0.1
bind-interfaces

Then point clients to DNS = 10.44.0.1 (as shown earlier). For blocking, you can add entries to /etc/dnsmasq.d/blocked.conf like:

address=/example-bad-domain.com/0.0.0.0

Restart dnsmasq:

sudo systemctl restart dnsmasq

Step 7: Troubleshooting Checklist

If a client connects but cannot reach anything, check these items: (1) confirm the server is listening on UDP 51820 and the cloud firewall allows it; (2) verify AllowedIPs on both server and client match the intended access; (3) ensure IP forwarding is enabled; (4) confirm your PostUp NAT rule uses the correct public interface; and (5) run sudo wg and look for the latest handshake time and transfer counters.

With these steps, you get a modern WireGuard VPN that follows a Zero-Trust mindset: per-user identities, minimal network exposure, and optional DNS filtering for safer browsing on remote devices.

Comments

Popular Posts

Install Ollama and Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install Ollama + Open WebUI on Ubuntu 24.04 with NVIDIA GPU Acceleration (Step-by-Step)

Install a Local AI Chatbot on Ubuntu 24.04 with Ollama and Open WebUI (Step-by-Step)

Trending Now

Recovering from Btrfs Boot Failures Using GUI Tools on Fedora

By the end of this guide the reader will be able to identify a Btrfs‑based Fedora installation, boot from a live USB, list and restore snapshots using the graphical utilities btrfs‑assistant and snapper, and verify that the system returns to a functional state without resorting to the command line. Understanding the Btrfs Layout Used by Fedora Fedora Workstation and Fedora KDE install the root filesystem as a single Btrfs partition that contains two default sub‑volumes. One sub‑volume holds the traditional “/” hierarchy, while the second is dedicated to /var/lib/machines . The latter exists to keep container images out of snapshot operations; it remains empty on systems that do not run virtual machines. Because Btrfs stores data in sub‑volumes rather than separate partitions, a snapshot captures the state of an entire sub‑volume at a point in time. The installer (Anaconda) automatically registers these sub‑volumes with the snapper service. Snapper maintains a series of read‑only ...