Architecture
Service Startup Order
depends_on with condition: service_healthy enforces strict ordering:
Peer Registration Flow
peer-watcher.sh
The WireGuard container runs peer-watcher.sh on startup. It:
- Waits in a loop until
wg show wg0succeeds. - Writes
wg show wg0 public-key > /config/keys/server_public.key. - Loads all existing
*.conffiles from/config/peers/viawg addconf. - Enters an
inotifywait -m -e close_writeloop — on each new.conffile, callswg addconf wg0 <file>.
No container restart is ever needed to activate new peers.
The peers/ directory is bind-mounted into both containers:
auth-service:/app/peers(writes peer configs here)wireguard:/config/peers(peer-watcher reads from here)
IP Allocation
def allocate_ip() -> str:
base = INTERNAL_SUBNET.rsplit(".", 1)[0]
used = _get_used_ips()
for i in range(2, 255):
candidate = f"{base}.{i}"
if candidate not in used:
return candidate
raise RuntimeError("No available IP addresses")The server holds 10.13.26.1. Clients are allocated .2 through .254 (253 max). Used IPs are determined by scanning AllowedIPs lines in all existing *.conf files in /app/peers/.
Public Key Validation
def is_valid_wg_pubkey(key: str) -> bool:
if not re.fullmatch(r'[A-Za-z0-9+/]{43}=', key):
return False
decoded = base64.b64decode(key)
return len(decoded) == 32An X25519 public key is 32 bytes. Base64-encoded it produces 44 characters (43 + one = padding). The validator checks both the character pattern and the decoded length.
Peer File Format
Each registered peer is stored at ./peers/<sha256-of-pubkey[:16]>.conf:
[Peer]
PublicKey = <client-public-key>
AllowedIPs = 10.13.26.X/32DNS Resolution Chain
Generated client configs set DNS = 172.29.144.30. Pi-hole forwards non-blocked queries to Unbound at 172.29.144.20:5053 via FTLCONF_dns_upstreams: "${IP_UNBOUND}#5053". Unbound performs full recursive resolution with DNSSEC — no Cloudflare or Google in the chain.
Nginx TLS Termination
listen 443 ssl;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_tickets off;
location / {
proxy_pass http://172.29.144.50:5000;
}All HTTPS traffic proxies to auth-service. The only internet-facing endpoint is POST /addnewpeer. GET /health is internal only (Docker healthcheck).
server_public.key Lifecycle
setup.shgenerates the keypair and writes./wireguard/keys/server_public.key.peer-watcher.shre-writes it on WireGuard startup viawg show wg0 public-key.- auth-service mounts it read-only at
/wg-keys/server_public.key. helpers.pyreads it on every/addnewpeerrequest to include in the returned client config.
The depends_on: wireguard: condition: service_healthy constraint guarantees WireGuard (and therefore the key file) is ready before auth-service starts.