wgusr
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 genkeysubprocess 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
wireguardkernel module. - Does not bring the interface up (
wg-quick up wg0is your responsibility). - Does not create the
wg-quick@wg0.servicesystemd unit (it ships withwireguard-tools). - Does not configure firewall rules beyond the
nftPostUp/PostDownhooks it emits duringinit. - 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. wgandwg-quickfromwireguard-tools— needed forinitandsync(key generation itself is pure-Python and does not require them).- Loaded
wireguardkernel 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
nfttable withWG_IFACESsets —wgusr initemitsnft add element … WG_IFACES { wg0 }inPostUp. The sets must exist beforewg-quick upruns, otherwise the interface fails to start. Create them in e.g./etc/nftables.d/wireguard.nft:
Apply with:table ip filter { set WG_IFACES { type ifname; } } table ip nat { set WG_IFACES { type ifname; } }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 setupdates just that peer, no disruption to others. - All peers:
wgusr sync --all—wg syncconfdoes a full atomic reconcile.
How it works
wgusr reads and writes wg0.conf using the wgconfig library. Every mutation:
- Acquires an advisory
fcntl.flockon<iface>.lock(shared for reads, exclusive for writes). - Reconciles
# {user}leading comments fromclients/*.pubfiles. - Writes to a temp file in the same directory (mode 0o600).
- 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@wg0service 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) —wgusrrefuses 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-dirorinterfacefrom escaping the intended directory. - After any manual edit to
wg0.conf, runwgusr sync --allto 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)
| File | Size | Uploaded | |
|---|---|---|---|
| wgusr-1.0.0.tar.gz | 32.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|