Automate WireGuard client management with scripts for provisioning and configuration

Intermediate 45 min Apr 03, 2026 357 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Create automated scripts to provision WireGuard clients, manage configurations, and monitor VPN connections. This tutorial builds comprehensive management tools for production WireGuard deployments.

Prerequisites

  • WireGuard server already installed and configured
  • Root or sudo access
  • Basic knowledge of WireGuard configuration
  • Understanding of JSON and bash scripting

What this solves

Managing WireGuard clients manually becomes cumbersome as your VPN infrastructure grows. This tutorial creates automated scripts for client provisioning, configuration management, and monitoring to streamline WireGuard operations. You'll build tools that generate client configurations, distribute keys securely, and track connection health automatically.

Step-by-step configuration

Verify WireGuard server installation

Ensure your WireGuard server is properly configured before setting up automation scripts. This confirms the base infrastructure is working correctly.

sudo systemctl status wg-quick@wg0
sudo wg show
Note: If WireGuard isn't installed, follow our WireGuard server setup guide first.

Install required dependencies

Install tools needed for automated client management including QR code generation and JSON processing utilities.

sudo apt update
sudo apt install -y qrencode jq curl wireguard-tools
sudo dnf install -y epel-release
sudo dnf install -y qrencode jq curl wireguard-tools

Create management directory structure

Set up organized directories for scripts, client configurations, and logs with proper permissions.

sudo mkdir -p /opt/wireguard-manager/{scripts,clients,logs,templates}
sudo mkdir -p /opt/wireguard-manager/clients/{active,revoked,configs}
sudo chown -R root:root /opt/wireguard-manager
sudo chmod -R 755 /opt/wireguard-manager
sudo chmod 700 /opt/wireguard-manager/clients

Create client database file

Initialize a JSON database to track client information, IP assignments, and connection status.

{
  "clients": [],
  "next_ip": 2,
  "subnet": "10.0.0.0/24",
  "server_public_key": "",
  "server_endpoint": "",
  "dns_servers": ["1.1.1.1", "8.8.8.8"]
}
sudo chmod 600 /opt/wireguard-manager/clients/database.json

Create client configuration template

Define a reusable template for generating client configurations with proper security settings.

[Interface]
PrivateKey = CLIENT_PRIVATE_KEY
Address = CLIENT_IP/32
DNS = DNS_SERVERS

[Peer]
PublicKey = SERVER_PUBLIC_KEY
AllowedIPs = 0.0.0.0/0
Endpoint = SERVER_ENDPOINT
PersistentKeepalive = 25

Create client provisioning script

Build the main script that provisions new clients, generates keys, and updates server configuration automatically.

#!/bin/bash

WireGuard Client Provisioning Script

Usage: ./provision-client.sh [server-endpoint] [dns-servers]

set -euo pipefail

Configuration

WG_DIR="/opt/wireguard-manager" CLIENT_DB="$WG_DIR/clients/database.json" SERVER_CONFIG="/etc/wireguard/wg0.conf" TEMPLATE="$WG_DIR/templates/client.conf" LOG_FILE="$WG_DIR/logs/provisioning.log"

Logging function

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" }

Validate input

