DEV Community

Khalid Khan
Khalid Khan

Posted on

Building SystemGuard: Why I'm Writing an Open-Source CrowdStrike Alternative in Rust

I manage infrastructure for clients across Pakistan. Last month, a freelancer friend got a $1,400 bill from CrowdStrike for 40 Linux servers. That's more than his monthly revenue.

Enterprise EDR is broken for the rest of the world. So I'm building an alternative.

SystemGuard is a lightweight, self-hosted HIDS I'm open-sourcing. It's not another wrapper around OSSEC — it's built from the kernel up with eBPF and Rust.

GitHub: https://github.com/systemguard-io/systemguard

The Problem Nobody Talks About

Western security tools assume:

  • You can afford $35/host/month
  • You want to send all your telemetry to US clouds
  • You have a SOC team to tune 10,000 alerts

In Karachi, Lahore, and Islamabad, we run 5-100 servers on tight margins. We need:

  1. Real-time detection (<100ms)
  2. Self-hosted (data sovereignty)
  3. <2% CPU overhead
  4. Free

OSSEC hasn't had a meaningful commit since 2019. Wazuh is great but requires Elasticsearch cluster (overkill for 10 servers).

Why Rust, Not Go?

I prototyped in Go first. It worked — until I hit 15k syscalls/sec during a stress test. GC pause: 8ms. In that window, we lost 120 events. For a security tool, that's unacceptable.

I rewrote the agent in Rust with aya-rs. Same workload: 1.4% CPU, zero allocations in hot path, 5MB static binary.

The decision is documented here: ADR-001

Key trade-off: steeper learning curve, but memory safety is non-negotiable when your agent runs as root.

eBPF vs auditd: The Numbers

I benchmarked both on a t3.medium:

Method CPU Events Lost
auditd 22% 4.2%
eBPF 1.6% 0%

auditd copies every event to userspace. eBPF filters in-kernel. For monitoring openat() and execve(), eBPF is 14x more efficient.

Full analysis: ADR-002

Here's the actual probe (simplified):


c
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(void *ctx) {
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, 16);
    // Filter in kernel: ignore /proc, /sys
    bpf_ringbuf_submit(e, 0);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)