Last Updated: July 2026 Version: 0.1.0
This document describes the security practices and threat model for vecnode: the
vn Rust CLI/TUI, its per-OS scripts, and the Dockerized web apps launched from
the TUI's Open menu. It is written to match the code as it exists in this
repository.
- Rust CLI Security Practices
- Script Execution Model
- Dockerized Web Apps
- MCP Host
- Filesystem Access
- Network Access
- Additional Resources
The cross-platform command/TUI code (main.rs, config.rs, commands/, tui/) is
100% safe Rust. The Windows-only tray module (tray.rs, cfg(target_os = "windows")) contains a small number of reviewed unsafe blocks around windows-sys
FFI calls (ShellExecuteW, LoadIconW, MessageBoxW, GetConsoleWindow) needed to
drive the system tray, show the UAC-elevated terminal, and hide the console window —
there is no safe-Rust equivalent for these Win32 calls. It never runs on Linux and is
excluded from the rest of the crate.
Verification:
grep -rl "unsafe" cli/crates/vn/src/
# Returns: cli/crates/vn/src/tray.rs (Windows-only FFI, see above)Commands the TUI spawns in the background (vn app open, vn run ..., vn ai pull,
etc.) run via the command-group crate
instead of std::process::Command::spawn() directly, so each spawned command owns its
own process group (Unix) / job object (Windows). Quitting the TUI or killing a running
job also kills anything that command itself spawned (e.g. a docker build or
yt-dlp invocation) instead of leaving it orphaned in the background.
cargo auditscansCargo.lockagainst the RustSec Advisory Database.cargo deny(see cli/deny.toml) warns on unmaintained crates and non-permissive licenses, and bansopensslin favor of rustls.- Both now run in CI on every push/PR to
main(.github/workflows/ci.yml), alongsidecargo fmt --check,cargo clippy -D warnings, andcargo build --lockedon Ubuntu and Windows.
Cargo.lockis committed to version control.- Builds use
cargo build --lockedfor reproducible versions.
Current direct dependencies (see cli/Cargo.toml):
| Crate | Version | Purpose |
|---|---|---|
anyhow |
1.0 | Error handling |
clap |
4.5 | CLI argument parsing |
command-group |
5.0 | Process-group/job-object kill for spawned commands (see Process Lifecycle) |
crossterm / ratatui |
0.28 | Terminal UI |
dirs |
5.0 | Config/data paths |
ollama-rs |
0.3 | Ollama client for vn ai |
reqwest |
0.12 (rustls, no default features) | HTTP client |
rmcp |
2 | Official MCP SDK - vn mcp serve and the TUI's embedded server (see MCP Host) |
hyper-util / schemars |
0.1 / 1 | HTTP serving / JSON-schema generation for rmcp |
serde / serde_json / toml |
1.0 / 1.0 / 0.8 | Config + session serialization |
sysinfo |
0.31 | vn sys info |
tokio |
1.40 | Async runtime |
chrono |
0.4 | Timestamps |
futures-util |
0.3 | Streaming |
tray-item / windows-sys |
0.10 / 0.48 | Windows-only tray agent |
vn does NOT require root or admin privileges for normal operation.
The Windows launcher (run_cli.bat) runs unelevated; the tray offers a separate
"Open Admin TUI Terminal" (UAC prompt) only for tasks that need it.
| Command | Privilege | Notes |
|---|---|---|
vn sys info |
None | Read-only system information |
vn docker ... / Open-menu apps |
Docker daemon access | docker group (Linux) or Docker Desktop (Windows) |
vn git sync|status |
None | Operates on your own repositories |
vn net scan |
None | RustScan TCP connect scan (see Network Access) |
vn run <script> |
Depends on script | Some scripts install packages (may prompt for sudo/UAC) |
vn run <name> does not execute arbitrary paths or shell strings. Script
names map through a static allow-list in
cli/crates/vn/src/commands/run.rs to fixed
relative paths under scripts/ in the detected repo root (validated to contain
.git/ + scripts/). .bat files run via cmd /C; .sh files run via
wsl bash on Windows. UNC paths are rejected for WSL translation. User input is
never interpolated into a command line; vn docker up|down <name> validates the
service name against Docker's allowed character set.
The TUI Open menu launches local web apps in Docker. Their shared posture:
- Loopback-only exposure: every app publishes its ports as
-p 127.0.0.1:<port>:<port>, so nothing is reachable from the LAN. If you deliberately expose one, put authentication in front of it first. - Locally built apps run as a non-root user (uid 10001 baked into the
image; on Linux
vnpasses--user $(id -u):$(id -g)so files written to bind mounts stay owned by you), with--cap-drop ALL,--security-opt no-new-privileges, and--pids-limit 512. - Pulled vendor images (SilverBullet, Stirling-PDF) are not cap-dropped:
their entrypoints legitimately need privilege transitions (Stirling-PDF
setprivs from root to its app user, which requires CAP_SETUID/SETGID and breaks under--cap-drop ALL). They run as upstream intends, protected by the loopback-only binding. - All of this is enforced by one Rust code path
(cli/crates/vn/src/commands/apps.rs, the
vn app open|stopengine) instead of per-OS scripts, so the posture cannot drift between platforms.
Per-app threat model:
| App | Port | Mounts | Notes |
|---|---|---|---|
| library-portal | 8090 | library/ read-write |
Unauthenticated UI that can rename/move/delete PDFs - safe only because it is loopback-bound. State kept in library/.portal/. |
| doc-processor | 8085/8086 | host Desktop read-write | pandoc/tectonic markdown-to-PDF + pypdf join; output confined to the Desktop mount. |
| media-downloader | 8095 | host Desktop read-write | Fetches untrusted URLs (yt-dlp): http/https only, an initial fast-fail check rejects hosts resolving to loopback/private/link-local ranges, and every connection yt-dlp itself makes is routed through an in-container egress-guard proxy (start_egress_proxy in docker/media-downloader/app.py) that re-resolves and re-validates the target at actual connect time — this catches redirects to a different (private) host and DNS-rebinding between the initial check and yt-dlp's own lookup, not just the initial URL. Also: --ignore-config --restrict-filenames --no-exec --max-filesize, sanitized traversal-checked collision-safe output names. |
| SilverBullet / Stirling-PDF / docs | 3000/8080 | space/none | Pulled published images (docs is local). SilverBullet uses a default SB_USER=user:password credential and Stirling-PDF's login page is disabled (SECURITY_ENABLELOGIN=false) - both acceptable only while loopback-bound; change them before any wider exposure. |
Image supply chain: base images are pinned tags (debian:12-slim,
python:3.12-slim); Python deps are version-pinned except yt-dlp, which is
deliberately installed latest at build time (stale extractors break against
current sites). tectonic is a pinned release binary downloaded over HTTPS.
vecnode exposes its own host-control functions (currently: list/open/stop the Dockerized
apps) as an MCP server via the official rmcp SDK - see
AGENTS.md for the architecture. Security-relevant properties:
- Loopback-only by default. The TUI's embedded server and
vn mcp serve --httpboth bind127.0.0.1only, matching every other vecnode-managed service in this document.vn mcp serve(stdio, no--http) never opens a network port at all - it's spawned as a subprocess by the MCP client (Claude Desktop/Code). - v1 exposes one toolset (Apps) only -
list_apps,open_app,stop_app. There is no arbitrary command execution, filesystem, or process-control tool exposed to MCP clients or the local model in this version. stop_apprequires human approval. Any MCP client - external, or vecnode's own Ollama chat calling the identical in-process tool - must have that call approved in the vecnode TUI (ApprovalGate,InputPurpose::ApproveMcp) before it runs.list_apps/open_appare not gated, matching the TUI's own unconfirmed "vn app open" menu items.- Fail-closed when headless. A standalone
vn mcp serve(no TUI attached) auto-denies everystop_apprequest rather than hanging or silently allowing it - there is no console free to prompt on, since stdio is the protocol channel itself. open_appdefaults to not opening a browser when called via MCP (no_open: trueunless the caller explicitly passesfalse) - an LLM popping a browser window is a surprising side effect the human didn't ask for.
| Path (Unix) | Path (Windows) | Permission | Purpose |
|---|---|---|---|
~/.config/vn/config.toml |
%APPDATA%\vn\config.toml |
0o600 on Unix (Windows inherits profile ACLs) |
CLI configuration |
~/.local/share/vn/sessions/ |
%LOCALAPPDATA%\vn\sessions\ |
0o700 on Unix |
vn ai chat session history (plaintext JSON) |
<repo>/logs/ |
same | user | TUI session logs (gitignored) |
<repo>/scripts/ |
same | read/execute | Allow-listed task scripts |
<repo>/library/ |
same | read-write (library-portal only) | PDF library (gitignored, never enters images) |
Session history and TUI logs are plaintext. Treat them as sensitive; they are gitignored - do not commit or share them.
| Feature | Default | Network Access |
|---|---|---|
vn ai |
On demand | Ollama at http://127.0.0.1:11434 (localhost only; other hosts require explicit config) |
vn net scan |
On demand | Actively scans the local /24 subnet (common ports) or a given target with RustScan - only scan networks you own or have permission to test |
vn git sync |
On demand | Git remotes over HTTPS/SSH |
vn docker / Open apps |
On demand | Docker daemon; image pulls from Docker Hub/GHCR; media-downloader fetches user-supplied URLs (guarded, see above) |
TUI (always) / vn mcp serve --http |
On demand | MCP server on loopback 127.0.0.1:7332 (see MCP Host) - never reachable off-host |
vn mcp serve (stdio) |
On demand | None - no network port; runs as a subprocess of an MCP client over stdin/stdout |
vn sys |
- | None |