WireGuard has rapidly become the VPN protocol of choice for Linux servers and appliances thanks to its compact codebase, strong cryptography, and performance. When combined with systemd-networkd — a modern, declarative network manager built into systemd — you gain a fully reproducible, minimal, and scriptless network stack for production-grade VPN deployments. This article walks through the practical architecture, configuration patterns, and operational considerations for deploying WireGuard with systemd-networkd on Linux servers and workstations.
Why combine WireGuard with systemd-networkd?
Both WireGuard and systemd-networkd share important properties that make them a natural pair for reliable, automatable networking:
- Minimal and deterministic — WireGuard’s small userland footprint and systemd-networkd’s declarative files make configurations reproducible and auditable.
- Native kernel efficiency — WireGuard runs in-kernel (on modern Linux), providing high throughput and low latency; systemd-networkd applies IP addressing, routes, and DNS without heavyweight desktop tooling.
- Service integration — systemd makes it straightforward to control startup order, enforce dependency on network interfaces, and run pre/post commands for key handling or firewall setup.
- Automation-friendly — configuration files are simple plaintext, enabling version control and automated deployment across fleets.
High-level architecture
A typical production setup splits responsibilities:
- WireGuard: cryptographic tunnel, peers, keys, endpoints, keepalive, and AllowedIPs.
- systemd-networkd: assign IP/subnet to the WireGuard interface, configure routes, set MTU, and push DNS to systemd-resolved (if used).
- systemd units: bring up the WireGuard interface in a reproducible way (run ip/wg commands), and ensure firewall / NAT rules are applied after the tunnel exists.
Prerequisites
Before starting, ensure your system has:
- Linux kernel with WireGuard support (modern distributions include it).
- wireguard-tools package (provides the wg and wg-quick tools).
- systemd v239+ (common on modern distributions) and systemd-networkd enabled and running.
Key generation and basic WireGuard facts
Generate a private and public key pair on each peer. The commands are commonly:
wg genkey > privatekey
cat privatekey | wg pubkey > publickey
Important WireGuard config fields:
- PrivateKey — the local private key (never share).
- PublicKey — the peer’s public key used to identify and encrypt traffic.
- Endpoint — remote host and UDP port for the peer (for dynamic peers, keep Endpoint out and rely on persistent connections).
- AllowedIPs — the routes that the peer can send/receive; crucial for split-tunnel vs full-tunnel setups.
- PersistentKeepalive — useful for NAT traversal (e.g., 25 seconds for clients behind NAT).
Design pattern: systemd network + wg setconf
We recommend a pattern that avoids wg-quick for production servers: store a canonical WireGuard config in /etc/wireguard/wg0.conf, use a small systemd unit to create and configure the interface with wg setconf, and use systemd-networkd to manage IP addressing, routes, and DNS via a .network file. This splits concerns and fits well into systemd’s dependency graph.
1) Example /etc/wireguard/wg0.conf (conceptual)
PrivateKey = <server-private-key>
ListenPort = 51820
# Peer blocks follow
[Peer]PublicKey = <peer1-public-key>
AllowedIPs = 10.10.10.2/32
PersistentKeepalive = 25
Keep this file protected (chmod 600) and owned by root.
2) systemd unit to create and configure the interface
Create a unit such as /etc/systemd/system/wg0-setup.service with ExecStart lines that create the device, set the config, and bring it up. Example directives (conceptual):
[Unit]Description=WireGuard wg0 setup
After=network-pre.target
Wants=network-pre.target
[Service]Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ip link add dev wg0 type wireguard
ExecStart=/usr/bin/wg setconf wg0 /etc/wireguard/wg0.conf
ExecStart=/sbin/ip link set up dev wg0
ExecStop=/sbin/ip link set down dev wg0
ExecStop=/sbin/ip link delete dev wg0
[Install]WantedBy=multi-user.target
Enable and start this unit with systemctl enable –now wg0-setup.service. Because it is RemainAfterExit, the interface persists until stopped or deleted. This approach gives you an explicit lifecycle and allows systemd-networkd to further configure the interface.
3) systemd-networkd .network file
Create a network file such as /etc/systemd/network/25-wg0.network to manage IP and routing for wg0. The key is Match to bind to wg0 and then static addressing and DNS settings.
[Match]Name=wg0
[Network]Address=10.10.10.1/24
Gateway=10.10.10.254 (only if you need a gateway through the tunnel)
DNS=10.10.10.1
Domains=example.internal
systemd-networkd will assign the IP, add routes, and inform systemd-resolved if present. This separation ensures that WireGuard handles the cryptographic tunnel and peers while systemd-networkd takes care of traditional IP layer tasks.
Routing and AllowedIPs: patterns and pitfalls
AllowedIPs does double duty: it serves both as the selector for what traffic will be encrypted and as a source of routes that WireGuard will program into the kernel on the peer. Common patterns:
- Site-to-site — set AllowedIPs to the remote subnet (e.g., 10.20.0.0/24) so that traffic to that subnet is sent through the peer.
- Full-tunnel client — set AllowedIPs to 0.0.0.0/0, ::/0 on the client peer to route all traffic through the VPN. Ensure NAT or firewall rules on the server allow egress.
- Split-tunnel client — set AllowedIPs only to specific prefixes that should traverse the VPN.
Remember: incorrect AllowedIPs can create routing loops or inadvertently blackhole traffic. Always validate resulting ip route show output after bringing up the interface.
Firewalling and NAT
On an egress gateway, you’ll typically need to perform NAT or forwarding so VPN clients can reach the internet. With nftables or iptables, add rules that reference the WireGuard interface name (wg0). Because the interface may not exist at boot before your firewall unit runs, wire the firewall unit to depend on the wg0-setup.service or use a systemd drop-in to ensure ordering.
- Example nftables idea: allow udp dport 51820 and MASQUERADE traffic from 10.10.10.0/24 out via the main uplink interface.
- Ensure ip_forward is enabled at the kernel level (sysctl net.ipv4.ip_forward=1).
MTU and performance tuning
WireGuard’s default overhead is low (about ~60 bytes of extra overhead when using UDP). However, on links with smaller MTUs you should set the WG interface MTU explicitly to avoid fragmentation. Example: set MTU=1420 in your .network file under the [Link] or [Network] section depending on your systemd version, or via ip link set dev wg0 mtu 1420 in the setup unit.
For high throughput use-cases, ensure CPU offload features are enabled, consider batching sysctl tuning for socket buffers, and monitor with tools like iperf3 and wg show to verify actual performance.
Operational best practices
- Secrets management — keep PrivateKey files readable only by root (chmod 600), and do not commit them to source control. Use configuration management tooling (Ansible, Salt, etc.) to deploy keys securely.
- Automation — keep /etc/wireguard/*.conf canonical and declarative; use systemd templates (wg-setup@.service) if you deploy multiple interfaces.
- Monitoring — use wg show and systemd journal logs. Expose metrics (peer handshake time, bytes transferred) to Prometheus if you need long-term observability.
- Rolling updates — rotate keys carefully: add new public keys to peers before removing old ones, and use multiple peer entries to avoid downtime.
- Firewall dependency — enforce ordering so that firewall and NAT rules are applied after wg0 is up or ensure your rules are tolerant of missing interfaces.
Troubleshooting checklist
- Check interface exists: ip link show wg0
- View WireGuard status: wg show
- Verify routes: ip route show | grep wg0
- Confirm forwarding: sysctl net.ipv4.ip_forward
- Inspect logs: journalctl -u wg0-setup.service -u systemd-networkd -f
- Test connectivity with ping to AllowedIPs, then escalate to tcp/udp tests
Combining WireGuard with systemd-networkd produces a lean, maintainable networking stack that is especially suitable for infrastructure where reproducibility and service integration matter — such as servers, containers, and virtual appliances. The pattern described above — a small systemd unit to create and configure the WireGuard device, canonical WireGuard config files, and systemd-networkd .network files to manage IP and routing — yields clear separation of concerns and clean lifecycle management via systemd.
For further examples, configuration templates, and managed Dedicated IP VPN options that follow these principles, visit Dedicated-IP-VPN at https://dedicated-ip-vpn.com/.