From 7ef1b3e5c8dcc98354e9d58dfe29cec603653869 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 22:55:14 +0200 Subject: [PATCH] docs(wireguard): document the ownership model and config storage Co-Authored-By: Claude Opus 4.8 --- docs/wireguard-hub.md | 46 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/docs/wireguard-hub.md b/docs/wireguard-hub.md index eb3358f..5c4211d 100644 --- a/docs/wireguard-hub.md +++ b/docs/wireguard-hub.md @@ -58,9 +58,35 @@ next restart. ## The console (/admin/vpn) -Create, block and remove accesses, and see who is actually connected. Requires -the `vpn.manage` capability (Owner/Admin) — it also hides the nav entry, because -a VPN access reaches the management network. +Create, block and remove accesses, and see who is actually connected. + +**Who sees and does what** (designed with a Codex review; record-level rules live +in `VpnPeerPolicy`, elevated ones are capabilities): + +| | sees own | sees all | creates/blocks/revokes any | downloads a stored config | +|---|---|---|---|---| +| every operator | yes | — | — | own only | +| Developer | yes | `vpn.view.all` | — | own only | +| Admin / Owner | yes | `vpn.view.all` | `vpn.manage.all` | own only | + +Two decisions worth not undoing by accident: + +- **Downloading is owner-only — deliberately not for Owner or Admin.** A config + is a credential, not metadata; sharing one destroys attribution, because + WireGuard records the peer, not which person used a copied key. An + administrator who needs access revokes that one and issues their own. +- **Issuing is not self-service.** An access reaches the management network, so + it needs `vpn.manage.all` even for oneself. + +`kind` (staff|host|system) exists because a null owner previously had to mean +two different things: "belongs to the admins" and "this is a host peer". A staff +access without an owner is a bug; a peer merely found on the interface is +`system` and belongs to nobody. + +Losing the operator roles closes the owner's doors by itself — being staff is +part of ownership, so a role edit outside `revokeStaff()` still cuts the tunnel +paths. Revoking a staff member revokes their peers and purges their stored +configs. The web container has no wg0, so the page never talks to WireGuard. `SyncVpnPeers` runs on the provisioning queue (every minute from the scheduler, plus a throttled @@ -97,8 +123,18 @@ Things that are deliberate, and worth knowing before changing them: - **A host's key cannot be re-used** for an operator access: `wg set` would rewrite that host's allowed-ip and cut its management tunnel. - Generated keypairs come from libsodium in PHP, not `wg genkey` — this - container has no interface, and it keeps generation testable. The private key - is shown once and never stored. + container has no interface, and it keeps generation testable. +- **Storing a config is opt-in per access.** Encrypted with `VPN_CONFIG_KEY`, + never `APP_KEY`: a leaked application key must not also hand over the + management network. Generate one with `head -c 32 /dev/urandom | base64`; + empty means storing is switched off and the console says so. The secret is + overwritten on revocation (`secret_purged_at`). +- **The password is asked on every retrieval**, rate-limited to five attempts. + Laravel's `password.confirm` keeps a 15-minute session stamp, which would + authorise unlimited later downloads from an unattended browser. +- The plaintext never enters a Livewire snapshot (`ConfigHandoff` hands over an + opaque token), and the page stops polling while a config is on screen — + otherwise every five-second poll would put the private key in a response. ## Verifying it actually works