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
.confwith 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/syncforces immediate re-sync of all siteslast_sync_tsfield in/api/sitesresponse 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.
Related¶
- WireGuard VPN — Hub2 site-to-site WireGuard tunnels
- Network Layout — Overall network architecture
- UniFi API — UniFi controller API (separate service)