Why the monsys agent is written in Rust — and what that means for security
~5 MB statically linked, zero glibc dependencies, runs on kernel 2.6+. Memory safety without a garbage collector, scope-locked sudoers and three-layer auto-update verification.
The monsys agent is a ~5 MB statically linked Rust binary. That's a deliberate choice, not a coincidence. This article explains why, what the trade-offs are, and what "statically linked" concretely means for deployment on a production server.
The installation problem of monitoring agents
Most monitoring agents have a dependency problem. A Python agent needs Python — the right version, the right packages, the right virtual environment. A Node agent needs Node. A Java agent needs a JVM.
On a typical Belgian SMB server running Ubuntu 18.04 (EOL but still in production), RHEL 8, or Alpine in a Docker container, that's a problem. The server doesn't have the right runtime. You install the runtime. The runtime has security updates. You've now added a new attack surface in order to monitor your attack surface.
The monsys agent solves this differently: one binary, statically linked against musl libc, zero glibc dependencies. The binary contains everything it needs.
What "statically linked" means
A dynamically linked binary has runtime dependencies on shared libraries:
ldd /usr/bin/curl
libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
If libssl.so.3 is the wrong version, the binary crashes. If libc.so.6 is missing (Alpine uses musl, not glibc), the binary doesn't work.
A statically linked binary contains those libraries internally:
ldd /usr/local/bin/monsys-agent
not a dynamic executable
The binary runs on any Linux system with a kernel ≥ 2.6, regardless of installed libraries. That covers:
- Ubuntu 18.04, 20.04, 22.04, 24.04
- Debian 11, 12
- RHEL/AlmaLinux/Rocky 8, 9
- Fedora (any recent version)
- Alpine 3.10+
- SUSE/SLES
- Oracle Linux
- Amazon Linux 2, 2023
- Any Docker container with a minimal Linux kernel
Why Rust for a security agent
Memory safety without a garbage collector
The most common vulnerability classes in C/C++ are buffer overflows, use-after-free, and race conditions. Rust makes these classes impossible at compile time, not via runtime checks. There is no garbage collector that pauses, no runtime that uses extra memory.
For an agent that runs on production hosts with low overhead requirements, that's the right trade-off: the safety of memory-safe languages (Go, Python, Java) without the overhead of garbage collection or a runtime.
Small binary footprint
Rust compiles to efficient machine code without dragging a large runtime along. The agent (~5 MB) fits comfortably on embedded systems and containers with minimal disk space.
Fearless concurrency
The agent does a lot in parallel: polling metrics, building inventory, maintaining honeypot watches, sending HTTP payloads. Rust's ownership system makes data races impossible at compile time — a class of bugs that's notoriously hard to debug in production code.
The agent structure
The agent is split into four main tasks that run in parallel via Tokio (the async Rust runtime):
monsys-agent
├── metrics_collector ← CPU/RAM/disk/net every 15s via procfs
├── inventory_collector ← packages, services, ports, users, SSH keys every 60s
├── security_monitor ├── honeypot_watcher (inotify)
│ ├── process_dna_scanner (sha256 per top-process)
│ └── integrity_checker (the agent binary itself)
└── payload_sender ← HTTP/2 POST to the hub with Ed25519 signing
metrics_collector: reads /proc/stat, /proc/meminfo, /proc/diskstats, /proc/net/dev. No external tools, no shell calls. Direct kernel interface via procfs.
inventory_collector: invokes dpkg -l, rpm -qa, systemctl list-units, ss -tlnp, getent passwd, find /root/.ssh -name "authorized_keys". SSH keys are only stored as SHA256 fingerprints, never the key content.
honeypot_watcher: registers inotify watches on every canary path. On Linux this uses inotify_add_watch via the kernel syscall — no polling, no CPU usage when there are no events.
payload_sender: serialises events to canonical JSON, signs with the Ed25519 private key, sends via HTTP/2 POST to api.monsys.ai. On network failure: exponential backoff, local queue (in-memory, max 1000 events), no disk writes (avoids disk-full scenarios).
Scope-locked sudoers: minimal privileges
The installer writes the following sudoers rule:
monsys ALL=(ALL) NOPASSWD: /usr/local/sbin/monsys-emergency-action
monsys ALL=(ALL) NOPASSWD: /usr/local/sbin/monsys-self-update
That's all. The agent runs as a dedicated monsys user, not as root. Only two specific wrappers have sudo access:
monsys-emergency-action: executes Emergency Action Tokens (IsolateNetwork, KillProcess, etc.)monsys-self-update: performs the auto-update
Any other action that requires root is not possible from the agent. An attacker who compromises the agent has no sudo escalation to root via the agent.
Agent integrity monitoring: the agent watches itself
On first start, the agent computes its own SHA256 and sends it to the hub:
{
"agent_binary_hash": "sha256:a3f2c1...",
"agent_version": "0.14.2",
"observed_at": "2026-05-25T09:00:00Z"
}
The hub pins this hash. On every heartbeat, the agent recomputes its own hash. If an attacker replaces the agent binary, the new binary detects that its hash doesn't match what the hub expects — and the hub gets an integrity alert.
Yes, this has a bootstrapping problem: if an attacker replaces the binary with a version that sends fake integrity hashes, detection is harder. That's a deliberately accepted limitation — the signing chain (Ed25519) makes producing valid signed payloads hard without the private key, but not impossible if the whole agent is compromised.
Auto-update: how it stays safe
Auto-updates are opt-in. When enabled:
- Hub checks the release manifest at
releases.monsys.aievery 6 hours - If there's a new version, the hub (not the agent) downloads the binary
- Hub verifies SHA256 and the Release signature (Ed25519, signed by monsys)
- Hub sends a
self_updateEAT to the agent - Agent verifies the EAT signature (Ed25519 from the hub)
- The
monsys-self-updatewrapper downloads the binary, re-verifies SHA256, performs an atomic swap (mv), restarts the systemd service
Three layers of verification: the hub verifies monsys's release signature, the agent verifies the hub's EAT signature, and the wrapper verifies the binary's SHA256. A supply-chain attack on the release server has to bypass all three layers.
After the update, the new binary hash is signed and sent to the hub. Process DNA registers the new hash as legitimate (manifest-aware rebaseline), so no false-positive alert fires.
Windows: same approach, different syscalls
The Windows version (monsys-agent.exe) uses the same Rust codebase with platform-specific implementations:
metrics_collector:GlobalMemoryStatusEx,GetDiskFreeSpaceEx,GetSystemTimesvia Windows APIinventory_collector:wmic product get name,versionfor packages,sc query type= allfor serviceshoneypot_watcher:ReadDirectoryChangesWinstead of inotify- Service install:
StartServiceCtrlDispatcherinstead of systemd
The binary runs as a Windows Service, not as an interactive application. The emergency console on Windows uses PowerShell via ConPTY in a JEA endpoint (Just Enough Administration) with ~60 whitelisted IR cmdlets — no Invoke-Expression, no Remove-Item, no privilege escalation.
Why no eBPF
eBPF is popular for security monitoring: you can observe kernel events without writing a kernel module. But eBPF requires kernel ≥ 4.9 for basic functionality and ≥ 5.8 for the full feature set.
On Ubuntu 18.04 (kernel 4.15) or RHEL 8 (kernel 4.18) the eBPF feature set is limited. On Alpine in Docker, eBPF depends on the host kernel.
The monsys agent picks procfs and inotify: stable, universally available kernel interfaces that work on kernel 2.6+. Less powerful than eBPF, but universally deployable. For the use cases monsys covers (metrics, inventory, honeypots, process DNA), procfs + inotify is enough.
Summary: the agent's design principles
| Principle | Implementation |
|---|---|
| No runtime dependencies | Statically linked, musl libc |
| Minimal privileges | Dedicated user, scope-locked sudoers |
| Memory safety | Rust (compile-time guarantees) |
| Low overhead | ~5 MB binary, < 1% CPU, < 50 MB RAM |
| Universally deployable | kernel ≥ 2.6, every Linux distro + Windows x64 |
| Self-watching | Own binary hash signed and verified |
| Safe updates | Three-layer verification, atomic swap |
| Audit trail | Ed25519-signed payloads, EAT-logged actions |
A monitoring agent that is itself a security risk works against you. The design choices of the monsys agent aim at one goal: add as little attack surface as possible to the hosts it watches.
The agent architecture is documented at docs.monsys.ai/en/get-started/architecture. Install in 30 seconds: monsys.ai/en/signup.