Skip to main content

wgusr

Python 3.11+ License: MIT

WireGuard peer management — add, delete, list, view, and sync VPN users on a wg-quick server.

wgusr edits the [Peer] section of your wg0.conf directly: it generates keys and preshared keys, allocates VPN IPs from the existing pool, writes per-user client configs and QR codes, and hot-applies changes to the running interface — all under a file lock so concurrent invocations are safe.


Why wgusr?

Managing wg0.conf by hand means copy-pasting base64 keys, tracking IPs in a spreadsheet, and regenerating QR codes for every new user. wgusr automates that plumbing:

  • Atomic, locked edits — fcntl.flock + temp-file-rename so a crash mid-write never corrupts your config.
  • Automatic IP allocation — derives free IPs from existing AllowedIPs; no sidecar pool file.
  • Key generation in pure Python — no wg genkey subprocess needed.
  • Client artifacts — ready-to-scan WireGuard app configs (ASCII QR + PNG QR) written to <wg-dir>/clients/.
  • Hot sync — apply a single peer or all peers to the live interface without restarting wg-quick.

What wgusr does NOT do

  • Does not install or load the wireguard kernel module.
  • Does not bring the interface up (wg-quick up wg0 is your responsibility).
  • Does not create the wg-quick@wg0.service systemd unit (it ships with wireguard-tools).
  • Does not configure firewall rules beyond the nft PostUp/PostDown hooks it emits during init.
  • Does not provide a web UI.

wgusr edits the config file; wg-quick and the kernel apply it.


Prerequisites

  • Python ≥ 3.11
  • Root — required to read/write /etc/wireguard.
  • wg and wg-quick from wireguard-tools — needed for init and sync (key generation itself is pure-Python and does not require them).
  • Loaded wireguard kernel module:
    modprobe wireguard
    
  • net.ipv4.ip_forward=1 — required for the server to route VPN traffic:
    echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-wireguard.conf
    sysctl --system
    
  • nft table with WG_IFACES sets — wgusr init emits nft add element … WG_IFACES { wg0 } in PostUp. The sets must exist before wg-quick up runs, otherwise the interface fails to start. Create them in e.g. /etc/nftables.d/wireguard.nft:
    table ip filter {
        set WG_IFACES { type ifname; }
    }
    table ip nat {
        set WG_IFACES { type ifname; }
    }
    
    Apply with: nft -f /etc/nftables.d/wireguard.nft.
  • Systemd unit — systemctl enable --now wg-quick@wg0 (or bring the interface up manually).

Installation

Development

make install-dev    # editable install, includes ruff + mypy

Release

make build          # builds dist/wgusr-*.whl and *.tar.gz
make install        # pip install dist/wgusr-*-py3-none-any.whl

Or directly:

pip install .

Verify:

wgusr --version
wgusr --help

Quick start

1. Initialise the interface

sudo wgusr init

wgusr init is interactive. It will ask for:

Prompt Default Description
Listen port 51820 UDP port for the server
Server public address (required) Your server's public hostname or IP, e.g. vpn.example.com
VPN network CIDR 10.0.73.0/24 Tunnel network; server takes .1
Use existing private/public key? n Generate new keys, paste existing, or read from a file

init creates /etc/wireguard/wg0.conf with Address, ListenPort, PrivateKey, SaveConfig = true, and PostUp/PostDown nft hooks. It refuses to overwrite an existing config.

Bring the interface up:

sudo wg-quick up wg0
sudo systemctl enable --now wg-quick@wg0   # optional: start on boot

Print the server's public key (needed by clients):

sudo wg show wg0 public-key

2. Add a user

sudo wgusr add alice vpn.example.com

Output:

user 'alice' added with IP 10.0.73.2

Client config: /etc/wireguard/clients/alice-wg0.conf

QR code (scan with WireGuard app):
███████ ...

Distribute the QR code or the .conf file to the client. The client's [Interface] PrivateKey and the server's [Peer] PublicKey are stored in /etc/wireguard/clients/.

3. Sync the peer to the live interface

Changes to wg0.conf are not applied automatically — wg-quick only reads the config at startup. Use sync to push a peer live without restarting anything:

