Autonomous endpoint detection & response (EDR) for Linux workstations, written in Rust. No server, no cloud dependency, no agent-to-collector traffic — everything runs locally as a single hardened systemd service, with an optional desktop GUI.
Warden watches for the handful of things that actually matter on a single
workstation — ransomware, persistence mechanisms, privilege escalation,
malicious file content, fileless execution and outbound network activity
from suspicious binaries — and either logs what it sees (monitor mode)
or kills/quarantines it immediately (enforce mode).
Every detection module has been pushed through real, adversarial testing rather than just unit tests: two dedicated internal red-team audits, an external community audit, and a full SAST pass each found and fixed genuine bypasses — see Hardened through real testing below, and PROGRESS.md for the full blow-by-blow history, every design decision and the reasoning behind it.
- Ransomware (
warden-ransomware): fanotify-based monitoring of the user's data directories. Flags a burst of high-entropy file rewrites (tracked per-process, per-directory, and globally, so fork-per-file and multi-directory spreading can't slip under any single counter), container-format signature forgery, and touches to seeded, per-machine randomized honeypot files (randomized folder name and filename, drawn from independent word pools) planted among real user directories. Entropy sampling reads three spread-out zones of each file (start, middle, end) and keeps the maximum, so prefixing a file with a plaintext header can't hide an encrypted payload. - Persistence (
warden-persistence): inotify-based monitoring of cron, sudoers.d, systemd units, XDG autostart entries, profile.d, and shell rc files for new or modified entries, with heuristics for obviously suspicious content (reverse-shell one-liners, curl-pipe-shell, etc.) and diffing against a baseline to catch subtle appended lines. Existing dotfiles are always report-only (never auto-reverted — a botched revert of/etc/sudoerscould lock every admin out); a brand new file in a unit directory (cron.d, sudoers.d, autostart) is safe to quarantine outright. - Privilege escalation (
warden-privesc): polls for new setuid/setgid binaries and unexpected files dropped in system binary directories, distinguishing an existing system binary gaining the bit (GTFOBins-style — the bit is stripped, the binary kept) from a brand new setuid file (quarantined). - Malware signatures (
warden-yara): YARA-based scanning, both live (fanotify, on file close) and on-demand (StartScanover the control socket, restricted to the caller's own home directory and/tmp, with a symlink-root check and a 100 MB file-size cap against abuse). - Fileless exec & network (
ebpf-probe/, optional): eBPF tracepoints onsched_process_execandinet_sock_set_statefor exec/network visibility beyond what filesystem watching alone can see — a process launched from/tmpor~/Downloadsgets killed and quarantined on execution; the same binary later opening an outbound connection is a second, independent line of defense if the first one was somehow missed. Needs a nightly Rust toolchain +bpf-linker; the rest of Warden works fully without it.
Every detection module runs its own independent watch loop inside the
same warden daemon process (except the two eBPF modules, which are
separate binaries/services). A single confirmed detection can kill the
offending process (pidfd-based, immune to PID-reuse races, with a
best-effort raw-kill fallback) and quarantine the affected files.
flowchart TB
subgraph kernel["Linux kernel"]
fanotify["fanotify\n(FAN_CLOSE_WRITE)"]
inotify["inotify"]
poll["polling (5s)"]
ebpf["eBPF tracepoints\nsched_process_exec /\ninet_sock_set_state"]
end
fanotify --> ransom["warden-ransomware"]
fanotify --> yara["warden-yara"]
inotify --> persist["warden-persistence"]
poll --> privesc["warden-privesc"]
ebpf --> exec["warden-exec"]
ebpf --> net["warden-network"]
ransom --> core["warden-core (daemon, root)\nsupervises modules via a JoinSet"]
persist --> core
privesc --> core
yara --> core
core -->|"kill (pidfd) +\nquarantine"| response["response"]
core -->|DetectionEvent| history["history.jsonl"]
core -->|"JSON over\nstdin/stdout"| helper["warden-notify-helper\n(dropped to user uid)"]
helper -->|D-Bus| desktop["actionable desktop\nnotification"]
core <-->|"Unix socket, 0600"| socket["control.sock"]
socket <--> gui["warden-gui (GTK4)\nuser session"]
gui -->|pkexec| privileged["privileged actions:\nexceptions, restore,\nmode switch"]
privileged --> core
exec -.->|"kill + quarantine\n(own process)"| response
net -.->|"kill + quarantine\n(own process)"| response
warden(inwarden-core/) — the main daemon. Runs as root (fanotify filesystem-wide marks, killing/quarantining arbitrary user processes, and stripping setuid bits all genuinely need it), but with a narrowedCapabilityBoundingSetand full systemd sandboxing (ProtectSystem=strict,NoNewPrivileges,RestrictSUIDSGID,RestrictAddressFamilies=AF_UNIX,ProtectProc=invisible,WatchdogSec=30, ...) — see systemd/warden.service for the exact directives and the reasoning behind each one (including which hardening directive was tried and reverted because it broke the YARA module's JIT — see PROGRESS.md).warden-common— shared building blocks: quarantine (with cross-processflock-protected manifest, setuid-stripping on copy, path-length-safe naming), detection history, the SHA-256-anchored exceptions list, XDG directory resolution (hardened against a malicioususer-dirs.dirspointing at/), package-manager detection (UID-checked, directory-checked — to avoid flagging a legitimateapt/dnfupgrade without becoming a bypass itself), andpidfd-based process termination.warden-notify-helper— a tiny helper the daemon spawns with privileges dropped to the logged-in user, since a root process can't reach that user's D-Bus session bus (dbus-daemonrefuses by design — see PROGRESS.md for the full investigation). Talks to the daemon over stdin/stdout as newline-delimited JSON, withNOTIFY_SOCKETstripped from its environment so it can never forge a systemd watchdog ping.warden-gui(GTK4 + libadwaita) — the desktop dashboard: live module status, detection history, quarantine management, on-demand scans, exception management, and mode switching — all consequential actions (--add-exception,--quarantine-file,--set-mode, ...) are gated behind realpkexecauthentication, never reachable through the plain control socket.QuarantineFile/RestoreQuarantinewere deliberately excluded from the socket protocol itself after a review found they'd let any process at the same uid disable protection without real authentication.- Control socket (
/run/warden/control.sock) — a Unix domain socket, owner-only (0600,umaskset aroundbind()so it's never briefly wider), capped at 64 KiB per line (a client can't OOM the daemon by streaming without a newline), for read-only GUI queries (status, history, on-demand scan) that don't need a privileged prompt.
Warden's security model isn't a claim — it's the output of repeated, adversarial testing against the actual running daemon, with every finding reproduced, fixed, and re-validated live rather than just patched and assumed fixed. The full history is in PROGRESS.md; a few representative examples:
- Package-manager spoofing:
cp /bin/sleep /tmp/apt && /tmp/apt 300 &used to be enough to make Warden think a real package manager was running and suspend auto-quarantine — found and fixed by checking that the binary's directory, not just its name, canonicalizes to a known system path. - Fork-per-file ransomware across multiple directories: spreading 48 encrypted files across 6 directories at 8 files each, each under its own short-lived PID, slipped under both the per-PID and per-directory thresholds — fixed with a third, global counter.
- Unprivileged auto-quarantine bypass:
is_active()never checked the calling process's UID, so any local, non-root user could keep it permanently "true" and suspend Enforce-mode protection indefinitely — caught by an external community audit, fixed with a UID check. - A quarantine bypass via an overlong filename: any detected file
under a long enough path made both
renameand itscopyfallback fail withENAMETOOLONG, silently — a 100%-reliable Enforce bypass for modules that only quarantine (no process to kill). Found in a dedicated SAST pass, fixed with safe truncation plus a collision-proof hash. - TOCTOU on the YARA and ransomware fanotify monitors: both re-opened
a file by path after the fact instead of using the original event's
file descriptor, leaving a window to swap the content between the
close event and the scan — fixed by reading through a
dup()'d fd.
Every fix above shipped with a reproducing test and was re-validated against the real daemon on real VMs (Debian, Ubuntu) and containerized distros (Fedora, Arch, openSUSE) before being considered closed — not just re-read.
Never built or run directly on a development host in this project's own workflow — see PROGRESS.md's workflow rule. Build inside the dedicated container:
docker build -t warden-build:rockylinux -f docker/Dockerfile.build .
docker run --rm -v "$PWD:/build" \
-v warden-cargo-registry:/usr/local/cargo/registry \
-w /build warden-build:rockylinux cargo build --release --workspaceThe eBPF modules (warden-exec, warden-network) need a separate
nightly toolchain + bpf-linker, built via docker/Dockerfile.build-ebpf
against the ebpf-probe/ sub-workspace.
Run the test suite and lints the same way:
docker run --rm -v "$PWD:/build" \
-v warden-cargo-registry:/usr/local/cargo/registry \
-w /build warden-build:rockylinux bash -c \
"cargo test --workspace && cargo clippy --workspace --all-targets -- -D warnings".github/workflows/test.yml runs the same
build/test/clippy/cargo-audit checks automatically on every push and
pull request (not inside the Docker build container there, but on a
plain ubuntu-latest runner with the GTK4/libadwaita dev packages
installed directly).
sudo ./install.shDetects your distro's package manager (apt, dnf, pacman, or zypper),
installs build dependencies, builds Warden from source, installs the
systemd units (with a computed sandboxing drop-in for the target user's
actual home directory), and starts protection in either enforce mode
(kills/quarantines immediately) or monitor mode (logs only) — asked
interactively on a fresh install (defaults to enforce if not run
interactively; set WARDEN_INSTALL_MODE=monitor to skip the prompt).
Either way it's switchable anytime afterward from the GUI or
warden --set-mode. Safe to re-run to upgrade an existing install in
place — it never overwrites an existing config.toml, so re-running
never resets a mode you already chose, and it always prefers your own
rustup toolchain over an outdated distro-packaged cargo if one happens
to be on the PATH.
To remove it:
sudo ./uninstall.sh # stops services, removes binaries/units/GUI assets
# — config and quarantine are left in place
sudo ./uninstall.sh --purge # also removes config and quarantine, after confirmationBoth scripts have been validated end-to-end (real install → real
detection → real uninstall) across every supported package-manager
family: apt on two real VMs (Debian 13, Ubuntu 25.10), and dnf/pacman/
zypper each in a dedicated systemd-as-PID1 Docker test image (see
docker/Dockerfile.test.*).
Under active development, hardened through multiple rounds of real
adversarial testing against the running detection modules — two internal
red-team audits, one external community audit, and a dedicated SAST pass
(fanotify TOCTOU windows, package-manager spoofing, honeypot naming,
burst-detector bypasses, control-socket oracles, an overlong-filename
quarantine bypass, and more — every one confirmed live and fixed; see
PROGRESS.md for the full list). All four core detection
modules, the GUI, desktop notifications, the control socket, quarantine
with cross-process locking, and the installer/uninstaller are implemented
and tested. Not yet done: a simplified Sigma-rule detection layer,
setcap-based capability tracking for privesc (SUID/SGID only today),
and cargo-deny alongside the cargo-audit check already in place.
MIT.