Secure connections with AutoPass invites, HMAC capabilities, and viewer default.
Release rolling / release (push) Successful in 12m24s

Default peers are read-only; admin requires seed proof and operators redeem AutoPass packages with signed grants. ACL UI and docs match the new trust model.
This commit is contained in:
Raven Scott
2026-07-14 19:38:53 -04:00
parent b8bd4eb902
commit c818edac9d
31 changed files with 2138 additions and 233 deletions
+14 -3
View File
@@ -92,9 +92,16 @@ PEARDOCK_BROWSE_ROOTS=/var/lib/docker/volumes
PEARDOCK_AUDIT=1
```
3. As admin: **Access → Create invite** → send token to operator.
4. Operator connects with server public key + invite token as supported.
5. **Revoke** lost devices from Access.
3. As **admin** (public key + `SERVER_SEED` in the client Add peer form): **Access → Create invite** → share the **AutoPass invite** string (never share `SERVER_SEED`).
4. Operator pastes the AutoPass invite in **Add peer** — the client pairs, receives `{ publicKey, capability }`, and connects with the granted role.
5. Public key alone is **viewer** (read-only). Optional: `PEARDOCK_INSECURE_OPEN_ADMIN=1` restores the old “everyone is admin” dev convenience (loud warning).
6. **Revoke** lost devices from Access.
| Connect with | Role |
|--------------|------|
| Server public key only | `viewer` |
| Public key + `SERVER_SEED` | `admin` (HMAC seed proof; seed stays on the client) |
| AutoPass invite (z32) | Role in package (`viewer` / `operator` / `admin`) |
See [THREAT_MODEL.md](./THREAT_MODEL.md) for hardening rationale.
@@ -117,6 +124,10 @@ Credentials encrypted at rest (AES-GCM derived from `SERVER_SEED`):
| `PEARDOCK_TUNNEL_HOSTS` | Extra allowed tunnel target hosts (comma-separated) |
| `PEARDOCK_UNRESTRICTED_CLI=1` | Broader `docker` CLI for **admin** |
| `PEARDOCK_BROWSE_OPEN=1` | Legacy open host FS browse (discouraged) |
| `PEARDOCK_DEFAULT_ROLE` | Baseline peer role (`viewer` default; avoid `admin` in multi-op) |
| `PEARDOCK_INSECURE_OPEN_ADMIN=1` | Dev only: every peer is admin (loud warning) |
| `PEARDOCK_LEGACY_INVITES=1` | Allow old random 48-hex invite tokens |
| `PEARDOCK_AUTOPASS_DIR` | AutoPass vault directory (default `./peardock-autopass`) |
## Logging
+22 -5
View File
@@ -11,7 +11,7 @@ Wire protocol for PearDock v2 control plane.
| Name | Value | Meaning |
|------|-------|---------|
| `PROTOCOL` | `peardock/rpc` | protomux-rpc protocol id |
| `PROTOCOL_VERSION` | `2` | Negotiated in `handshake` |
| `PROTOCOL_VERSION` | `3` | Negotiated in `handshake` (HMAC auth) |
| Encoding | compact-encoding JSON-ish values | Via `shared/encodings.js` |
Bump `PROTOCOL_VERSION` when breaking request/response shapes.
@@ -34,7 +34,17 @@ flowchart LR
Rank: viewer (1) < operator (2) < admin (3).
`roleAllows(role, method)` requires rank ≥ `MethodRoles[method]` (default **admin** if method missing).
Default peer role when unconfigured: **admin** (`resolveRole` / `PEARDOCK_DEFAULT_ROLE`).
Default peer role when unconfigured: **viewer** (`resolveRole` / `PEARDOCK_DEFAULT_ROLE`).
### Auth modes (handshake)
| Mode | Client provides | Resulting role |
|------|-----------------|----------------|
| `viewer` | Public key only | viewer (read-only) |
| `seed` | `adminProof` HMAC from `SERVER_SEED` | admin |
| `capability` | HMAC capability grant (often via AutoPass package) | role in grant |
Admin proof and capabilities are verified with `HMAC-SHA256` over a key derived from `SERVER_SEED` via HKDF (`shared/crypto-auth.js`). The seed is never sent on the wire.
---
@@ -60,9 +70,12 @@ sequenceDiagram
### Handshake
- Client may send invite token for peer registration.
- Server validates protocol version compatibility.
- Returns assigned **role** and **peerId** (client public key hex).
- Client may send:
- `adminProof: { nonce, mac }` — seed ownership
- `capability` — HMAC grant (AutoPass package)
- `inviteToken` — legacy / capability string (prefer AutoPass)
- Server validates protocol version compatibility + HMAC proofs.
- Returns assigned **role**, **peerId** (stable client public key hex), and `auth.mode`.
### Errors
@@ -74,6 +87,10 @@ Normalized codes (non-exhaustive):
| `RATE_LIMIT_EXCEEDED` | Per-peer limiter |
| `INVALID_ARGS` | Schema validation failed |
| `FEATURE_DISABLED` | Swarm/Holesail/plugins gated off |
| `CAPABILITY_INVALID` / `EXPIRED` / `SPENT` | HMAC grant failed |
| `ADMIN_PROOF_FAILED` | Seed ownership proof rejected |
| `PEER_DENIED` | Revoked or not on allowlist |
| `INVITE_INVALID` | Legacy/capability invite redeem failed |
| Docker-derived | Sanitized via `dockerErrors.js` |
---
+8 -6
View File
@@ -105,7 +105,7 @@ All Engine access goes through **dockerode** on the machine running peardock-ser
### 5. Capabilities are gated by role
Every method has a minimum role in `MethodRoles` (`viewer` < `operator` < `admin`). Unknown methods default to **admin**. Default peer role is **admin** for single-operator convenience; harden for fleets.
Every method has a minimum role in `MethodRoles` (`viewer` < `operator` < `admin`). Unknown methods default to **admin**. Default peer role is **viewer** (read-only); elevate with admin seed proof, AutoPass HMAC capability, `PEARDOCK_ADMIN_KEYS`, or `PEARDOCK_INSECURE_OPEN_ADMIN=1` for single-operator dev.
---
@@ -123,8 +123,8 @@ sequenceDiagram
C->>DHT: connect(publicKey)
DHT-->>C: secret stream (Noise)
C->>S: protomux-rpc open
C->>S: handshake { protocolVersion, inviteToken? }
S-->>C: { peerId, role, protocolVersion, docker ok }
C->>S: handshake { protocolVersion, adminProof? | capability? }
S-->>C: { peerId, role, protocolVersion, auth.mode }
C->>S: listContainers / deploy / …
S->>D: dockerode API
D-->>S: result
@@ -214,11 +214,13 @@ Details: [OPERATOR.md](./OPERATOR.md), [THREAT_MODEL.md](./THREAT_MODEL.md).
flowchart TB
PK["Knows public key"] --> DIAL["Can dial DHT"]
DIAL --> HS["handshake"]
HS --> ROLE{"Role resolution"}
ROLE -->|default| ADMIN["admin if unconfigured"]
ROLE -->|hardened| VIEW["viewer / operator / admin keys"]
HS --> AUTH{"Auth mode"}
AUTH -->|pubkey only| VIEW["viewer"]
AUTH -->|adminProof seed| ADMIN["admin"]
AUTH -->|capability AutoPass| CAP["grant role"]
VIEW --> MR["MethodRoles check"]
ADMIN --> MR
CAP --> MR
MR --> RL["Rate limiter"]
RL --> SCH["schema validateMethodArgs"]
SCH --> HND["Handler → dockerode"]
+17 -9
View File
@@ -12,7 +12,7 @@
| `SERVER_SEED` | Critical: identity + vault key derivation |
| Docker socket access | Critical: full host container control |
| Registry passwords (vault) | High: encrypted at rest |
| Peer invite tokens | Medium: short-lived capability grants |
| HMAC capability grants / AutoPass invites | Medium: short-lived elevated access without seed |
| Audit log | Medium: forensic integrity |
| Container data / env secrets | High: via inspect, logs, exec, archive |
| Holesail `hs://` URLs | High: capability to a published port |
@@ -31,7 +31,7 @@
```
- **Anyone with the server public key** can attempt a DHT connection.
- **Default role is admin** unless `PEARDOCK_DEFAULT_ROLE` / `PEARDOCK_ADMIN_KEYS` / peer policy tighten it.
- **Default role is viewer (read-only)** unless elevated via admin seed proof, HMAC capability (AutoPass), `PEARDOCK_ADMIN_KEYS`, peer policy, or `PEARDOCK_INSECURE_OPEN_ADMIN=1`.
- **Plugins** are off unless `ENABLE_PLUGINS=1`.
- **Swarm** APIs are on by default (`ENABLE_SWARM=0` to disable).
- **Holesail tunnels** are on by default (`ENABLE_HOLESAIL=0` to disable). Each `hs://` URL is a capability to the target port.
@@ -42,8 +42,8 @@
## 3. Adversaries
1. **Remote peer with public key only:** should not get Docker control if allowlist + non-admin default are set.
2. **Stolen invite token:** limited by TTL / max uses; rotate after use.
1. **Remote peer with public key only:** gets **viewer** only (read-only lists/inspect/logs).
2. **Stolen AutoPass invite / capability:** limited by TTL / max uses + HMAC; revoke peer jti path via revokePeer after redeem.
3. **Compromised client machine:** can use any role the peer holds until revoke.
4. **Local host attacker with filesystem:** can steal `SERVER_SEED` and vault if file perms are wrong.
5. **Holder of an `hs://` URL:** can reach the tunneled service until the tunnel is closed / key rotated.
@@ -57,7 +57,12 @@
|---------|-----------|
| Transport E2E | HyperDHT Noise |
| Capability ACL | `viewer` / `operator` / `admin` + `MethodRoles` |
| Peer policy | Invite, register, revoke, optional allowlist |
| Default role | **viewer** (secure); elevate via seed HMAC / capability |
| Admin proof | HMAC-SHA256 of seed-derived macKey (never sends seed on wire) |
| HMAC grants | Signed capability tokens (role, exp, jti); constant-time verify |
| AutoPass invites | Distributes `{publicKey, capability}` packages without seed |
| Peer policy | Register, revoke, capability jti spend, optional allowlist |
| Stable client id | Persistent DHT keypair (`~/.config/peardock/identity.json`) |
| Audit | Append-only log for privileged methods |
| Rate limit | Per-peer limiter on RPC |
| Registry secrets | AES-256-GCM vault |
@@ -69,7 +74,7 @@
## 5. Residual risks
- **Default admin** is intentional for single-operator setup. Change it for multi-tenant fleets.
- **Default viewer** is intentional for production. Use `PEARDOCK_INSECURE_OPEN_ADMIN=1` only for single-operator dev convenience.
- Binary image/export streams are size-capped but still large; DoS via memory if many concurrent transfers.
- JSON-over-RPC is not hyperschema-validated end-to-end; malformed args rely on handler validation.
- Swarm secrets/configs once enabled are highly privileged.
@@ -79,13 +84,16 @@
## 6. Operator hardening checklist
- [ ] Generate unique `SERVER_SEED`; back up offline; never commit `.env`
- [ ] Set `PEARDOCK_DEFAULT_ROLE=viewer` or `operator`
- [ ] Set `PEARDOCK_ADMIN_KEYS=<your client public key hex>`
- [ ] Connect as admin with public key + `SERVER_SEED` in the client (seed is session-only)
- [ ] Share operators via Access → AutoPass invite (never share `SERVER_SEED`)
- [ ] Confirm public-key-only peers are viewer (read-only)
- [ ] Set `PEARDOCK_ADMIN_KEYS=<client public key hex>` for fixed admin machines (optional)
- [ ] Enable `PEARDOCK_PEER_ALLOWLIST=1` after registering operators
- [ ] Do **not** set `PEARDOCK_INSECURE_OPEN_ADMIN` in production
- [ ] Set `PEARDOCK_BROWSE_ROOTS` only if host path pickers are needed
- [ ] Set `ENABLE_SWARM=0` if Swarm APIs are not needed; leave `ENABLE_PLUGINS` off unless required
- [ ] Treat `hs://` tunnel URLs as secrets; set `ENABLE_HOLESAIL=0` if unused
- [ ] File mode `600` on vault, peer policy, tunnels, audit, `.env`
- [ ] File mode `600` on vault, peer policy, autopass dir, tunnels, audit, `.env`, identity
- [ ] Run server as non-root in `docker` group (see `deploy/peardock.service`)
- [ ] Enable `PEARDOCK_AUDIT=1` and review `peardock-audit.log` periodically
- [ ] Revoke lost client keys immediately (`revokePeer` / Access UI)