docs(wireguard): document the ownership model and config storage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 22:55:14 +02:00
parent 17d7dc2ad9
commit 7ef1b3e5c8
1 changed files with 41 additions and 5 deletions

View File

@ -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