VPN node based in Wireguard with a RESTful API exposed to manage peers.
What you get
- FastAPI service that manages WireGuard peers over HTTP.
- Auto IP allocation when
allowed_ipsare omitted. - Peers persisted to
/config/peers.jsonand restored on startup. - Public
/healthand/metricsendpoints for probes and Prometheus. - Docker/Compose flow that bootstraps
wg0, NAT, and IP forwarding for you.
Important
Disable Source/destination check on the EC2 instance or routing will fail:
- AWS Console → EC2 → Instances → select instance.
- Actions → Networking → Change source/destination check.
- Uncheck the box and save.
Security Groups
- UDP
51820: inbound from0.0.0.0/0(WireGuard). - TCP
8008: inbound only from your management IP (API access).
git clone https://github.com/ragnarok22/wireguard-api.git
cd wireguard-api
API_TOKEN=your_token \
SERVER_ENDPOINT=vpn.example.com:51820 \
docker compose up --buildWhat this does
- Builds the app image with
uv(Python 3.13) and linuxserver/wireguard runtime. - Boots
wg0at10.13.13.1/24, enables NAT + IP forwarding, and restores peers from/config/peers.json. - Exposes UDP
51820(WireGuard) and TCP8008(API).
docker run -d \
--name=wireguard_api \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
-e API_TOKEN=your_secret_token \
-e SERVER_ENDPOINT=vpn.yourdomain.com:51820 \
-e SERVER_PUBLIC_KEY="server_public_key" \
-e WG_INTERFACE=wg0 \
-p 51820:51820/udp \
-p 8008:8008 \
-v /lib/modules:/lib/modules \
-v $(pwd)/config:/config \
--sysctl="net.ipv4.conf.all.src_valid_mark=1" \
--sysctl="net.ipv4.ip_forward=1" \
--restart unless-stopped \
ghcr.io/lugodev/wireguard-api:main| Variable | Required | Default | Purpose |
|---|---|---|---|
API_TOKEN |
Yes | – | Shared secret for X-API-Token auth on peer endpoints. |
SERVER_ENDPOINT |
No | vpn.example.com:51820 |
Host:port shown in generated client configs. Missing port is auto-filled to 51820. |
SERVER_PUBLIC_KEY |
No | Fetched from interface | Server pubkey used in configs; if unset we call wg show <interface> public-key. |
WG_INTERFACE |
No | wg0 |
WireGuard interface the API manages. |
VPN_PORT |
No | 51820 |
WireGuard UDP port (host & container). |
API_PORT |
No | 8008 |
Host-mapped API port. The app still listens on 8008 in-container. |
Persistence
- Peers are stored in
/config/peers.json; mount/configto persist across restarts. service_runalso keeps the server private key at/config/server_private.key.
Base URL defaults to http://localhost:8008 (inside container it always listens on 8008).
Peer operations require X-API-Token: <API_TOKEN>.
curl -X GET http://localhost:8008/peers \
-H "X-API-Token: your_secret_token"curl -X POST http://localhost:8008/peers \
-H "X-API-Token: your_secret_token" \
-H "Content-Type: application/json" \
-d '{"allowed_ips": ["10.13.13.2/32"]}'If public_key is omitted, one will be generated.
If allowed_ips is omitted, the next available IP in the subnet will be automatically allocated.
To generate a ready-to-use WireGuard configuration file directly:
curl -X POST "http://localhost:8008/peers?format=config" \
-H "X-API-Token: your_secret_token" \
-H "Content-Type: application/json" \
-d '{}' > client.confcurl -X GET http://localhost:8008/peers/<PUBLIC_KEY> \
-H "X-API-Token: your_secret_token"Returns a partial config block for the client.
curl -X GET http://localhost:8008/peers/<PUBLIC_KEY>/config \
-H "X-API-Token: your_secret_token"curl -X DELETE http://localhost:8008/peers/<PUBLIC_KEY> \
-H "X-API-Token: your_secret_token"curl http://localhost:8008/health | jqSample response:
{
"status": "healthy",
"version": "0.4.2",
"uptime_seconds": 12.3,
"wireguard_interface": "wg0",
"wireguard_available": true,
"peer_count": 0
}curl http://localhost:8008/metrics
Exposes request metrics (wireguard_api_requests_total, wireguard_api_request_duration_seconds) and WireGuard stats (wireguard_peers_total, wireguard_peer_transfer_rx_bytes, wireguard_peer_transfer_tx_bytes, wireguard_peer_last_handshake_seconds). Scrape interval of 15–30s is typical.
This project uses uv for dependency management and Python 3.13.
- Python 3.13+
uvinstalledmake
make install # Sync dependencies
make run # Run dev server (uvicorn)
make lint # Run ruff check
make format # Run ruff format
make test # Run pytestThanks goes to these wonderful people (emoji key):
Reinier Hernández |
Carlos Lugones |
This project follows the all-contributors specification. Contributions of any kind welcome!