docs(wireguard): document the ownership model and config storage
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
17d7dc2ad9
commit
7ef1b3e5c8
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue