Phase log 56: 2026-10-09 - Phase 39: pre-1.0 security review and pentest checklist
Version unchanged (0.38.0): this phase changed no code. It is a read-only review of the surfaces Phases 34 to 38 added, plus the checklist the maintainer must run against a real deployment before tagging 1.0.0 (this environment cannot: no second host, no Windows, no real window). Nothing committed; no commit script (no files changed except this handoff, which is gitignored).
1. What was reviewed (code and tests, 2026-10-09)
| Surface | Result |
|---|---|
Node routes (envv-server/src/nodes.rs) | Off unless --nodes (404 test). Every node route authenticates a per-request Ed25519 signature over method, path, timestamp and body hash; strictly increasing persisted timestamp; revoked node 401; auth failures counted per IP and rate limited (test with 10+ bad tokens). Owner routes use require_owner; a node's signed headers are not a session (test). |
| Command execution on a node | apply/validate/reload come only from the node's own config file; a pushed target is looked up in that config by id (Agent::act), so the hub cannot name a command, a path or a version directory (valid_id rejects .. and /). Commands run with a timeout and scrubbed error text. |
| Pull uploads | Only for a target the owner requested (409 otherwise), base64 decoded, 1 MiB cap, held in memory for a bounded time and dropped on revoke. |
| Approval | Hub withholds the bytes; signed token binds node, target, sha256, expiry, nonce; node verifies independently and spends the nonce on a write; hub key pinned TOFU. 16 mutations in phase log 54, all caught. |
| History API | One owner-only route; dispatcher ops take no file paths (list/show/diff/snapshot/prune/verify/stats/policy/blast/where). Content only masked unless reveal. |
| Dependencies | npm audit --omit=dev --audit-level=high: 0. cargo deny check: advisories, bans, licenses, sources ok. |
| Parsers added in 38 | YAML writer quotes anything not matching a conservative bare-scalar pattern, so a hostile value cannot add a key or a document (tests with reserved words, numbers, newlines, : , #). |
No defect found. Residual risks, accepted and written down:
- A compromised hub can mint approvals (ADR-0143). Mitigation is the node's own
require_approvalplus the pinned key; a device-held signing key is not built. - Auth-failure rate limiting is per source IP. Behind a reverse proxy every node shares an address, so one bad node can lock out the others for the window. Run nodes directly or give the proxy a per-client address; a per-node-id limit is a possible follow-up.
- Long-poll beats hold a connection up to 25 s per node; only enrolled nodes can do it, but nothing caps beats per node.
- The three new project types are unverified in CI until the
exporters.ymljobs run green once. - The VS Code extension has never run in a real editor.
2. Checklist for the maintainer (authorised, own systems only)
Run against a real hub, two nodes (one with require_approval), a workstation.
-
Hub with
--nodesoff: every/api/nodes/*returns 404; with it on, unsigned requests 401. - Replay a captured beat (same timestamp) and one with an old timestamp: 401.
-
Reuse an enrollment token; try 20 wrong tokens: second use refused, then 429 with
Retry-After. - Revoke a node mid-poll: its next request 401, its approvals withdrawn.
- Edit the node config's target path/commands on the host: only that file is the source of commands.
-
On a
require_approvaltarget: push without approval, with another hub's key, with other bytes, replayed: nothing is written, hub audit shows the reason. - Change the vault while a request is pending: a new request opens, the old yes does not apply.
-
Read
/api/historyas a sub-user and with a node's headers: refused. -
Inspect
nodes.json,materialisations.jsonl,approvals-seen.jsonfor 0600 and for absence of any secret value (envv scan --exposedon the state directory). -
Tamper with a history row in the SQLite file;
envv history verifyreports the break. -
Stack outputs: run the
exporters.ymlworkflow once and confirm all three jobs, including the bad-input controls, go green; then consider graduating the types. -
Windows: build,
envv node runas a service, file permission note (no 0600 equivalent). - Real window: the A4/A9/A10 smoke tests listed in phase log 37, and the TLS TOFU flow.
-
Repository settings: Dependabot alerts, code scanning, private vulnerability reporting, Pages,
CATALOGUE_SIGNING_KEYsecret.
3. Next
Sub-phases 28.1, 29.1, 30.1, 30.2, 31.3, 34.1, 38.1, bundle acceptance, and the 1.0.0 viability review (phase log 57).