# sync a single user
sudo wgusr sync alice

# sync all peers at once (wg syncconf)
sudo wgusr sync --all

Usage workflow

# Create a new interface (one-time)
sudo wgusr init

# Add a peer
sudo wgusr add alice vpn.example.com               # auto IP
sudo wgusr add bob  vpn.example.com --ip 10.0.73.50 # fixed IP
sudo wgusr add carol vpn.example.com --select-ip   # interactive

# Push to the live interface
sudo wgusr sync alice          # one peer
sudo wgusr sync --all         # all peers at once

# Inspect
sudo wgusr list                # table: USER, IP, PUBKEY
sudo wgusr view alice          # client config + QR

# Remove a peer
sudo wgusr delete alice        # by name
sudo wgusr delete --key <pubkey>  # by public key (escape hatch)
sudo wgusr clear -y            # wipe ALL peers (confirmation required)

# Short-hand
wgusr -v alice                 # equivalent to: wgusr view alice

wgusr add flags

Flag Description
--ip 10.0.73.x Assign a specific VPN IP
--select-ip Interactively choose from available IPs
--dns 1.1.1.1 DNS server for the client (default: server tunnel IP)
--keepalive 25 PersistentKeepalive seconds (0 = off)
--no-psk Skip preshared key (not recommended)
--existing-keys Use a provided keypair instead of generating
--private-key Private key (requires --existing-keys)
--public-key Public key (requires --existing-keys)

Using wgusr on an existing server

wgusr init refuses to overwrite an existing wg0.conf. For servers you already have running, wgusr works immediately — it only reads the [Interface] section to learn the address, port, and private key:

sudo wgusr list                    # shows existing peers
sudo wgusr add dave vpn.example.com  # appends a new peer
sudo wgusr sync dave               # push dave live

Peers added by hand (without wgusr) may lack the # {user} leading comment. wgusr reconciles these automatically from clients/*.pub files on every command run.


Example output

wgusr list

USER                  IP                 PUBKEY
-------------------------------------------------------------------------------------
alice                 10.0.73.2/32       XElbIX...qFE=                1 user(s)

wgusr add alice

user 'alice' added with IP 10.0.73.2

Client config: /etc/wireguard/clients/alice-wg0.conf

QR code (scan with WireGuard app):
███████
█ ▄▄▄█
█ █▄█ █
...

clients/ directory

/etc/wireguard/clients/
├── alice-wg0.conf    # WireGuard app config (full tunnel)
├── alice-wg0.png    # QR code PNG
├── alice.pri        # client's private key
├── alice.pub        # client's public key
└── alice.psk        # preshared key

Command reference

Global options

Flag Default Description
--wg-dir DIR /etc/wireguard WireGuard config directory
--interface, -i wg0 Interface name
--clients-dir DIR <wg-dir>/clients Where per-user artifacts are stored

wgusr init

Interactively create a new wg0.conf. One-time setup; refuses to overwrite an existing file.

wgusr add <user> <endpoint-host>

Add a peer to wg0.conf, generate keys, allocate an IP, and write client artifacts.

wgusr add alice vpn.example.com
wgusr -i wg0 add bob 192.168.1.100 --ip 10.0.73.50

wgusr delete <user>

Remove a peer and its client artifacts. Cross-checks the # {user} comment against clients/{user}.pub before deleting.

wgusr delete alice
wgusr delete --key XElbIX...qFE=    # direct public-key delete

wgusr list

List all peers with their VPN IPs and public keys.

sudo wgusr list

wgusr view <user>

Print a user's client config and QR code to stdout.

sudo wgusr view alice

wgusr sync <user> / wgusr sync --all

Apply peer(s) to the live interface via wg set (single user) or wg syncconf (all).

sudo wgusr sync alice          # one peer live immediately
sudo wgusr sync --all         # full wg syncconf

wgusr clear

Remove all peers and all client artifacts. Requires --yes or confirmation prompt.

sudo wgusr clear -y

Reloading the live interface

wg-quick reads wg0.conf only at startup. To apply config changes to a running interface:

  • Single peer: wgusr sync <user> — wg set updates just that peer, no disruption to others.
  • All peers: wgusr sync --all — wg syncconf does a full atomic reconcile.

