Skip to content

VPN WireGuard Manager (CT1119)

Read-only WireGuard configuration transformer and operator dashboard. Reads peer state from UniFi Cloud Gateways via forced-command SSH, rewrites raw IP endpoints to stable DNS names, and serves configs/QR codes on demand.

Overview

Property Value
Host CT1119 (Proxmox LXC, px1)
IP 10.44.1.19
Port 8085
Dashboard http://10.44.1.19:8085/
API Docs http://10.44.1.19:8085/docs
Service vpn-wg-manager.service
Working Dir /opt/vpn-wg-manager
Phase 1.2 (Read-Only Dashboard)

Read-Only

CT1119 cannot create, delete, or modify WireGuard peers. It can only read state from the UCG devices via a forced-command SSH key. Private keys are never stored — they are accepted transiently for config/QR rendering and immediately discarded.

Architecture

  Browser ──► CT1119:8085
                │
                ├── GET /           → Operator dashboard
                ├── GET /static/*   → CSS, JS
                ├── GET /api/*      → JSON API
                └── POST /api/*     → Config/QR rendering
                        │
                        │ SSH (forced-command)
                        ▼
         ┌──────────────────────────────┐
         │  uk-ucg (10.44.1.1:51821)   │
         │  fr-ucg (10.35.1.1:51820)   │
         └──────────────────────────────┘

The SSH key at /opt/vpn-wg-manager/.ssh/id_ed25519 is restricted on each UCG to a forced command (/usr/local/bin/wg-export). This exports WireGuard server config and peer list as JSON. No writes are possible.

Sites

Site Name UCG Host WG Port Tunnel Subnet DNS Endpoint
uk UK Silverstone 10.44.1.1 51821 10.44.4.0/24 uk-vpn.charliehub.net
fr FR Le Mans 10.35.1.1 51820 10.35.2.0/24 fr-vpn.charliehub.net

DNS endpoints are stable domain names used in generated client configs instead of raw IPs. This means client configs survive WAN IP changes without reconfiguration.

Operator Dashboard

The dashboard at http://10.44.1.19:8085/ provides a visual inventory without requiring a terminal:

  • Site cards — peer count, server public key, DNS endpoint with resolution status, port, live cache age counter
  • Peer table — filterable by site (All / UK / FR), shows name, tunnel IP, truncated public key
  • Config modal — select AllowedIPs preset (Full Tunnel / Homelab / Custom), optionally paste a private key, generate .conf with Copy and Download buttons, generate QR code
  • Auto-refresh — polls API every 60 seconds, cache age updates every second

The dashboard is zero-build: three static files (index.html, style.css, app.js) served by FastAPI's built-in StaticFiles. No npm, no Node.js, no CDN.

API Endpoints

Method Path Description
GET /health Health check with per-site status
GET /api/sites List sites with peer counts, sync timestamps
GET /api/sites/{site_id} Single site detail
GET /api/clients List all peers (?site=uk to filter)
GET /api/clients/{site_id}/{pubkey}/config Download .conf (placeholder key)
GET /api/clients/{site_id}/{pubkey}/qr QR code (placeholder key)
POST /api/render-config Render .conf with private key in POST body
POST /api/render-qr Render QR with private key in POST body
POST /api/sync Force re-sync from all UCG devices
GET /api/drift DNS endpoint resolution check

Example Usage

# List sites with sync timestamps
curl http://10.44.1.19:8085/api/sites | jq .

# List all peers
curl http://10.44.1.19:8085/api/clients | jq .

# List UK peers only
curl "http://10.44.1.19:8085/api/clients?site=uk" | jq .

# Force sync from UCG devices
curl -X POST http://10.44.1.19:8085/api/sync | jq .

# Check DNS drift
curl http://10.44.1.19:8085/api/drift | jq .

# Health check
curl http://10.44.1.19:8085/health | jq .

Rendering Configs with Private Key

Private keys are accepted via POST body only (never query params, never logged):

curl -X POST http://10.44.1.19:8085/api/render-config \
  -H "Content-Type: application/json" \
  -d '{
    "site_id": "uk",
    "peer_pubkey": "PEER_PUBLIC_KEY",
    "private_key": "PEER_PRIVATE_KEY",
    "allowed_ips": "0.0.0.0/0"
  }'

Caching

  • In-memory cache with 60-second TTL per site
  • Auto-populates on first API request if cache is empty or stale
  • Startup sync runs when the service starts
  • POST /api/sync forces immediate re-sync of all sites
  • last_sync_ts field in /api/sites response tracks wall-clock time of last sync (ISO 8601 UTC)

Service Management

# Status
systemctl status vpn-wg-manager

# Restart (required after Python file changes)
systemctl restart vpn-wg-manager

# Follow logs
journalctl -u vpn-wg-manager -f

# Recent logs
journalctl -u vpn-wg-manager --since "5 minutes ago" --no-pager

Hot Reload for Static Files

Changes to app/static/ (HTML, CSS, JS) take effect immediately — no service restart needed. Only Python file changes (main.py, models.py, etc.) require a restart.

Configuration

Environment variables in /opt/vpn-wg-manager/.env:

Variable Description
UCG_SSH_KEY Path to ed25519 private key for UCG SSH
UCG_SSH_USER SSH username on UCG devices
UCG_UK_HOST UK UCG IP (default: 10.44.1.1)
UCG_FR_HOST FR UCG IP (default: 10.35.1.1)
UK_DNS_ENDPOINT Stable DNS for UK VPN (default: uk-vpn.charliehub.net)
FR_DNS_ENDPOINT Stable DNS for FR VPN (default: fr-vpn.charliehub.net)

Dependencies

fastapi==0.115.6
uvicorn[standard]==0.34.0
httpx==0.28.1
qrcode[pil]==8.0
pydantic==2.10.4
paramiko>=3.4.0

Troubleshooting

Service won't start

# Check for Python errors
journalctl -u vpn-wg-manager --no-pager -n 30

# Test manually
cd /opt/vpn-wg-manager
./venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8085

SSH sync failing for a site

# Test SSH connectivity
ssh -i /opt/vpn-wg-manager/.ssh/id_ed25519 <user>@10.44.1.1

# Force sync and check result
curl -X POST http://10.44.1.19:8085/api/sync | jq .

Dashboard shows "Connection lost"

  • Check the service is running: systemctl is-active vpn-wg-manager
  • Check API responds: curl http://10.44.1.19:8085/api/sites
  • Check browser console for network errors (firewall, routing)

Server public key shows "Key unavailable"

The UCG SSH export didn't return the server key. Force a sync:

curl -X POST http://10.44.1.19:8085/api/sync | jq .

If still null, check the wg-export script on the UCG device.

DNS drift shows failures

CT1119's DNS resolver may not be able to resolve external domains. This is expected if the container uses a local resolver without upstream forwarding for charliehub.net. The drift endpoint is most useful when the server has full DNS resolution.