Technical deep-dive · 2026-05-25

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:

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:

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:

  1. Hub checks the release manifest at releases.monsys.ai every 6 hours
  2. If there's a new version, the hub (not the agent) downloads the binary
  3. Hub verifies SHA256 and the Release signature (Ed25519, signed by monsys)
  4. Hub sends a self_update EAT to the agent
  5. Agent verifies the EAT signature (Ed25519 from the hub)
  6. The monsys-self-update wrapper 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:

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

PrincipleImplementation
No runtime dependenciesStatically linked, musl libc
Minimal privilegesDedicated user, scope-locked sudoers
Memory safetyRust (compile-time guarantees)
Low overhead~5 MB binary, < 1% CPU, < 50 MB RAM
Universally deployablekernel ≥ 2.6, every Linux distro + Windows x64
Self-watchingOwn binary hash signed and verified
Safe updatesThree-layer verification, atomic swap
Audit trailEd25519-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.

Back to blog