How it works

wgusr reads and writes wg0.conf using the wgconfig library. Every mutation:

  1. Acquires an advisory fcntl.flock on <iface>.lock (shared for reads, exclusive for writes).
  2. Reconciles # {user} leading comments from clients/*.pub files.
  3. Writes to a temp file in the same directory (mode 0o600).
  4. Atomically replaces the target via os.replace (guaranteed atomic on POSIX).

IP addresses are derived from existing peer AllowedIPs — no sidecar pool file is needed. wgusr sync shells out to wg set or wg syncconf to push changes into the kernel; it does not restart wg-quick.


Backup & recovery

/etc/wireguard/clients/ is the source of truth for all private keys, preshared keys, and client configs. To back up or migrate:

tar -czf wg-backup.tar.gz /etc/wireguard/wg0.conf /etc/wireguard/clients/

Security: .pri and .psk files are plaintext (mode 0o600). Store backups encrypted at rest and never sync them to untrusted storage.

To restore: extract the archive and run wgusr sync --all to re-apply all peers.


Troubleshooting

Symptom Fix
error: 'wg' binary not found apt install wireguard-tools (or your distro's package)
wg-quick up wg0 fails on nft add element Define the WG_IFACES sets before starting the interface (see Prerequisites)
VPN clients can't reach the internet sysctl net.ipv4.ip_forward=1 and add a masquerade rule to your nft/iptables
wgusr list shows (no name) Peer lacks a # {user} comment; auto-reconciled on the next wgusr command
Permission denied in clients/ Run wgusr as root
wg-quick@wg0 is running stale config sudo wgusr sync --all to push current conf, or systemctl restart wg-quick@wg0

Multiple interfaces

Manage multiple WireGuard interfaces by passing --interface:

sudo wgusr -i wg1 add dave vpn.example.com
sudo wgusr -i wg1 sync dave

Each interface has its own wg{n}.conf and its own PostUp/PostDown hooks. The PostDown hook removes only that interface from the shared WG_IFACES sets (bug fixed in v0.1.1; older versions flushed the entire set).


Uninstall

pip uninstall wgusr

This removes the CLI. Left behind:

  • /etc/wireguard/wg0.conf — your server config
  • /etc/wireguard/clients/ — all private keys and client configs
  • The running wg-quick@wg0 service and the nft ruleset

Clean these up manually if no longer needed.


Security notes

  • All file writes run under umask 0o077; artifacts are mode 0o600.
  • Private keys are created with exclusive-create (O_CREAT | O_EXCL) — wgusr refuses to overwrite an existing key.
  • All user inputs are validated before use: username, interface name, IP address, public key, and keepalive are checked against strict regexes and ranges.
  • Path-traversal guards prevent wg-dir or interface from escaping the intended directory.
  • After any manual edit to wg0.conf, run wgusr sync --all to bring the live interface in sync.

License

Licensed under the MIT License.

Metadata

Release files for wgusr 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wgusr 1.0.0
File Size Uploaded
wgusr-1.0.0.tar.gz 32.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wgusr 1.0.0
File Interpreter ABI Platform
wgusr-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 60.9 kB

Release files / wgusr-1.0.0.tar.gz

Download URL wgusr-1.0.0.tar.gz
Size 32.5 kB
Tags Source
SHA-256 checksum
How to use checksums
86b05889cdad7e6685a984ac6bb111233e11af7fa58ab2705ce0ce50e49fb5c3
BLAKE2b-256 checksum
How to use checksums
8446438486ad898fd8385d8379b718d853e031400f9037e12a85e2071e599d42
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.5.9

Release files / wgusr-1.0.0-py3-none-any.whl

Download URL wgusr-1.0.0-py3-none-any.whl
Size 28.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
31b4ed89700704a741a0c5f887e6ef124721e2189d42c0bd3337d5bb2c423b9d
BLAKE2b-256 checksum
How to use checksums
cf10ac77bc0434482922a7c64b4a0cbc1dd4348b0331fb1c5b6e0009580ddbf2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.5.9

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page