- Rust 97.6%
- Just 1.6%
- Dockerfile 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
Publish crates / cargo publish (Forgejo Cargo registry) (push) Successful in 13s
Check / fmt + clippy + tests (push) Successful in 24m50s
Build Linux package / Build vervain-agent and publish generic package (push) Successful in 21m48s
Build Windows agent / Cross-compile, sign, and publish vervain-agent.exe (push) Successful in 32m56s
Build macOS package / Build vervain-agent (macOS) and publish generic package (push) Has been cancelled
Check macOS features / cargo check --features agent-gui (aarch64-apple-darwin) (push) Has been cancelled
Check macOS features / cargo check --features agent-gui (x86_64-apple-darwin) (push) Has been cancelled
Reviewed-on: #158 |
||
| .cargo | ||
| .forgejo/workflows | ||
| .idea | ||
| crates | ||
| docs | ||
| oci-build | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| compose.yml | ||
| Dockerfile | ||
| justfile | ||
| README.md | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
Vervain Agent (Rust)
A Rust agent for the Vervain remote-management platform. Single-language, async, memory-safe; ships as one static binary.
Status
Phase 1 (control-channel MVP) is functional against a stub server. See ROADMAP.md for the phased plan.
What works today:
- Encrypted local persistence (VA-68): config + identity + the enrolled
mTLS cert/key + server CA in two SQLCipher (encrypted SQLite) databases,
unlocked by the
VERVAIN_DB_KEYenvironment variable set in the service unit. Seedocs/local-settings-store.md. - Wire-protocol type definitions and binary handshake encode/decode
(
vervain-agent-protocol), with round-trip tests against the documented packed-struct layouts. vervain-agent --checkvalidates the config database (read-only: never creates a database) and exits.vervain-agent configsubcommand to inspect and edit the config database:show [--reveal],get <key>,set <key> <value>, andimport --from <file>. See Configuration.- Persistent identity: RSA-3072 self-signed root cert generated and stored in the encrypted SQLite data database.
- TLS plus WebSocket transport with the secondary handshake state
machine:
AgentCommitDate,HostInfo,ServerId,AuthRequest,AuthVerify,AuthInfo,AuthConfirm. Server SPKI is pinned from theServerIDkey in the settings store; the agent verifies the server's RSA-PKCS1v1.5 signature against that pin. - Idle loop: post-auth, the agent answers
CoreModuleHash(11) with a 4-byte "no core" reply and logs other server traffic.CoreModule(10) andCompressedCoreModule(20) pushes are explicitly ignored (see "No JS core" decision below). - Self-update receiver (Phase 2 scaffold):
AgentHash(12) replies with the SHA-384 of the running binary, with any config block embedded in the executable (the appended-config sentinel, distinct from the on-disk settings store) stripped before hashing.AgentUpdate(13) andAgentUpdateBlock(14) frames stage an.updatefile beside the agent and verify its hash onEnd. ThedisableUpdate,forceUpdate, andlogUpdateflags from the settings store are honored. Atomic binary swap and restart is not yet implemented; verified payloads stay staged on disk for now. - Integration tests in
crates/vervain-agent/tests/handshake.rs: spin up a stub TLS plus WebSocket server and drive the agent through the full handshake. Four scenarios: happy path,ServerIDpin mismatch, bad server signature, andAgentHashquery/response after auth.
What does not work yet: applying a verified self-update (swap and
restart), JSON command dispatch, host info collection beyond
hostname/OS/arch, KVM, terminal, file transfer. Running vervain-agent
against a real Vervain server is not supported until the paired
vervain-server repo lands its control-channel implementation.
Build and test
cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- --deny warnings
cargo fmt --all --check
The toolchain is pinned via rust-toolchain.toml to match what CI will
eventually use. Override with RUSTUP_TOOLCHAIN=... only when intentionally
testing a different version.
Local development (mTLS enrollment)
just dev brings a hot-reloading agent online against the local
vervain-server dev stack. The agent enrolls over the server's mTLS port with
a server-issued client certificate (VA-67); there is no plain-WebSocket path.
just dev-enroll-bundle # mint a one-time enrollment bundle into run/ (server must be up)
just dev # build + run the container agent; auto-mints on first run
The agent enrolls on the first run and reuses the identity stored in
run/vervain-agent.db afterward. Full flow, prerequisites, and the native
just dev-host path: docs/dev-enrollment.md.
Configuration
The agent reads its runtime configuration from an encrypted SQLite database,
not a plaintext file. Full design and threat model:
docs/local-settings-store.md.
The agent keeps two SQLCipher (encrypted SQLite) databases, both unlocked by
the VERVAIN_DB_KEY environment variable (set in the service unit at install):
config.dbunder the config directory (chosen with--settings <dir>, default~/.config/vervain-agent): the flat settings table.vervain-agent.dbunder the data directory (chosen with--data-dir <dir>, default~/.local/share/vervain-agent): the agent identity, the enrolled mTLS cert/key + server CA, and runtime keys.
The config database carries the same keys the .vagent did, e.g. the required
ServerURL, ServerID, GroupID, plus optional tunables (enableMdns,
disableUpdate, backoffInitialMs, idleTimeoutSecs, ...).
Bootstrap or reconfigure from a plaintext .vagent-format seed file:
vervain-agent config import --from server-issued.vagent --settings /etc/vervain-agent
Inspect and edit (credential-bearing values are masked unless --reveal):
vervain-agent config show --settings /etc/vervain-agent
vervain-agent config get ServerURL --settings /etc/vervain-agent
vervain-agent config set enableMdns 0 --settings /etc/vervain-agent
Bootstrap is by import, not migration: on first run against a config
directory that holds a vervain-agent.vagent seed but an empty config
database, the agent imports the seed into the encrypted config.db. There is
no migration from the old sled / .vss stores. A missing or wrong
VERVAIN_DB_KEY makes the databases unreadable; greenfield, the agent then
re-enrolls. --check is read-only and never creates a database.
vervain-agent install --from <seed> imports the seed into the encrypted
config.db, generates a fresh VERVAIN_DB_KEY, and writes a systemd unit that
sets it via Environment= and runs with --settings. The install target is
chosen with --install-type {user|system}; it defaults to system.
--install-type system is a system-wide install: the binary lands at
/usr/local/sbin/vervain-agent, the config database under /etc/vervain-agent/,
and /etc/systemd/system/vervain-agent.service running as root that
survives reboots. Running as root (with no ProtectHome/ProtectSystem
sandbox and no dedicated user) lets the agent attach to the active user's
graphical session, which is what the desktop tab (KVM, over Wayland or X11)
needs to drive the already-logged-in desktop regardless of who is logged in. This needs root,
so run it under sudo; invoked without root it exits non-zero and tells you to
either re-run with sudo or pass --install-type user, rather than silently
downgrading to a session unit. A locked-down least-privilege unit is out of
scope: script your own unit file if you need one.
--install-type user is a per-user install (no root) at
~/.config/systemd/user/vervain-agent.service. The user unit runs inside the
calling user's session and drives only that user's desktop. Reach for it when
you do not want a root service; prefer the system default everywhere else.
vervain-agent uninstall mirrors the same default (--install-type system
unless you pass --install-type user).
Server URL and cert pinning under TLS offload (VA-66)
ServerURL (and the server-issued GroupServer) target the API server on the
/api/v1/agent control path, e.g. wss://api.vervain.a8n.systems/api/v1/agent
(dev connects over mTLS to dev-vervain-server-app-dev:15443; see
docs/dev-enrollment.md). Agents connect straight
to the API host, never the browser frontend (parent epic VS-54).
The handshake has two independent pins, so it stays correct when a reverse
proxy terminates TLS in front of the server (VERVAIN_TLS_MODE=offload):
- ServerID (signing identity). The agent pins the SHA-384 of the server's
signing SPKI from the
ServerIDkey in its store and verifies the server's RSA-PKCS1v1.5 signature against it. This is the server's own identity, not the TLS certificate, so it is unaffected by who terminates TLS. - TLS leaf hash. The agent's TLS verifier accepts the presented leaf but
captures its SHA-384; the server signs over
SHA384(leaf_hash || agent_nonce || server_nonce), binding the proven identity to the exact certificate the agent saw. Under offload the agent sees the proxy's certificate, so the server must sign that same hash. It does: configureVERVAIN_CERT_URLon the server to point at the terminating proxy (VS-27) and the server fetches the proxy cert at startup and advertises its hash, so the agent's observed hash matches and the pin passes. WithoutVERVAIN_CERT_URLthe server would sign its own web-cert hash while the agent sees the proxy's, and the handshake fails withServerCertMismatch- the expected, fail-closed behavior. The cachedserver_tls_cert_hashis only a reconnect fast-path and does not weaken either pin.
Runtime dependencies (optional, per tunnel mode)
The agent itself ships as a single static binary. A few tunnel modes shell out to host-installed helpers; install them on the agent host if you intend to use the mode:
- VNC bridge (
USAGE_VNC = 6, seecrates/vervain-agent/src/host/vnc.rs):x11vncon X11 sessions,wayvncon wlroots / KDE Wayland. The helper is selected at tunnel-open time from$WAYLAND_DISPLAY/$DISPLAYand is bound to127.0.0.1only. - RDP bridge (
USAGE_RDP = 7, seecrates/vervain-agent/src/host/rdp.rs):xrdp+xrdp-sesmanrunning as a host service on127.0.0.1:3389. The agent does not spawn or supervise the daemon; the host's init system (systemd / etc.) owns its lifetime. Install via your distribution's package manager (apt install xrdp,dnf install xrdp). - MCP server (
USAGE_MCP = 8, seecrates/vervain-agent/src/host/mcp.rs,crates/vervain-agent-mcp/): no host-side dependency. The agent runs an in-process MCP (Model Context Protocol) server speaking JSON-RPC 2.0 over the tunnel; one Text frame per JSON-RPC message. v1 (VA-26) exposes 17 read-only host inventory tools (osinfo,process_list,file_read,log_tailwith streaming progress, etc.). v2 (VA-27, default-onmutating-toolsfeature) adds 9 mutating tools (exec,file_write,file_delete, servicesstart/stop/restart,power_reboot,packages_install,process_kill) gated through the agent's consent provider (zenity/kdialogdialog by default, with per-tool argument scoping forfile_write). Strictly read-only deployment:cargo build --no-default-features --features vervain-core. - Native RFB / VNC server (
USAGE_VNC_NATIVE = 9, seecrates/vervain-agent/src/host/vnc_native/): no helper binary. The agent speaks RFB 003.008 in-process, reusing thevervain-agent-kvmcapture pipeline (X11 + Wayland-via-grim) and XTEST / uinput input. VeNCrypt-TLS is offered with a fresh self-signed cert per session; rustls +rcgenare pulled from existing workspace deps. Tested against TigerVNC; viewers that cannot accept a self-signed cert prompt should fall back to the plain "None" security path that this server also advertises.
Layout
.
├── Cargo.toml workspace manifest
├── rust-toolchain.toml pinned toolchain
├── ROADMAP.md phased migration plan
├── crates/
│ ├── vervain-agent/ binary entry point (CLI, runtime bootstrap)
│ ├── vervain-agent-config/ encrypted settings store + required-key validation
│ ├── vervain-agent-kvm/ KVM (display, input, capture) primitives
│ ├── vervain-agent-mcp/ MCP (Model Context Protocol) server adapter
│ └── vervain-agent-protocol/ binary command codes and handshake structs
Today the vervain-agent crate has both a binary (src/main.rs, the
CLI) and a library (src/lib.rs) that exposes hostinfo, identity,
net, settings (config/data DB resolution and load), and
store (the encrypted SQLite key-value data store) modules. Integration
tests in crates/vervain-agent/tests/ drive that library directly.
Future crates (planned, not yet split out): vervain-agent-store
(replacement for the data store), vervain-agent-net (TLS plus
WebSocket transport plus secondary-cert handshake),
vervain-agent-identity, vervain-agent-hostinfo,
vervain-agent-update, vervain-agent-core (post-auth dispatcher).
They live as modules inside vervain-agent for now and will be
promoted to their own crates once each gains enough surface area to
justify the boundary.
Design decisions
- No embedded JavaScript runtime. Every behavior is implemented as
a Rust module. The agent will not accept
CoreModuleorCompressedCoreModulepayloads from the server and will not advertiseCapabilities::JAVASCRIPTinAuthInfo. - Tokio (current-thread by default). Multi-threaded workloads (KVM
tile encoding, large file transfers) move to
spawn_blockingor dedicated rayon pools where they belong. - rustls plus webpki, not OpenSSL. The secondary cert pinning in the handshake is implemented above the TLS layer, so this doesn't affect protocol behavior.
- Cert-pin under TLS offload. The pin is on the application-layer
ServerID(SHA-384 of the server's agent-root cert SPKI), verified in the secondary handshake via the server's RSA-PKCS1v1.5 signature, not on the TLS transport cert. The TLS verifier (CapturingNoopVerifierincrates/vervain-agent/src/net.rs) accepts any transport cert and only captures the leaf for inspection. Consequently, when production terminates TLS at Traefik forapi.vervain.a8n.systems(the server advertises a matching transport hash viaVERVAIN_CERT_URL, see VS-27), the offload is transparent to the agent: it accepts the Traefik proxy cert at the transport layer, then the secondary handshake proves the backend server'sServerIDend to end. The pin passes regardless of where TLS terminates because the pinned identity is the backendServerID, never the proxy's transport cert. tokio-tungstenitefor WebSocket.- Encrypted SQLite (SQLCipher) for both config and data (VA-68). Two
whole-database-encrypted SQLite files, unlocked by the
VERVAIN_DB_KEYenv var: the datavervain-agent.dbholds the agent identity (RSA key + cert), node id, cached server cert hash, and the enrolled mTLS cert/key + server CA; the configconfig.dbholds the flat settings table managed byvervain-agent-config(see Configuration anddocs/local-settings-store.md). Replaces the formersleddata store and the whole-file.vsssettings blob. - Conditional compilation, not separate per-arch trees.
cfg(target_os)andcfg(target_arch)gate platform code. Cross-compilation iscargo build --target=....
What is intentionally out of scope (initially)
- Windows service hosting.
- KVM Windows backend.
- WebRTC. The agent will start as WebSocket-only; WebRTC data channels
come back via
webrtc-rsin a later phase. - Intel AMT/LMS.
These will come back, but pretending they're "v1" features makes v1 infinite. ROADMAP.md tracks when each rejoins.