if [[ $# -lt 1 ]]; then echo "Usage: $0 [server-endpoint] [dns-servers]" echo "Example: $0 john-laptop vpn.example.com:51820 '1.1.1.1,8.8.8.8'" exit 1 fi CLIENT_NAME="$1" SERVER_ENDPOINT="${2:-$(jq -r '.server_endpoint' "$CLIENT_DB")}" DNS_SERVERS="${3:-$(jq -r '.dns_servers | join(",")' "$CLIENT_DB")}"

Validate client name

if [[ ! "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then log "ERROR: Client name must contain only alphanumeric characters, hyphens, and underscores" exit 1 fi

Check if client already exists

if jq -e ".clients[] | select(.name == \"$CLIENT_NAME\")" "$CLIENT_DB" > /dev/null; then log "ERROR: Client '$CLIENT_NAME' already exists" exit 1 fi

Generate client keys

CLIENT_PRIVATE_KEY=$(wg genkey) CLIENT_PUBLIC_KEY=$(echo "$CLIENT_PRIVATE_KEY" | wg pubkey)

Get next available IP

NEXT_IP=$(jq -r '.next_ip' "$CLIENT_DB") SUBNET_BASE=$(jq -r '.subnet' "$CLIENT_DB" | cut -d'/' -f1 | cut -d'.' -f1-3) CLIENT_IP="$SUBNET_BASE.$NEXT_IP"

Get server public key

SERVER_PUBLIC_KEY=$(jq -r '.server_public_key' "$CLIENT_DB") if [[ "$SERVER_PUBLIC_KEY" == "" || "$SERVER_PUBLIC_KEY" == "null" ]]; then SERVER_PUBLIC_KEY=$(sudo wg show wg0 public-key 2>/dev/null || echo "") if [[ "$SERVER_PUBLIC_KEY" == "" ]]; then log "ERROR: Cannot retrieve server public key" exit 1 fi fi

Create client configuration

CLIENT_CONFIG="$WG_DIR/clients/configs/$CLIENT_NAME.conf" sed -e "s/CLIENT_PRIVATE_KEY/$CLIENT_PRIVATE_KEY/g" \ -e "s/CLIENT_IP/$CLIENT_IP/g" \ -e "s/SERVER_PUBLIC_KEY/$SERVER_PUBLIC_KEY/g" \ -e "s/SERVER_ENDPOINT/$SERVER_ENDPOINT/g" \ -e "s/DNS_SERVERS/$DNS_SERVERS/g" \ "$TEMPLATE" > "$CLIENT_CONFIG" chmod 600 "$CLIENT_CONFIG"

Add peer to server configuration

echo "" >> "$SERVER_CONFIG" echo "# Client: $CLIENT_NAME" >> "$SERVER_CONFIG" echo "[Peer]" >> "$SERVER_CONFIG" echo "PublicKey = $CLIENT_PUBLIC_KEY" >> "$SERVER_CONFIG" echo "AllowedIPs = $CLIENT_IP/32" >> "$SERVER_CONFIG"

Update client database

TMP_DB=$(mktemp) jq --arg name "$CLIENT_NAME" \ --arg pubkey "$CLIENT_PUBLIC_KEY" \ --arg ip "$CLIENT_IP" \ --arg created "$(date -Iseconds)" \ --arg status "active" \ '.clients += [{"name": $name, "public_key": $pubkey, "ip": $ip, "created": $created, "status": $status, "last_seen": null}] | .next_ip += 1 | .server_public_key = $ARGS.named.server_pubkey | .server_endpoint = $ARGS.named.endpoint' \ --arg server_pubkey "$SERVER_PUBLIC_KEY" \ --arg endpoint "$SERVER_ENDPOINT" \ "$CLIENT_DB" > "$TMP_DB" mv "$TMP_DB" "$CLIENT_DB"

Generate QR code

QR_FILE="$WG_DIR/clients/configs/$CLIENT_NAME.png" qrencode -t png -o "$QR_FILE" < "$CLIENT_CONFIG" chmod 600 "$QR_FILE"

Restart WireGuard to apply changes

sudo systemctl reload-or-restart wg-quick@wg0 log "SUCCESS: Client '$CLIENT_NAME' provisioned successfully" log "Config: $CLIENT_CONFIG" log "QR Code: $QR_FILE" log "Client IP: $CLIENT_IP" echo "Client '$CLIENT_NAME' has been provisioned successfully!" echo "Configuration file: $CLIENT_CONFIG" echo "QR code: $QR_FILE"
sudo chmod 755 /opt/wireguard-manager/scripts/provision-client.sh

Create client revocation script

Build a script to safely revoke client access by removing their configuration and updating the server.

#!/bin/bash

WireGuard Client Revocation Script

Usage: ./revoke-client.sh

set -euo pipefail

Configuration

WG_DIR="/opt/wireguard-manager" CLIENT_DB="$WG_DIR/clients/database.json" SERVER_CONFIG="/etc/wireguard/wg0.conf" LOG_FILE="$WG_DIR/logs/provisioning.log"

Logging function

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" }

Validate input

if [[ $# -ne 1 ]]; then echo "Usage: $0 " exit 1 fi CLIENT_NAME="$1"

Check if client exists

if ! jq -e ".clients[] | select(.name == \"$CLIENT_NAME\")" "$CLIENT_DB" > /dev/null; then log "ERROR: Client '$CLIENT_NAME' not found" exit 1 fi

Get client public key for removal

CLIENT_PUBLIC_KEY=$(jq -r ".clients[] | select(.name == \"$CLIENT_NAME\") | .public_key" "$CLIENT_DB")

Remove client from server config

TMP_CONFIG=$(mktemp) awk -v client="$CLIENT_NAME" -v pubkey="$CLIENT_PUBLIC_KEY" ' /^# Client:/ { if ($3 == client) skip=1; else skip=0 } /^\[Peer\]/ && skip { skip_peer=1; next } /^PublicKey/ && skip_peer && $3 == pubkey { next } /^AllowedIPs/ && skip_peer { skip_peer=0; skip=0; next } !skip { print } ' "$SERVER_CONFIG" > "$TMP_CONFIG" sudo mv "$TMP_CONFIG" "$SERVER_CONFIG"

Move client files to revoked directory

mkdir -p "$WG_DIR/clients/revoked" if [[ -f "$WG_DIR/clients/configs/$CLIENT_NAME.conf" ]]; then mv "$WG_DIR/clients/configs/$CLIENT_NAME.conf" "$WG_DIR/clients/revoked/" fi if [[ -f "$WG_DIR/clients/configs/$CLIENT_NAME.png" ]]; then mv "$WG_DIR/clients/configs/$CLIENT_NAME.png" "$WG_DIR/clients/revoked/" fi

Update client database

TMP_DB=$(mktemp) jq --arg name "$CLIENT_NAME" \ --arg revoked "$(date -Iseconds)" \ '(.clients[] | select(.name == $name)) |= (.status = "revoked" | .revoked = $revoked)' \ "$CLIENT_DB" > "$TMP_DB" mv "$TMP_DB" "$CLIENT_DB"

Restart WireGuard

sudo systemctl reload-or-restart wg-quick@wg0 log "SUCCESS: Client '$CLIENT_NAME' revoked successfully" echo "Client '$CLIENT_NAME' has been revoked successfully!"
sudo chmod 755 /opt/wireguard-manager/scripts/revoke-client.sh

Create client monitoring script

Build a monitoring script that checks client connection status and updates the database with connection information.

#!/bin/bash

WireGuard Client Monitoring Script

Updates client connection status and last seen timestamps

set -euo pipefail

Configuration

WG_DIR="/opt/wireguard-manager" CLIENT_DB="$WG_DIR/clients/database.json" LOG_FILE="$WG_DIR/logs/monitoring.log" STATS_FILE="$WG_DIR/logs/connection-stats.json"

Logging function

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" }

Get current WireGuard status

WG_STATUS=$(sudo wg show wg0 dump 2>/dev/null || echo "") if [[ -z "$WG_STATUS" ]]; then log "WARNING: Cannot retrieve WireGuard status" exit 1 fi

Initialize stats

STATS=$(cat <Process each client TMP_DB=$(mktemp) cp "$CLIENT_DB" "$TMP_DB" TOTAL_CLIENTS=0 ACTIVE_CONNECTIONS=0 ONLINE_CLIENTS="[]" OFFLINE_CLIENTS="[]" while IFS= read -r client; do CLIENT_NAME=$(echo "$client" | jq -r '.name') CLIENT_PUBKEY=$(echo "$client" | jq -r '.public_key') CLIENT_STATUS=$(echo "$client" | jq -r '.status') if [[ "$CLIENT_STATUS" == "revoked" ]]; then continue fi ((TOTAL_CLIENTS++)) # Check if client is connected PEER_INFO=$(echo "$WG_STATUS" | grep "$CLIENT_PUBKEY" || echo "") if [[ -n "$PEER_INFO" ]]; then LAST_HANDSHAKE=$(echo "$PEER_INFO" | cut -f5) RX_BYTES=$(echo "$PEER_INFO" | cut -f6) TX_BYTES=$(echo "$PEER_INFO" | cut -f7) # Update last seen if handshake is recent (within 3 minutes) CURRENT_TIME=$(date +%s) if [[ "$LAST_HANDSHAKE" != "0" ]] && [[ $((CURRENT_TIME - LAST_HANDSHAKE)) -lt 180 ]]; then LAST_SEEN=$(date -Iseconds -d "@$LAST_HANDSHAKE") ((ACTIVE_CONNECTIONS++)) ONLINE_CLIENTS=$(echo "$ONLINE_CLIENTS" | jq --arg name "$CLIENT_NAME" '. += [$name]') # Update database jq --arg name "$CLIENT_NAME" \ --arg last_seen "$LAST_SEEN" \ --arg rx "$RX_BYTES" \ --arg tx "$TX_BYTES" \ '(.clients[] | select(.name == $name)) |= (.last_seen = $last_seen | .rx_bytes = ($rx | tonumber) | .tx_bytes = ($tx | tonumber))' \ "$TMP_DB" > "${TMP_DB}.tmp" && mv "${TMP_DB}.tmp" "$TMP_DB" else OFFLINE_CLIENTS=$(echo "$OFFLINE_CLIENTS" | jq --arg name "$CLIENT_NAME" '. += [$name]') fi else OFFLINE_CLIENTS=$(echo "$OFFLINE_CLIENTS" | jq --arg name "$CLIENT_NAME" '. += [$name]') fi done < <(jq -c '.clients[]' "$CLIENT_DB")

Update main database

mv "$TMP_DB" "$CLIENT_DB"

Generate statistics

STATS=$(echo "$STATS" | jq \ --arg total "$TOTAL_CLIENTS" \ --arg active "$ACTIVE_CONNECTIONS" \ --argjson online "$ONLINE_CLIENTS" \ --argjson offline "$OFFLINE_CLIENTS" \ '.total_clients = ($total | tonumber) | .active_connections = ($active | tonumber) | .clients_online = $online | .clients_offline = $offline') echo "$STATS" > "$STATS_FILE" log "Monitoring complete: $ACTIVE_CONNECTIONS/$TOTAL_CLIENTS clients online"
sudo chmod 755 /opt/wireguard-manager/scripts/monitor-clients.sh

Create client listing script

Build a utility script to display client information in a readable format for administrative purposes.

#!/bin/bash

WireGuard Client Listing Script

Displays client information in various formats

set -euo pipefail

Configuration

WG_DIR="/opt/wireguard-manager" CLIENT_DB="$WG_DIR/clients/database.json"

Parse arguments

SHOW_ALL=false SHOW_ACTIVE=false SHOW_REVOKED=false OUTPUT_FORMAT="table" while [[ $# -gt 0 ]]; do case $1 in --all) SHOW_ALL=true shift ;; --active) SHOW_ACTIVE=true shift ;; --revoked) SHOW_REVOKED=true shift ;; --json) OUTPUT_FORMAT="json" shift ;; --help) echo "Usage: $0 [--all|--active|--revoked] [--json]" echo " --all Show all clients (default)" echo " --active Show only active clients" echo " --revoked Show only revoked clients" echo " --json Output in JSON format" exit 0 ;; *) echo "Unknown option: $1" exit 1 ;; esac done

Default to show all if no filter specified

if [[ "$SHOW_ACTIVE" == "false" && "$SHOW_REVOKED" == "false" ]]; then SHOW_ALL=true fi

Build jq filter

JQ_FILTER=".clients[]" if [[ "$SHOW_ACTIVE" == "true" ]]; then JQ_FILTER="$JQ_FILTER | select(.status == \"active\")" elif [[ "$SHOW_REVOKED" == "true" ]]; then JQ_FILTER="$JQ_FILTER | select(.status == \"revoked\")" fi if [[ "$OUTPUT_FORMAT" == "json" ]]; then jq "[$JQ_FILTER]" "$CLIENT_DB" else echo "WireGuard Client Status Report" echo "Generated: $(date)" echo "" printf "%-20s %-15s %-12s %-20s %-20s\n" "Name" "IP Address" "Status" "Created" "Last Seen" printf "%-20s %-15s %-12s %-20s %-20s\n" "----" "----------" "------" "-------" "---------" while IFS= read -r client; do if [[ "$client" == "null" ]]; then continue fi NAME=$(echo "$client" | jq -r '.name') IP=$(echo "$client" | jq -r '.ip') STATUS=$(echo "$client" | jq -r '.status') CREATED=$(echo "$client" | jq -r '.created' | cut -d'T' -f1) LAST_SEEN=$(echo "$client" | jq -r '.last_seen // "Never"' | cut -d'T' -f1) printf "%-20s %-15s %-12s %-20s %-20s\n" "$NAME" "$IP" "$STATUS" "$CREATED" "$LAST_SEEN" done < <(jq -c "$JQ_FILTER" "$CLIENT_DB") fi
sudo chmod 755 /opt/wireguard-manager/scripts/list-clients.sh

Set up automated monitoring with cron

Configure cron to run client monitoring automatically and rotate logs to maintain system health.

sudo crontab -e

Add these lines to run monitoring every 5 minutes and daily log rotation:

# WireGuard client monitoring
/5    * /opt/wireguard-manager/scripts/monitor-clients.sh

Daily log rotation

0 0 find /opt/wireguard-manager/logs -name ".log" -mtime +30 -delete

Create management wrapper script

Build a main management script that provides a unified interface for all WireGuard client operations.

#!/bin/bash

WireGuard Manager - Unified client management interface

set -euo pipefail SCRIPT_DIR="/opt/wireguard-manager/scripts" show_help() { cat << EOF WireGuard Manager - Client Management Tool Usage: wg-manager [options] Commands: provision [endpoint] [dns] Provision new client revoke Revoke client access list [--active|--revoked] [--json] List clients monitor Run monitoring check status Show connection statistics Examples: wg-manager provision john-laptop vpn.example.com:51820 wg-manager list --active wg-manager revoke old-device wg-manager status For more details, see: /opt/wireguard-manager/README.md EOF } if [[ $# -lt 1 ]]; then show_help exit 1 fi COMMAND="$1" shift case "$COMMAND" in provision) "$SCRIPT_DIR/provision-client.sh" "$@" ;; revoke) "$SCRIPT_DIR/revoke-client.sh" "$@" ;; list) "$SCRIPT_DIR/list-clients.sh" "$@" ;; monitor) "$SCRIPT_DIR/monitor-clients.sh" "$@" ;; status) if [[ -f "/opt/wireguard-manager/logs/connection-stats.json" ]]; then jq -r '"Last Updated: " + .timestamp + "\nTotal Clients: " + (.total_clients | tostring) + "\nActive Connections: " + (.active_connections | tostring) + "\nOnline: " + (.clients_online | join(", ")) + "\nOffline: " + (.clients_offline | join(", "))' /opt/wireguard-manager/logs/connection-stats.json else echo "No statistics available. Run 'wg-manager monitor' first." fi ;; help|--help|-h) show_help ;; *) echo "Unknown command: $COMMAND" echo "Run 'wg-manager help' for usage information." exit 1 ;; esac
sudo chmod 755 /opt/wireguard-manager/wg-manager
sudo ln -sf /opt/wireguard-manager/wg-manager /usr/local/bin/wg-manager

Initialize the management system

Set up the initial configuration with your server details and run the first monitoring check.

# Update database with server information
sudo jq --arg endpoint "vpn.example.com:51820" \
        --arg dns "1.1.1.1,8.8.8.8" \
        '.server_endpoint = $endpoint | .dns_servers = ($dns | split(","))' \
        /opt/wireguard-manager/clients/database.json > /tmp/db.json

sudo mv /tmp/db.json /opt/wireguard-manager/clients/database.json

Run initial monitoring

sudo /opt/wireguard-manager/scripts/monitor-clients.sh
Note: Replace vpn.example.com:51820 with your actual WireGuard server endpoint.

Verify your setup

Test the automation scripts to ensure they work correctly and integrate with your WireGuard server.

# Test client provisioning
sudo wg-manager provision test-client

Verify client was created

sudo wg-manager list --active

Check WireGuard server configuration

sudo wg show

Test monitoring

sudo wg-manager monitor sudo wg-manager status

Clean up test client

sudo wg-manager revoke test-client

Your client management system should show the test client creation, configuration, and revocation process. The status command should display current connection statistics.

Common issues

SymptomCauseFix
Permission denied accessing databaseIncorrect file ownershipsudo chown root:root /opt/wireguard-manager/clients/database.json
QR code generation failsqrencode not installedInstall qrencode package for your distribution
Cannot retrieve server public keyWireGuard interface not activesudo systemctl status wg-quick@wg0 and restart if needed
Monitoring shows no clients onlineClock synchronization issuessudo systemctl status systemd-timesyncd and sync time
Client configuration not workingFirewall blocking UDP portEnsure UDP port 51820 is open on server firewall
Database corruption errorsInvalid JSON in database filejq . /opt/wireguard-manager/clients/database.json to validate JSON

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle private cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.