{"id":43505223,"url":"https://github.com/wg-keeper/wg-keeper-node","last_synced_at":"2026-03-13T22:03:29.962Z","repository":{"id":336112430,"uuid":"1147611372","full_name":"wg-keeper/wg-keeper-node","owner":"wg-keeper","description":"Minimal WireGuard node with REST API for centralized peer management.","archived":false,"fork":false,"pushed_at":"2026-03-08T15:24:05.000Z","size":90,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-08T19:25:21.194Z","etag":null,"topics":["devops","golang","rest-api","security","vpn","wireguard"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wg-keeper.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-02T01:54:36.000Z","updated_at":"2026-03-08T15:24:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/wg-keeper/wg-keeper-node","commit_stats":null,"previous_names":["wg-keeper/wg-keeper-node"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/wg-keeper/wg-keeper-node","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wg-keeper%2Fwg-keeper-node","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wg-keeper%2Fwg-keeper-node/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wg-keeper%2Fwg-keeper-node/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wg-keeper%2Fwg-keeper-node/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wg-keeper","download_url":"https://codeload.github.com/wg-keeper/wg-keeper-node/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wg-keeper%2Fwg-keeper-node/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30477253,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T20:45:58.186Z","status":"ssl_error","status_checked_at":"2026-03-13T20:45:20.133Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["devops","golang","rest-api","security","vpn","wireguard"],"created_at":"2026-02-03T12:12:52.373Z","updated_at":"2026-03-13T22:03:29.956Z","avatar_url":"https://github.com/wg-keeper.png","language":"Go","readme":"\u003ch1 align=\"center\"\u003eWG Keeper Node\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eREST API–driven WireGuard node for central orchestration at scale.\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/wg-keeper/wg-keeper-node/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/wg-keeper/wg-keeper-node/actions/workflows/ci.yml/badge.svg?branch=main\" alt=\"CI\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/wg-keeper/wg-keeper-node/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/wg-keeper/wg-keeper-node\" alt=\"License\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/wg-keeper/wg-keeper-node/releases/latest\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/wg-keeper/wg-keeper-node\" alt=\"GitHub Release\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/wg-keeper/wg-keeper-node/pkgs/container/node\"\u003e\u003cimg src=\"https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fghcr-badge.elias.eu.org%2Fapi%2Fwg-keeper%2Fwg-keeper-node%2Fnode\u0026query=%24.downloadCount\u0026label=image%20pulls\u0026color=blue\" alt=\"Image Pulls\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/wg-keeper/wg-keeper-node\"\u003e\u003cimg src=\"https://codecov.io/gh/wg-keeper/wg-keeper-node/graph/badge.svg\" alt=\"codecov\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/wg-keeper/wg-keeper-node\"\u003e\u003cimg src=\"https://goreportcard.com/badge/github.com/wg-keeper/wg-keeper-node\" alt=\"Go Report Card\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/wg-keeper/wg-keeper-node\"\u003e\u003cimg src=\"https://img.shields.io/github/go-mod/go-version/wg-keeper/wg-keeper-node\" alt=\"Go\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\nWG Keeper Node runs a WireGuard interface on a Linux host and exposes a **REST API** for peer management. It is built to be a minimal, secure node controlled by a single orchestrator that manages many nodes over HTTP.\n\n- **Orchestration-first** — manage hundreds of nodes from one control plane\n- **Security-focused** — small attack surface, API key auth, optional IP allowlists, rate limiting\n- **Production-ready** — WireGuard stats, peer lifecycle, optional persistence, TLS, security headers\n\n## Table of contents\n\n- [Why this project](#why-this-project)\n- [Features](#features)\n- [Architecture](#architecture)\n- [Security](#security)\n- [Requirements](#requirements)\n- [Quick start](#quick-start)\n- [Configuration](#configuration)\n- [Deployment](#deployment)\n  - [Docker Compose — local](#docker-compose--local)\n  - [Docker Compose — production with Caddy](#docker-compose--production-with-caddy)\n  - [Running locally](#running-locally)\n- [API reference](#api-reference)\n- [Peer store persistence](#peer-store-persistence)\n- [Trademark](#trademark)\n\n---\n\n## Why this project\n\nManaging WireGuard at scale means coordinating dozens or hundreds of nodes: adding and removing peers, rotating keys, tracking expiry, and staying consistent after reboots — all without manual `wg` commands on each machine.\n\nWG Keeper Node is the agent that runs on every host. It exposes a single, consistent REST API so a central orchestrator can manage any node identically — regardless of how many there are. Each node stays **lean and security-focused**: small surface area, strict API key auth, post-quantum preshared keys per peer, and no dependency on any external service.\n\n## Features\n\n| Area | Capabilities |\n|------|--------------|\n| **Orchestration** | Central API layer to manage many nodes; automatic IP allocation and key rotation |\n| **Security** | API key auth, optional IP allowlists, rate limiting, TLS, security headers, request ID for tracing |\n| **Resilience** | Post-quantum preshared keys per peer; optional file-based peer store persistence |\n| **Observability** | WireGuard stats, peer activity, auto-generated client configs |\n\n## Architecture\n\n```mermaid\nflowchart LR\n  Orchestrator[Central Orchestrator] --\u003e|REST API\u003cbr/\u003eTCP:51821| API\n  Clients[VPN Clients] --\u003e|WireGuard\u003cbr/\u003eUDP:51820| WG\n\n  subgraph Node[WireGuard Node]\n    API[REST API]\n    WG[WireGuard Interface]\n  end\n\n  WG --\u003e WAN[(WAN/Internet)]\n```\n\n## Security\n\n| Mechanism | Details |\n|-----------|---------|\n| **API key auth** | All protected endpoints require `X-API-Key`; `/healthz` and `/readyz` are public |\n| **IP allowlist** | `server.allowed_ips` — when set, only listed IPs/CIDRs can reach protected routes |\n| **Trusted proxies** | Only `127.0.0.1` and `::1` are trusted as reverse proxies, preventing `X-Forwarded-For` spoofing from external clients |\n| **Rate limiting** | 20 req/s per client IP, burst 30; automatically disabled when an allowlist is configured |\n| **Body limit** | 256 KB maximum; larger requests get `413 Request Entity Too Large` |\n| **Input validation** | Pagination `offset` must be ≥ 0 and `limit` between 1–1000; invalid values return `400 Bad Request` |\n| **Config validation** | `wireguard.routing.wan_interface` is validated against a safe character set (letters, digits, `-`, `_`, `.`) to prevent injection into routing rules |\n| **Security headers** | `X-Content-Type-Options`, `X-Frame-Options`, `X-XSS-Protection`, `Referrer-Policy`; `Strict-Transport-Security` when TLS is enabled |\n| **Request tracing** | Every response includes `X-Request-Id` (UUID v4) |\n| **WireGuard config** | Written with mode `0600`; minimal host surface |\n## Requirements\n\n| | Requirement |\n|---|-------------|\n| **Host** | Linux with WireGuard kernel support; root or `CAP_NET_ADMIN` |\n| **Docker** | Capabilities `NET_ADMIN` and `SYS_MODULE` |\n| **Bare metal** | `wireguard-tools`, `iproute2`, `iptables` |\n\n## Quick start\n\n1. **Clone and enter the repo**\n   ```bash\n   git clone https://github.com/wg-keeper/wg-keeper-node.git \u0026\u0026 cd wg-keeper-node\n   ```\n\n2. **Copy and edit config**\n   ```bash\n   cp config.example.yaml config.yaml\n   # Edit server.port, auth.api_key, wireguard.* as needed\n   ```\n\n3. **Run with Docker Compose**\n   ```bash\n   docker compose up -d\n   ```\n   API: `http://localhost:51821` · WireGuard UDP: `51820`\n\n4. **Create a peer**\n   ```bash\n   curl -X POST http://localhost:51821/peers \\\n     -H \"X-API-Key: YOUR_API_KEY\" \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\"peerId\":\"7c2f3f7a-6b4e-4f3f-8b2a-1a9b3c2d4e5f\"}'\n   ```\n\n## Configuration\n\nConfig is loaded from `./config.yaml` by default. Override the path:\n\n```bash\nNODE_CONFIG=/path/to/config.yaml\n```\n\n`DEBUG=true` or `DEBUG=1` enables verbose logs and detailed API error responses. Do not use in production.\n\n### Server\n\n| Setting | Description |\n|---------|-------------|\n| `server.port` | API port (HTTP, or HTTPS if TLS is configured) |\n| `server.tls_cert` | Path to TLS certificate PEM file; must be set together with `tls_key` |\n| `server.tls_key` | Path to TLS private key PEM file; must be set together with `tls_cert` |\n| `server.allowed_ips` | Optional IPv4/IPv6 addresses or CIDRs; when set, only these IPs can call protected endpoints |\n| `auth.api_key` | API key for all protected endpoints |\n\n### WireGuard\n\n| Setting | Description |\n|---------|-------------|\n| `wireguard.interface` | Interface name (e.g. `wg0`) |\n| `wireguard.listen_port` | WireGuard UDP listen port |\n| `wireguard.subnet` | IPv4 CIDR for peer IP allocation (max prefix `/30`); at least one of `subnet`/`subnet6` is required |\n| `wireguard.server_ip` | Optional IPv4 address for the server within the subnet |\n| `wireguard.subnet6` | IPv6 CIDR for peer IP allocation (max prefix `/126`); optional when `subnet` is set |\n| `wireguard.server_ip6` | Optional IPv6 address for the server within the subnet |\n| `wireguard.routing.wan_interface` | WAN interface used for NAT rules (e.g. `eth0`); only letters, digits, `-`, `_`, `.` are accepted |\n| `wireguard.peer_store_file` | Optional path to a JSON file for persistent peer storage |\n\n## Deployment\n\nOn startup, the node creates `/etc/wireguard/\u003cinterface\u003e.conf` if it does not exist and brings the interface up. In Docker this is handled by `entrypoint.sh` before `wg-quick up`. When running without root, `./wireguard/\u003cinterface\u003e.conf` is used instead.\n\n### Docker Compose — local\n\nSuitable for local use and simple setups. Uses `docker-compose.local.yml`.\n\n1. Copy config:\n   ```bash\n   cp config.example.yaml config.yaml\n   ```\n\n2. *(Optional)* Place TLS certificates in `./certs/`. If not using HTTPS, remove or comment the `./certs:/app/certs:ro` volume in the compose file.\n\n3. Start:\n   ```bash\n   docker compose -f docker-compose.local.yml up -d\n   ```\n\nThe compose file uses `ghcr.io/wg-keeper/node:0.0.6` (or `edge` for the latest `main` build), with `NET_ADMIN` + `SYS_MODULE` capabilities, volumes for `config.yaml` and `./wireguard`, and ports `51820/udp` and `51821`. IPv4/IPv6 forwarding sysctls and an IPv6-capable network are preconfigured; adjust as needed for your environment.\n\n### Docker Compose — production with Caddy\n\nUses `docker-compose.prod-secure.yml` — the REST API is never exposed directly on the host. [Caddy](https://caddyserver.com) is the only HTTP(S) entrypoint.\n\n**Network layout:**\n\n| Service | Host ports | Internal |\n|---------|-----------|----------|\n| `wireguard` | `51820/udp` | REST API on `51821` (Docker-internal only) |\n| `caddy` | `80`, `443` | Reverse-proxies to `wireguard:51821` |\n\n**Start:**\n```bash\ndocker compose -f docker-compose.prod-secure.yml up -d\n```\n\n**Recommended settings for production:**\n\n- Use a long, random `auth.api_key`.\n- Set `server.allowed_ips` to your orchestrator's IPs — only those can call protected endpoints.\n- Restrict ports `80` and `443` at the firewall to your orchestrator only.\n- Point a domain at the node (e.g. `api.example.com`) for automatic HTTPS via Let's Encrypt.\n\n**Example `Caddyfile`:**\n\n```Caddyfile\n# Replace :443 with your domain for automatic HTTPS\n:443 {\n    encode gzip zstd\n    reverse_proxy wireguard:51821\n}\n```\n\nCustomisation tips:\n- **Domain:** replace `:443` with `api.example.com` — Caddy provisions certificates automatically when ports 80/443 are reachable.\n- **Different API port:** update `reverse_proxy wireguard:\u003cport\u003e` to match `server.port` in `config.yaml`.\n- The `caddy` service uses a stock `caddy:2` image — extend the `Caddyfile` freely.\n\n### Running locally\n\n1. Copy config:\n   ```bash\n   cp config.example.yaml config.yaml\n   ```\n\n2. Run:\n   ```bash\n   go run ./cmd/server\n   ```\n\n**Available subcommands:**\n\n| Command | Description |\n|---------|-------------|\n| *(no args)* | Start the API server |\n| `init` | Ensure WireGuard config exists, then exit |\n| `init --print-path` | Same as `init`, also prints the config file path to stdout |\n\n## API reference\n\nAll protected endpoints require the `X-API-Key` header. Every response includes `X-Request-Id` (UUID v4).\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| `GET` | `/healthz` | public | Liveness probe — process is up |\n| `GET` | `/readyz` | public | Readiness probe — WireGuard backend is available |\n| `GET` | `/stats` | required | WireGuard interface statistics |\n| `GET` | `/peers` | required | List peers (paginated) |\n| `GET` | `/peers/:peerId` | required | Peer details and traffic stats |\n| `POST` | `/peers` | required | Create or rotate a peer |\n| `DELETE` | `/peers/:peerId` | required | Delete a peer |\n\n---\n\n### GET /stats\n\n```bash\ncurl http://localhost:51821/stats -H \"X-API-Key: \u003cyour-api-key\u003e\"\n```\n\n```json\n{\n  \"service\": { \"name\": \"wg-keeper-node\", \"version\": \"0.0.6\" },\n  \"wireguard\": {\n    \"interface\": \"wg0\",\n    \"listenPort\": 51820,\n    \"subnets\": [\"10.0.0.0/24\", \"fd00::/112\"],\n    \"serverIps\": [\"10.0.0.1\", \"fd00::1\"],\n    \"addressFamilies\": [\"IPv4\", \"IPv6\"]\n  },\n  \"peers\": { \"possible\": 253, \"issued\": 0, \"active\": 0 },\n  \"startedAt\": \"2026-02-02T00:06:06Z\"\n}\n```\n\n---\n\n### GET /peers\n\nReturns a paginated list of peers.\n\n**Query params:**\n\n| Param | Default | Description |\n|-------|---------|-------------|\n| `offset` | `0` | Number of items to skip; must be ≥ 0 |\n| `limit` | all | Maximum items to return; must be between 1 and 1000 |\n\nInvalid values return `400 Bad Request`.\n\n```bash\ncurl \"http://localhost:51821/peers?offset=0\u0026limit=50\" \\\n  -H \"X-API-Key: \u003cyour-api-key\u003e\"\n```\n\n```json\n{\n  \"data\": [\n    {\n      \"peerId\": \"7c2f3f7a-6b4e-4f3f-8b2a-1a9b3c2d4e5f\",\n      \"allowedIPs\": [\"10.0.0.2/32\"],\n      \"addressFamilies\": [\"IPv4\"]\n    }\n  ],\n  \"meta\": {\n    \"offset\": 0,\n    \"limit\": 50,\n    \"totalItems\": 42,\n    \"hasPrev\": false,\n    \"hasNext\": false\n  }\n}\n```\n\n---\n\n### POST /peers\n\nCreates a new peer, or rotates keys if the peer already exists.\n\n**Request body:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `peerId` | yes | UUIDv4 peer identifier |\n| `expiresAt` | no | RFC3339 timestamp; omit for a permanent peer |\n| `addressFamilies` | no | `[\"IPv4\"]`, `[\"IPv6\"]`, or `[\"IPv4\",\"IPv6\"]`; omit to use all families the node supports |\n\n```bash\ncurl -X POST http://localhost:51821/peers \\\n  -H \"X-API-Key: \u003cyour-api-key\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"peerId\":\"7c2f3f7a-6b4e-4f3f-8b2a-1a9b3c2d4e5f\"}'\n```\n\nThe response contains everything the client needs to configure WireGuard:\n\n```json\n{\n  \"server\": {\n    \"publicKey\": \"\u003cserver-public-key\u003e\",\n    \"listenPort\": 51820\n  },\n  \"peer\": {\n    \"peerId\": \"7c2f3f7a-6b4e-4f3f-8b2a-1a9b3c2d4e5f\",\n    \"publicKey\": \"\u003cpeer-public-key\u003e\",\n    \"privateKey\": \"\u003cpeer-private-key\u003e\",\n    \"presharedKey\": \"\u003cpreshared-key\u003e\",\n    \"allowedIPs\": [\"10.0.0.2/32\"],\n    \"addressFamilies\": [\"IPv4\"]\n  }\n}\n```\n\n\u003e **Note:** The private key is returned only on creation and is never stored by the node.\n\n---\n\n### DELETE /peers/:peerId\n\n```bash\ncurl -X DELETE http://localhost:51821/peers/7c2f3f7a-6b4e-4f3f-8b2a-1a9b3c2d4e5f \\\n  -H \"X-API-Key: \u003cyour-api-key\u003e\"\n```\n\n## Peer store persistence\n\nBy default, peer state is in-memory only and is lost on restart. Enable persistence by setting `wireguard.peer_store_file` to a writable path (e.g. `/var/lib/wg-keeper/peers.json`).\n\n**Lifecycle:**\n\n| Event | Behaviour |\n|-------|-----------|\n| Startup — file missing | Start with an empty store |\n| Startup — invalid JSON or duplicate `peer_id` | Startup fails with a clear error |\n| Startup — file valid | Restore all peers to the WireGuard device; remove any peers outside the current subnets |\n| Peer created / rotated / deleted | Store is written atomically (temp file + rename) |\n| Host reboot, interface recreated | Load file; re-add all stored peers to the device |\n| Subnet changed in config | On next startup, peers outside the new subnets are removed from the store and device |\n\n**File format:** JSON array. Each entry contains `peer_id`, `public_key`, `preshared_key`, `allowed_ips`, `created_at`, and optional `expires_at`. Private keys are never stored. The file is written with mode `0600` — create its directory with tight permissions.\n\n## Trademark\n\nWireGuard® is a registered trademark of Jason A. Donenfeld.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwg-keeper%2Fwg-keeper-node","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwg-keeper%2Fwg-keeper-node","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwg-keeper%2Fwg-keeper-node/lists"}