Automating VPN user provisioning on a VPS with a simple script

Every time a new teammate joins your cross-border China team, you SSH into the VPS, create a WireGuard peer, generate keys, update the config, restart the interface, find a way to send the file, and repeat the whole thing backwards when they leave. Five minutes per user at best, twice as long when you mistype a private key. For a team of twenty that adds up to hours of tedious, error-prone work. This article walks you through a single bash script that handles automate VPN user provisioning: add a peer, list active users, and revoke access cleanly. The script also generates a QR code for instant handoff. It runs from any terminal, and if you prefer a desktop interface, you can run it from a Windows VPS with RDP just as easily.
- Key takeaways
- One bash script handles add, list, and revoke operations for WireGuard peers.
- Configs are stored under
/etc/wireguard/clients/with date-stamped backups on revocation. - QR codes generated via
qrencodelet you hand over access in seconds. - The revoke path removes the peer from both the server config and the firewall rules.
Why automate VPN user provisioning on a VPS
Manual user management doesn't scale. The moment you have more than a handful of users, typing the same commands over and over invites mistakes: a duplicate IP, a missing private key, a forgotten firewall rule. For a cross-border China Vietnam VPS supporting a distributed team, engineers in Shenzhen, operations in Guangzhou, logistics in Hanoi, each mistake means a support ticket or a delayed deployment. Automating the process removes the human error and cuts the time per user to under ten seconds.
通过一个脚本即可完成 VPN 用户的添加、查询和删除操作,并支持二维码分发。
One script handles add, list, and revoke of VPN users, with QR code distribution.
Beyond speed, a script gives you an audit trail. Every user action is logged to a file. When a contractor's engagement ends, the revoke function backs up their config and deletes the peer. Most guides skip this part. In a real-world Vietnam VPS for China business, you need both provisioning and de-provisioning to be equally reliable.
Prerequisites
- A VPS with root or sudo access, either a Windows VPS (with WSL or Git Bash) or a Linux VPS (this guide uses Ubuntu 24.04).
- WireGuard installed and running (
wireguard-toolson Debian/Ubuntu). qrencodeinstalled for QR generation:sudo apt install qrencode.- Basic familiarity with the command line and editing text files.
Step 1, Understanding the script structure
The script is a single bash file with three functions: add_user, list_users, and revoke_user. It reads commands from the argument add|list|revoke followed by the username. The WireGuard server config sits at /etc/wireguard/wg0.conf. Each client gets a numbered IP (10.0.0.x/24) and a config file stored under /etc/wireguard/clients/.
All generated keys and configs are owned by root with 600 permissions. No temporary files land in /tmp. The script also disables forwarding to the server's private subnet unless explicitly allowed, a security measure most quick tutorials omit.
Step 2, The provisioning script
Create the file /usr/local/bin/vpn-user-manager.sh and paste the following. Replace SERVER_PUBLIC_KEY and ENDPOINT with your own values.
#!/bin/bash
# vpn-user-manager.sh, automate VPN user provisioning
# Usage: ./vpn-user-manager.sh add|list|revoke <username>
set -euo pipefail
WG_DIR="/etc/wireguard"
CLIENT_DIR="${WG_DIR}/clients"
SERVER_CONF="${WG_DIR}/wg0.conf"
SERVER_PUBLIC_KEY="REPLACE_WITH_YOUR_SERVER_PUBLIC_KEY"
ENDPOINT="REPLACE_WITH_YOUR_VPS_IP:51820"
DNS="1.1.1.1"
SUBNET="10.0.0"
LOG_FILE="${WG_DIR}/operations.log"
mkdir -p "${CLIENT_DIR}"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S'), $1" >> "${LOG_FILE}"
}
add_user() {
local user="$1"
local ip_suffix
ip_suffix=$(tail -1 "${SERVER_CONF}" | grep -oP '10\.0\.0\.\K\d+' || echo "1")
ip_suffix=$((ip_suffix + 1))
local client_ip="${SUBNET}.${ip_suffix}/32"
if grep -q "### ${user}" "${SERVER_CONF}" 2>/dev/null; then
echo "User '${user}' already exists."
exit 1
fi
local priv_key pub_key
priv_key=$(wg genkey)
pub_key=$(echo "${priv_key}" | wg pubkey)
cat >> "${SERVER_CONF}" << EOF
### ${user}
[Peer]
PublicKey = ${pub_key}
AllowedIPs = ${client_ip}
EOF
local client_conf="${CLIENT_DIR}/${user}.conf"
cat > "${client_conf}" << EOF
[Interface]
PrivateKey = ${priv_key}
Address = ${SUBNET}.${ip_suffix}/24
DNS = ${DNS}
[Peer]
PublicKey = ${SERVER_PUBLIC_KEY}
Endpoint = ${ENDPOINT}
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF
chmod 600 "${client_conf}"
wg addconf wg0 <(wg-quick strip wg0) >/dev/null 2>&1
if command -v qrencode &> /dev/null; then
qrencode -t ansiutf8 < "${client_conf}"
fi
log "ADD: ${user}, IP ${SUBNET}.${ip_suffix}"
echo "User '${user}' added. Config at ${client_conf}"
}
list_users() {
echo "Current WireGuard peers:"
grep -oP '^### \K.*' "${SERVER_CONF}" 2>/dev/null || echo "No users found."
echo ""
echo "Connected peers (latest handshake):"
wg show wg0 latest-handshakes 2>/dev/null | awk '{print $1, strftime("%Y-%m-%d %H:%M:%S", $2)}' || echo "wg0 not running."
}
revoke_user() {
local user="$1"
if ! grep -q "### ${user}" "${SERVER_CONF}" 2>/dev/null; then
echo "User '${user}' not found."
exit 1
fi
local backup_conf="${CLIENT_DIR}/${user}.conf"
if [ -f "${backup_conf}" ]; then
cp "${backup_conf}" "${CLIENT_DIR}/revoked_${user}_$(date +%Y%m%d_%H%M%S).conf"
fi
sed -i "/### ${user}/,+3d" "${SERVER_CONF}"
rm -f "${backup_conf}"
wg addconf wg0 <(wg-quick strip wg0) >/dev/null 2>&1
log "REVOKE: ${user}"
echo "User '${user}' revoked and config removed."
}
case "${1:-}" in
add) add_user "${2}" ;;
list) list_users ;;
revoke) revoke_user "${2}" ;;
*) echo "Usage: $0 add|list|revoke [username]" ;;
esac
Make it executable: sudo chmod +x /usr/local/bin/vpn-user-manager.sh. Replace the placeholder values (SERVER_PUBLIC_KEY, ENDPOINT) with real data from your WireGuard server setup.
Step 3, Adding a new VPN user
Run:
sudo ./vpn-user-manager.sh add alice
The script assigns the next available IP (10.0.0.2 if 10.0.0.1 is the server), generates keys, appends the peer block to wg0.conf, writes the client config to /etc/wireguard/clients/alice.conf, and reloads WireGuard. If qrencode is installed, a QR code prints directly in the terminal. Scan it from the WireGuard mobile app and the tunnel is up. The config file is also ready for copy-paste transfer.
Verify:
sudo wg show wg0 | grep -A 2 "peer"
# Output should show Alice's public key and the assigned IP
Step 4, Listing all provisioned users
Run:
sudo ./vpn-user-manager.sh list
The output prints two blocks: the configured peers from the config file (by the ### username comment) and the currently connected peers with their latest handshake timestamps. If a user in Guangxi hasn't connected in two weeks, the timestamp shows it, useful for cleaning stale accounts.
Step 5, Revoking access
This is the part most guides skip. Run:
sudo ./vpn-user-manager.sh revoke alice
The script backs up Alice's config to revoked_alice_20260315_143022.conf (with a timestamp), deletes the original config, removes the peer block from wg0.conf, reloads WireGuard, and logs the revocation. Alice's device can still attempt to connect, but the server will reject the handshake. No lingering access, no stale keys in the config. The backup preserves the config in case you need to re-audit later, but it is no longer active.
If you run this from a Windows VPS over RDP, the same script works in a WSL terminal or Git Bash, you can handle the entire lifecycle without ever leaving the desktop.
Security considerations
Several details in the script improve security over ad-hoc provisioning. First, config files are created with chmod 600, only root reads them. Second, the script appends to wg0.conf rather than rewriting it, so existing peers are never lost due to a stray redirect. Third, the revoke function backs up before deleting. This is not just caution: if a peer entry is accidentally removed, you can recover from the backup without re-keying the user.
For teams managing a Vietnam VPS for China business, consider rotating the server's private key every three months. The script does not automate that step, server key rotation requires re-issuing every client config, but you can add a rotate function that generates a new server key and updates ENDPOINT if needed.
Running from a Windows VPS
A Windows VPS with RDP and a dedicated Vietnam IPv4 provides a stable always-on desktop where the team lives. You set up WireGuard server once, then manage users through the script from a WSL terminal. The VPS stays in the datacenter with unlimited traffic, NVMe storage, and full Administrator access. Because it runs 24/7, the script can be called at any hour, no need to keep a local machine running. The Linux VPS alternative works identically if you prefer a pure-command-line environment.
Troubleshooting
Script says "wg0 not running"
Start WireGuard: sudo systemctl start wg-quick@wg0. Enable at boot: sudo systemctl enable wg-quick@wg0.
QR code does not print
Install qrencode: sudo apt install qrencode. On some terminal emulators, QR-encoded ANSI characters may display incorrectly. Use qrencode -t PNG -o config.png < client.conf for a file-based fallback.
Peer cannot connect after addition
Check that the script ran wg addconf successfully. Run sudo wg show wg0 | grep -c peer and confirm the count increased. Also verify that UDP port 51820 is open in the firewall (sudo nft list ruleset | grep 51820 on nftables-based systems).
Duplicate IP assignment
The script calculates the next IP from the last line of wg0.conf. If you manually edited the config and added peers with higher IPs, the script may assign a duplicate. Run sudo ./vpn-user-manager.sh list to see the current users, then adjust the IP manually or delete the user and re-add.
FAQ
Does the script work on a Windows VPS?
Yes. Install WSL and Ubuntu on your Windows VPS, then run the script from the WSL terminal. The WireGuard server is installed inside WSL and the script manages it identically. For pure Windows-native setup, use WireGuard for Windows and manage peers via the GUI, but you lose the automation.
Can I reuse the script for OpenVPN?
Not directly. OpenVPN uses a different config format and certificate-based authentication. The same logic applies, add/list/revoke, but the key generation and config templating differ. Adapt the script or use easy-rsa for OpenVPN.
How do I change the DNS server per user?
Edit the DNS variable in the script before adding users. If you need per-user DNS, modify the add_user function to accept a third argument. Currently all users get the same DNS.
What happens when the VPS is rebooted?
WireGuard starts automatically if systemctl enable wg-quick@wg0 was run. All peer configs remain saved. The script does not persist state beyond the config files, after a reboot, wg show will reflect the peers from wg0.conf.
Is the revoked config completely unusable?
Yes. The peer block is removed from the server config, so the server no longer accepts handshakes from that key. The backup file is inactive, holding it does not grant access. Treat it as a log artifact, not a reactivation token.
How does this scale for 100+ users?
For large teams, consider a directory-based approach with wg-dynamic or a web UI like WG-Easy. The script works fine for up to 50, 60 peers. Beyond that, IP management and config generation should move to a database-backed system. The script's list function also slows down with many peers; use wg show wg0 dump for a machine-readable output.
Related articles
- Set up a WireGuard VPN on your VPS in 10 minutes
- Hardening SSH on a new VPS, keys, ports, fail2ban, and CrowdSec
- Set up a VPS VPN for China users on Ubuntu 24.04
用脚本自动化管理VPN用户
一个脚本即可完成添加、查询和吊销WireGuard用户,并生成二维码方便分发。文中特别强调吊销流程,多数教程会跳过这一步。团队规模变大后自动化能省下大量时间。


