feat(lan): mTLS LAN control-plane listener, read-only (VA-8) #136

Merged
longjacksonle merged 1 commit from feat/VA-8-lan-listener into main 2026-07-14 17:39:40 +02:00

What

The agent advertised itself over mDNS (mdns.rs) but never accepted inbound LAN requests (DEFAULT_PORT was a placeholder with no socket). This adds the matching listener so a peer agent on the same subnet can connect and run a restricted, read-only set of inventory actions.

Trust model: mTLS via the shared agent CA

The listener is a TLS server presenting the agent's enrolled leaf and requiring a client cert, verified against the agent CA the server issues to all its agents. Only a peer holding a valid server-issued agent cert can connect - no separate pairing secret.

Important prerequisite: the agent CA is not shipped by the base enrollment flow (the agent stores only its own leaf + the web-serving root that verifies the server's TLS cert; the agent CA is a separate hierarchy). It is provisioned separately by VS-92 (filed) and stored under the new AGENT_CA_KEY. Until it is present the listener stays disabled (fail-closed): spawn returns None rather than accepting unauthenticated peers. VA-8 is linked depends-on VS-92.

Capability gate (default-deny)

An authenticated peer is confined to a read-only inventory allowlist: ping, sysinfo, osinfo, cpuinfo, disks_list, processlist, networkinterfaces, listen_ports, agent_stats, time_status, coreinfo. Everything else - mutating (exec, runcommands, host_power, ...), session-opening (terminal_*, tunnels, kvm_*), filesystem (file_*, getdir, log_tail), and network-probing (tcp_probe, dns_lookup) - is refused with an error frame and never executed. The per-peer dispatch context carries no stores and no update/uninstall apply, so a mistakenly-allowed action still cannot persist or tear down the agent.

Plumbing

  • New lan module: capability gate, peer client-cert verifier builder, ServerConfig builder, and a TCP+TLS+WS accept loop bound to the mDNS-advertised port.
  • identity::server_cert_resolver presents the enrolled leaf server-side (mirrors the existing client resolver).
  • New AGENT_CA_KEY store key.
  • New optional agent_ca_pem field on the VA-130 agentconfig frame, persisted to AGENT_CA_KEY, so the server (VS-92) can push the agent CA to already-enrolled agents on reconnect without re-enrollment.
  • Spawned fail-closed from main alongside the mDNS responder; torn down on shutdown.

Testing

Unit tests cover the security-critical logic:

  • Capability gate: allows the read-only set, blocks every mutating/session/filesystem/probe action, default-denies unknown/empty actions.
  • Peer verifier: builds from a real CA, fail-closed on empty/garbage PEM.
  • Spawn boundary: disabled without an agent CA (and with an empty one).

401 lib tests pass; clippy --deny warnings and fmt clean; full workspace builds.

The live two-agent mTLS round-trip (acceptance criteria: two agents discover + handshake, read-only round-trip, gate blocks mutating) can only be exercised once VS-92 provisions the agent CA to agents. This PR is the agent side; it is safe to merge now because it is fail-closed until that material exists.

Part of VA-8 (agent side). Depends on VS-92.

## What The agent advertised itself over mDNS (`mdns.rs`) but never accepted inbound LAN requests (`DEFAULT_PORT` was a placeholder with no socket). This adds the matching listener so a peer agent on the same subnet can connect and run a restricted, read-only set of inventory actions. ## Trust model: mTLS via the shared agent CA The listener is a TLS server presenting the agent's enrolled leaf and **requiring** a client cert, verified against the agent CA the server issues to all its agents. Only a peer holding a valid server-issued agent cert can connect - no separate pairing secret. Important prerequisite: the agent CA is **not** shipped by the base enrollment flow (the agent stores only its own leaf + the web-serving root that verifies the server's TLS cert; the agent CA is a separate hierarchy). It is provisioned separately by **VS-92** (filed) and stored under the new `AGENT_CA_KEY`. Until it is present the listener stays **disabled (fail-closed)**: `spawn` returns `None` rather than accepting unauthenticated peers. VA-8 is linked depends-on VS-92. ## Capability gate (default-deny) An authenticated peer is confined to a read-only inventory allowlist: `ping`, `sysinfo`, `osinfo`, `cpuinfo`, `disks_list`, `processlist`, `networkinterfaces`, `listen_ports`, `agent_stats`, `time_status`, `coreinfo`. Everything else - mutating (`exec`, `runcommands`, `host_power`, ...), session-opening (`terminal_*`, tunnels, `kvm_*`), filesystem (`file_*`, `getdir`, `log_tail`), and network-probing (`tcp_probe`, `dns_lookup`) - is refused with an error frame and never executed. The per-peer dispatch context carries no stores and no update/uninstall apply, so a mistakenly-allowed action still cannot persist or tear down the agent. ## Plumbing - New `lan` module: capability gate, peer client-cert verifier builder, `ServerConfig` builder, and a TCP+TLS+WS accept loop bound to the mDNS-advertised port. - `identity::server_cert_resolver` presents the enrolled leaf server-side (mirrors the existing client resolver). - New `AGENT_CA_KEY` store key. - New optional `agent_ca_pem` field on the VA-130 `agentconfig` frame, persisted to `AGENT_CA_KEY`, so the server (VS-92) can push the agent CA to already-enrolled agents on reconnect without re-enrollment. - Spawned fail-closed from `main` alongside the mDNS responder; torn down on shutdown. ## Testing Unit tests cover the security-critical logic: - Capability gate: allows the read-only set, blocks every mutating/session/filesystem/probe action, default-denies unknown/empty actions. - Peer verifier: builds from a real CA, fail-closed on empty/garbage PEM. - Spawn boundary: disabled without an agent CA (and with an empty one). 401 lib tests pass; clippy `--deny warnings` and fmt clean; full workspace builds. The live two-agent mTLS round-trip (acceptance criteria: two agents discover + handshake, read-only round-trip, gate blocks mutating) can only be exercised once VS-92 provisions the agent CA to agents. This PR is the agent side; it is safe to merge now because it is fail-closed until that material exists. Part of VA-8 (agent side). Depends on VS-92.
feat(lan): mTLS LAN control-plane listener, read-only (VA-8)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 12m12s
Create release / Create release from merged PR (pull_request) Has been skipped
5f40883924
The agent advertised itself over mDNS but never accepted inbound LAN requests. Add the matching listener so a peer agent on the same subnet can connect and run a restricted, read-only set of inventory actions.

Trust model is mTLS via the shared agent CA: the listener presents the agent's enrolled leaf and requires a client cert verified against the agent CA that the server issues to all its agents, so only a peer holding a valid server-issued agent cert can connect - no separate pairing secret. The agent CA is not shipped by the base enrollment flow; it is provisioned separately (VS-92, filed) and stored under the new AGENT_CA_KEY. Until it is present the listener stays disabled (fail-closed): spawn returns None rather than accepting unauthenticated peers.

Even an authenticated peer is confined to a default-deny read-only inventory allowlist (ping, sysinfo, osinfo, cpuinfo, disks_list, processlist, networkinterfaces, listen_ports, agent_stats, time_status, coreinfo). Every mutating, session-opening, filesystem, or network-probing action is refused with an error frame and never executed, and the per-peer dispatch context carries no stores and no update/uninstall apply, so a mistakenly-allowed action still cannot persist or tear down the agent.

Plumbing: new `lan` module (capability gate, peer client-cert verifier builder, ServerConfig builder, TCP+TLS+WS accept loop bound to the mDNS-advertised port); `identity::server_cert_resolver` to present the enrolled leaf server-side (mirrors the client resolver); `AGENT_CA_KEY` store key; and an `agent_ca_pem` field on the VA-130 agentconfig frame so the server can push the agent CA to already-enrolled agents on reconnect. Spawned fail-closed from `main` alongside the mDNS responder and torn down on shutdown.

Unit-tested: the capability gate (allow read-only, block every mutating/session/probe action, default-deny unknowns), the peer verifier (builds from a real CA, fail-closed on empty/garbage PEM), and the fail-closed spawn boundary (disabled without an agent CA). The live two-agent mTLS round-trip can only be exercised once VS-92 provisions the agent CA; VA-8 is linked depends-on VS-92.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFsMi3Vmf3pCc7mBdhZqoV
longjacksonle deleted branch feat/VA-8-lan-listener 2026-07-14 17:39:40 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-agent!136
No description provided.