The Linux sensor
One user-space process, a resource ceiling the kernel enforces, and a brain that lives off the host. It watches every process, file, socket and DNS lookup, decides locally, and reports as a routable /128 anyone can verify. That last clause is the part a host-only sensor cannot copy: the identity it reports as, and the off-host switch that revokes it, live on the network, not the box, so they outlast a full compromise of the box itself. This page is the complete map: every capability it gives you, every knob it exposes, and how to run it across a fleet.
The identity is not optional. The sensor is.
Whisper is two tiers, and the first one needs nothing installed on the host.
- The network identity (always on, keyless). Every host and agent gets a routable IPv6
/128out of 2a04:2a01::/32, with DNSSEC-anchored reverse DNS, a DANE key pin and a public RDAP record. That identity, and the off-host kill-switch that revokes it, are network-layer. They cover a host with no sensor, a PLC, a camera, a contractor laptop, an ephemeral AI agent, anything that can hold an address. Nobody has to trust our API to check it (see below). - The sensor (opt-in, per-process depth). On top of that address, the Linux sensor adds the deep on-host view: which process did what, correlated into on-host detections, folded into the graph. It is the power-up, not the price of entry. A host can carry the
/128and the kill-switch forever and never run a sensor.
This is the whole product philosophy in one sentence: the useful, verifiable thing works with no key and no agent, and the sensor is an additive layer you turn on where you want process-level depth. The rest of this page is that layer.
What the sensor is, in one line. A single static Go binary running as an ordinary systemd service. No kernel module, no filter driver, no boot-time kernel code. The default build does not even load an eBPF program. Its failure domain is one process inside a cgroup, and that is the point.
A bad sensor update cannot take your fleet down
This is the first thing a skeptical operator should test against, so we lead with it. It is true for stacked architectural reasons, each independently verifiable in the shipped design, not a QA promise.
- It is user-space, not a kernel module. The default collectors ride stable kernel interfaces over ordinary syscalls: the process-events connector for exec, fanotify in notification class for files, the socket-diagnostics interface for connections, an
AF_PACKETtap for DNS. Nothing of ours executes in kernel context. Contrast that with a kernel-resident agent, where a bad update faults the kernel itself. Here the blast radius is one process. - Observe-mostly by construction. Every default collector is notification-class: the kernel reports what already happened and suspends nobody. No process ever waits on the sensor. The single blocking primitive in the whole codebase (an opt-in exec guard) is a separate config section, is never armed by enabling telemetry, and fails open on every path.
- The brain is off-host. Verdict fusion happens against the backend graph (7.52B nodes, 39.8B relationships, the public figures). If the sensor is slow, degraded, or gone, the host keeps resolving, keeps its identity, and keeps its network-layer protection. Nothing on the host's critical path waits for it.
- The kernel enforces the ceiling. The generated unit pins the sensor inside a cgroup: a quarter of one core, a hard memory cap, swap denied. A runaway is throttled and then killed inside its own box, never a user process. The ceiling is a kernel guarantee, not a line of our code that has to run correctly.
- Even a crash-loop is bounded. Restart is bounded with an anti-flap backoff, so a bad binary crash-loops quietly in its own cgroup while the host and its connectivity carry on. On stop or crash the kernel releases everything automatically: fanotify groups close, any pending permission event resolves as allowed, netlink subscriptions vanish. No armed state outlives the process.
The framing to hold onto. The sensor is one user-space process with a hard, kernel-enforced resource ceiling, observe-mostly collectors, and an off-host brain. A bad update crash-loops inside its own cgroup; your kernel, your workloads, and your connectivity never notice. That is not a slide, it is the failure domain.
Install: one command, a bounded unit
On a Linux host:
# curl -fsSL https://get.whisper.online | sh
whisper: installing…
whisper: installed ✓ (run: whisper)
whisper: installed to /usr/local/bin/whisper - run: whisper
That is the plain path, and it is deliberately small: a signed, checksum-verified binary on PATH, nothing more. As root it lands in /usr/local/bin, which is already on PATH on Debian and RHEL and in sudo’s secure_path, so whisper works in the same shell that installed it with nothing to export. An unprivileged user gets ~/.local/bin instead, which is the right place for them; --dir or WHISPER_DIR overrides either. It ships as a raw binary straight from our own /dl, not a distro package, so there is no apt, dnf or apk entry to wire; that is what keeps it current, since /dl is built from the same tree we run in production and so cannot lag behind it.
Add your API key and the same command mints the host's own /128, because now there is a key to mint it with; add WHISPER_SENSOR=1 alongside it and it also installs the sensor under a bounded systemd unit:
# curl -H "X-API-Key: whisper_live_...redacted" https://get.whisper.online | WHISPER_SENSOR=1 sh
whisper: installing…
whisper: installed ✓ (run: whisper)
whisper: bound 2a04:2a01:f804:fd8:4b5b:1a4b:4885:9c9c (a4b5b1a4b48859c9c.td68f56a48850edbd42b1dcd97f680e8d.agents.whisper.online)
whisper: sensor service installed + running (whisper-sensor)
Three lines, and the host is enrolled, identified and observed. Check it the way you would check any unit:
# systemctl status whisper-sensor
whisper-sensor: active (running)
# journalctl -u whisper-sensor -n 3
whisper: sensor: reporting as 2a04:2a01:f804:fd8:4b5b:1a4b:4885:9c9c
whisper: sensor: seeded 5 already-running process(es) from /proc
whisper: posture: submitted 0 open CVE(s), 149 package(s) for agent-a4b5b1a4b48859c9c
whisper enroll is idempotent, so running the line again on a host that is already bound re-finds the same /128 and prints it rather than minting a second one. That matters if a first attempt got partway: re-running the command above is the repair, not a reinstall-from-scratch.
If the unit is running and nothing arrives
There is one way to end up with a sensor that looks healthy and sends nothing, and it is worth naming because the host gives you no hint. Install without a key, sign in afterwards with whisper login --web, and you have a key but no bound /128. The sensor then installs, starts, and reports active, while every window it builds is dropped on the way out:
$ systemctl is-active whisper-sensor
active
$ journalctl -u whisper-sensor | tail -2
whisper: sensor: no bound /128 identity - summaries stay local until one is bound
whisper: sensor: host-telemetry ship failed (no /128 endpoint identity
(no usable autobind marker at ~/.config/whisper/bound) - summary dropped
Signing in stores a key; it does not bind an address. The sensor ships under the host's own /128, so until one exists there is nowhere to ship to. Two commands fix it, and the first prints the address it bound:
$ whisper enroll
whisper: bound 2a04:2a01:8e72:79a8:4931:e2a3:652b:f2c8 - a4931e2a3652bf2c8.t0cb7eea9a37a2ccbf04309f8b0e61868.agents.whisper.online
$ sudo -E whisper service restart --sensor
whisper-sensor: restarted
The restart is not optional. A sensor already running when you enroll does not pick the new identity up on its own, which was measured rather than assumed: after enroll alone the unit stayed quiet, and only after the restart did it log reporting as 2a04:2a01:8e72:... and start building windows again.
Keep the -E. Plain sudo resets HOME to /root, and the marker enroll just wrote lives in your home, so without it the service looks in the wrong place and you are back where you started.
If you would rather read the unit before it runs, install and start are separate steps, and this path picks up whatever identity the CLI already has on disk:
# install the unit; read it; then start when you are ready
$ whisper service install --sensor
$ whisper service start --sensor
# or do both at once
$ whisper service install --sensor --now
Turn the host's own traffic onto its /128
Everything above gives the host an identity and watches it. The other half is making the host's own outbound traffic actually leave from that identity, which is a second unit and one more line:
$ sudo -E ~/.local/bin/whisper service install --now --tier wireguard
installed and started whisper-connect (linux-systemd)
config: /etc/whisper/connect.json
status: whisper service status
It names no agent, because the host already knows which one it is: enroll wrote that, and the connection unit reads the same marker the sensor does. It names no port either, because 1080 is the default and the service tells you what it bound:
$ whisper service status
whisper-connect (linux-systemd): running
serving: socks5h://127.0.0.1:1080
egress: 2a04:2a01:9bb9:18fc:9394:d6b4:50f9:4a37
use it: export ALL_PROXY=socks5h://127.0.0.1:1080
That last line is the whole interface. Point a shell, a tool or an agent at it and the traffic leaves as the host:
$ export ALL_PROXY=socks5h://127.0.0.1:1080
$ curl -s https://rdap.whisper.online/egress-ip
{"ip":"2a04:2a01:9bb9:18fc:9394:d6b4:50f9:4a37"}
$ dig +short -x 2a04:2a01:9bb9:18fc:9394:d6b4:50f9:4a37
a9394d6b450f94a37.tda5b2769f82218539b098f195da08096.agents.whisper.online.
The address the internet saw is the host's own /128, and the reverse DNS of that address names the agent. Nothing in that path needs root at runtime, a kernel module, or /dev/net/tun: the tunnel is userspace WireGuard over an ordinary outbound UDP socket, which is why the unit holds no capabilities at all.
Name --tier wireguard explicitly. The install still defaults to the SOCKS5 tier, because the daemon does not yet negotiate a fallback the way interactive whisper connect --tier auto does, and silently turning a working Tier-1.5 install into a failing Tier-1 one on a host where WireGuard cannot come up would be the worse trade.
If 1080 is already taken on the host, the daemon says so and takes a free port rather than refusing to start, and whisper service status then reports the port it actually serves:
whisper: connection daemon: 127.0.0.1:1080 is held by another Whisper proxy this service
does not own - serving on a free port instead
whisper: connection daemon: serving socks5h://127.0.0.1:34995, egress from 2a04:2a01:...
The sensor is its own unit, deliberately separate from the connection unit, so host telemetry never rides, or risks, the connection, and the reverse. The generated unit carries a published resource ceiling:
[Service]
CPUQuota=25% # a quarter of one core, hard
MemoryHigh=150M # soft: the kernel throttles the sensor first
MemoryMax=250M # hard: the kernel stops the sensor's cgroup, never the host
MemorySwapMax=0 # it can never push your pages to swap
IOWeight=10 # idle-class disk weight (neutral is 100)
TasksMax=64
Delegate=yes # its own cgroup sub-tree, for the throttled deferred child
Those exact values are also a first-class value in the code, shipped in every summary window as an actual-versus-ceiling proof, and a unit test asserts the generated unit text carries them. The promise on the wire and the floor in the kernel cannot drift apart silently. The full calm story is in Calm footprint & SLA.
Explained hardening. The unit turns on ProtectHome=read-only, ProtectSystem=full, PrivateTmp, RestrictSUIDSGID and LockPersonality. A few kernel-protection switches are deliberately left off, with the reason stated in the unit itself, because they conflict with fanotify and BPF work. A switch that has to be turned off again is worse than one that was never turned on. Every privilege the sensor holds is itemized with its reason in How the sensor works.
What it observes: the collectors
The telemetry layer is a set of independent collectors, each wrapping exactly one kernel observation surface, all draining into one local stream. Two laws govern every one of them. Observe never blocks: the worst failure anywhere in this layer is a dropped observation, counted and surfaced, never a stalled process. Forward first: every event crosses one boundary, is forwarded downstream unchanged, and only then is a copy fanned to any detector, each behind its own panic firewall. A faulting detector loses one copy of one event; the stream, the graph and the collector never notice.
kernel surfaces collectors one boundary downstream
proc-connector ─► exec collector ─┐
fanotify ─► file collector ├─► stream tap ─► process graph
sock_diag ─► socket collector │ (forwards + local NDJSON spool
AF_PACKET :53 ─► dns collector │ FIRST, then
/proc + statfs ─► host-state sampler┘ fans copies) ─► detectors (copies,
panic-firewalled)
The shipped set, all Linux, all kernel-graceful and fail-open:
| Collector | Answers | Mechanism | Build |
|---|---|---|---|
| Process exec | every execve, with image hash and stable identity | NETLINK proc-connector (pure Go) | default |
| Process exec (deep) | same, earlier and lower overhead | eBPF tracepoint | -tags ebpf |
| File changes | create / modify / move / delete in a watched dir | fanotify (notification class) | default |
| Critical-config watch | writes to classic persistence paths | fanotify + a compiled-in allowlist | default |
| Sockets, both directions | established out and accepted in, per process | NETLINK_SOCK_DIAG (no privilege) | default |
| Per-connect net-tap | each outbound dial at SYN_SENT | eBPF tracepoint | -tags ebpf |
| DNS attribution | which name each process asked for | AF_PACKET on port 53 | default |
| Inventory + posture | packages, open CVEs, SBOM-lite | distro security metadata | default |
A few points of real care, because they are the difference between a telemetry firehose and something an analyst can trust:
- Stable process identity across PID reuse. PIDs recycle; the sensor never pretends otherwise. The stable identity is the triple of pid, process start ticks, and the kernel's per-boot UUID. A recycled PID gets a fresh start time, and a reboot is fenced off. Attribution happens at fold time, and a process the sensor never saw exec degrades to a PID-only node. Fail open, never a wrong name.
- The image digest is of what executed. Each exec's image is hashed by reading the kernel's own handle to the running file, which keeps resolving to the bytes actually running even after an unlink or a rename-over. The hash is bounded (a size cap with growth detection), runs off the drain on a bounded worker lane, and is identity-cached so the same shell exec-ing all day is hashed once.
- Only real remotes. The socket collector filters out a host talking to itself or its own link, unmaps v4-in-v6 so one peer folds to one graph node, and marks accepted inbound connections distinctly from dialed outbound ones. It needs no privilege at all.
- Telemetry integrity. The netlink drains accept only datagrams sourced from the kernel, so a local process cannot inject forged exec or socket records to fabricate activity or bury real signal.
The differentiator: process, then name, then answer, then peer
Because Whisper also serves the host's DNS, the sensor closes a loop most endpoint tools cannot. The DNS collector sees which name each process asked for, including the loopback hop to a local stub resolver, which is exactly the hop that carries the real querying process. The socket collector sees which peer the process actually dialed. Each record carries the socket's source port. The backend already holds the resolver's half of the same moment: the query, the client port, the answer, and the graph verdict for the name.
process ──► name asked ──► answer served ──► peer dialed ──► graph verdict
(host DNS tap) (our resolver) (host socket tap) (7.52B-node graph)
Stamping the source port on both host-side legs gives the backend the join key to fuse resolution with the exact process that made it, with no guessing the domain back from an IP. PID attribution is post-hoc and degrades to an unattributed-but-held edge under a flood. Every collector, its exact mechanism, and its degradation ladder are in Telemetry collectors.
And encrypted DNS, head-on, because every EDR has this blind spot and most bury it. A process that switches to DoH or DoT does bypass the port-53 name tap, but it does not go dark. The sensor still records the dial itself as a pid-attributed connection edge; the graph still scores the peer, so a dial to a known public resolver reads as exactly that; and a process opening TLS straight to an external resolver, instead of the host's configured DNS path, is itself a loud novel signal on a host that never did it before. Then comes the lever no host-only sensor has: the resolution plane the host already points at is a policy point, so per-device policy can refuse to bootstrap known DoH resolvers by name, and where the endpoint's egress rides the Whisper plane the same policy denies or gates the direct resolver dial itself, off-host, with no agent cooperation. Sidestepping the tap does not sidestep the network that carries it.
What it decides: the detection engines
Collectors are visibility. The verdict layer that reads them is twelve on-host engines in total; five of them, the ones with the most to say about how an attack actually looks, are below, each an observer that side-consumes copies off the wide tap. By construction a detector can delay a detection, never a byte of telemetry, and every engine is observe-only: it emits a finding and does nothing else. No engine kills, ptraces, suspends or quarantines anything, and a build-time safety test scans the engine source and fails the build if that promise is broken. The full twelve, with the rest of the catalog, are in Detection engines.
| Engine | Catches | ATT&CK | Default |
|---|---|---|---|
| Ransomware correlator | encrypt-and-destroy behavior, recovery destruction | T1486, T1490 | on |
| Process-injection classifier | shellcode staging, hollowing, deleted backings | T1055, T1620 | on |
| Event-chain correlator | write a file, execute it, dial out | T1105 | on |
| Sigma evaluator | real SigmaHQ rules over all four streams | from the rule | on |
| YARA-X scanner | byte patterns in unknown executables | from the rule | on in a -tags yara build; the default build carries no YARA at all |
Under an enabled sensor, every one of these observes out of the box; each still takes an explicit false in the config to opt back out (see Install-time switches). YARA is the one with a second gate ahead of that: the scanner has to be compiled in with -tags yara before there is anything to opt out of, and the binary the plain one-liner installs does not carry it.
The engine you should judge the design by is the ransomware correlator, because its whole story is false-positive safety by construction. A compiler, a backup job, rsync, or a package upgrade cannot trip it, and each protection is structural, not a tuned constant an operator has to get right:
- Corroboration is required. No single cheap signal, and no innocuous pair, can reach the firing line. High-entropy content only counts beside destruction: an in-place overwrite of files the process did not create, a mass delete, or a uniform rename churn. A benign encrypted vault being populated only creates files, so its content can never convict on its own. The public formulation is deliberate: multi-signal correlation that requires a corroborating destructive signal before entropy can convict.
- Content evidence exonerates too. When sampled files read as genuinely plain, the burst is judged to be the build or the sync it looks like, and confidence is reduced. A parallel compile crosses the same mass-write gate ransomware would, and is cleared the same way, by its own plain output.
- Neutral verdicts an attacker cannot bank. A sample that starts with a common container or archive signature is neutral, so prepending a known magic to ciphertext buys nothing; base64 or hex armored near-uniform bytes are neutral too, so armoring to duck the entropy bar earns no innocence.
- Canary tripwires. Decoy files are planted in the watched directories. Beyond a fixed set, additional decoys are derived per host from a keyed function over a stable host secret, so they are different on every machine and impossible to ship a skip-list for. Nothing legitimate touches a canary.
The injection classifier reads a process's own memory map and nothing else, no disassembler ever, curates out the JIT runtimes that legitimately hold executable anonymous memory (the JVM, V8, .NET and kin), and re-checks a package upgrade's replaced libraries against the process's own filesystem view before it will call a deleted backing suspicious. Sigma and YARA are the real upstream engines running on-host, and the rule's own metadata resolves the technique: a rule that names none emits an unmapped sentinel rather than an invented id. Confidence never reaches certainty anywhere; that, and destination reputation, are the graph second pass's to add. The full engine-by-engine treatment, including the embedded rule sets and their authors, is in Detection engines.
Known-good: the calm majority, affirmatively
Beside every image digest the sensor stamps a known-good mark from a local, build-embedded allowlist of signed OS-package executables. It is an O(1) set lookup with zero network. The mark is a trust annotation only: it rides to the graph and the console so an analyst sees the calm majority instead of only red, and it lets the heavy YARA scan skip a signed system binary. It is structurally never consulted by a behavioral detector, so a known-good binary that behaves badly still ships its detection. Less heavy work, zero lost coverage.
Why you never feel it: the calm plane
"You never feel it" is engineered, not hoped for. Above the kernel ceiling sits an on-host governor that splits the sensor's work in two.
- The hot path never rides the scheduler. The kernel taps, the local verdict, and the connection are outside the governor's reach by construction. The sensor's reaction time never depends on the host being idle.
- Deferrable work waits for calm. Posture collection, deep scans, the injection sweep and shipping the summary are admitted against live host state: pressure-stall signals, CPU, load, battery, thermal, whether a person is at the keyboard, and link activity. Every signal is fail-soft, so an unreadable one means unconstrained, never an error.
- It bursts only when it is safe. When the host and the network are measured quiet and nobody is logged in, the worker pool grows up to tenfold, collapsing back to one the instant the host looks busy again. Admission fails open, but the burst needs positive evidence of a quiet link, because the burst multiplies the sensor's own footprint.
- Nothing waits forever. Every deferrable task carries a deadline; past it the task runs even on a busy host, still at the lowest priority the kernel offers. Keeping a machine loaded does not starve the sensor's homework, so an attacker cannot suppress a scan by keeping the box busy.
- Calm is measured, not asserted. A self-meter reads the sensor's own cgroup accounting and ships a footprint heartbeat every window. A breach flag is only ever positive evidence: an unmeasured dimension reads as unproven, never as a breach.
- Two more rungs below the ceiling. An in-process resource breaker grades one-directional shedding notches (reduce sampling, then pause heavy scans, then trip a latch) that only ever shed the sensor's own discretionary work; a global disarm latch drops the whole deferrable tier to a counted no-op without exiting, while the connection keeps serving. Every failure direction points at the sensor's own work, never at the user.
The heartbeat is a real line from an idle lab fleet host, and it is the proof, not a slide:
calm footprint: cpu 1.01% of the 25% quota, mem 11MB of the 150MB soft ceiling (250MB hard), within SLA
Read that line for what it is: one idle-baseline sample, a host mostly at rest, so the fair summary is about 1% of a core and about 11 MB resident, not a promise with two decimal places. Under active detection the sensor works harder, and the number that bounds the worst case is not a sample at all: it is the kernel-enforced 25% ceiling, which the sensor is engineered never to approach and which holds even if every soft mechanism above it fails. A full under-load footprint distribution is a fair thing to ask for in a POC, and we would rather measure one on your workload than publish a synthetic curve. What is not a sample: the same cgroup-accounting proof rides every window on the wire, idle or busy, so the footprint is a property you watch fleet-wide, not a line on a slide. The deeper mechanism, the vigilance bands, and the fleet SLA rollup are in Calm footprint & SLA.
How it reports: the /128 and a keyless uplink
The sensor never mints an identity. It reports as the persisted /128 recorded at install time, reused across every restart and upgrade, re-read each window so binding an identity later is picked up with no restart, and it states which identity it reports as in one journal line. Identity is server-stamped from the authenticated transport, never claimed from the payload: a host cannot claim to be another host.
Two products ride the wire, deliberately kept apart. The full event stream stays on the host as a local NDJSON spool the operator owns, one JSON object per line, mode 0600, flushed per line. The uplink ships a tiny curated digest: a novel-only delta of the host graph, hard-capped at 8 KiB gzipped per window, plus immediate detections, plus a per-window health and SLA proof. Everything observed is visible locally; only a bounded, deduplicated, curated summary ever leaves the machine.
The uplink is itself two-tier, Postel on both halves:
- Tier 1, keyless on the
/128(primary). The digest posts source-bound to the host's own address; the packets are the identity. No API key, no enrollment token, no host secret on the wire. The whole path works with no account at all. - Tier 2, keyed fallback. When the identity plane is unreachable, the same bytes post to the public ingest pair with the owner's API key or a scoped token, explicitly scoped to the host's own
/128. A key only widens the fallback; it is never required for the primary path.
What stays local versus what ships is a table worth reading before you deploy, because minimization here is by construction: the wire format cannot carry what it has no field for.
| Stream | Collected into the local spool | What actually ships |
|---|---|---|
| exec | pid, ppid, uid, comm, exe path, a bounded command line, image sha256, stable identity | never-before-shipped processes, with the image hash and known-good mark. The command line never ships; it is matched locally and stays on the host. |
| file | per-event verb with the exact path, plus a settled digest for executables only | folded directory prefixes of novel processes (the filename is dropped). Exact paths ship only for a settled executable write with its digest. File contents never leave the host in any form. |
| conn | every outbound and accepted-inbound leg, pid-attributed | one record per novel (process, destination, port) leg. No payloads, ever. |
| dns | every stub query, pid-attributed | one record per novel (process, name) leg. |
| posture | package list and open CVE ids from distro metadata | the CVE ids and package rows; the backend keeps only the package count. |
| detections | the full finding line | technique, confidence, signal tokens, a few bounded sample paths, spool-first then a fail-open uplink. |
A ship failure is one log line and a dropped window, never a blocked collector or a growing on-host queue. Summaries are novel-only, so the next window recovers, and every Nth window is a full re-assert so a restarted backend rehydrates. The /128, DANE and RDAP story is in The /128 identity.
Configure: the opt-in ladder and every knob
The sensor's whole operator surface is four independent opt-ins, in strictly escalating power, none of which ever implies another. This ladder is the single most important control fact: turning on telemetry can never turn on blocking, and turning on blocking can never turn on freezing.
Install-time switches
The one-liner installs the CLI and, by default, nothing that observes. Observing a host is an explicit choice, never a side effect of minting an identity. All switches are environment variables, case-folded liberally so Yes, TRUE and on all work:
| Env var | What it does | Default |
|---|---|---|
WHISPER_SENSOR | Also install, enable and start the host-sensor service. Binds a /128 first if none is bound. A sensor failure warns and never fails the install. | off |
WHISPER_AUTOBIND | Whether and how the installer binds this endpoint's /128: register a per-endpoint agent, reuse the key's own /128, or never. | inferred |
WHISPER_KEY / WHISPER_API_KEY | The API key ladder for the keyed tier. Keyless is a real tier, not an error. | keyless |
There are deliberately no sensor tuning environment variables. The sensor's configuration is a file, and most of what matters is not configurable at all.
The config file
The host sensor is project-independent: a fresh box has no project, so the installer resolves and creates a host-level config with a dedicated watch directory already wired. One command just works. An existing config is honored verbatim; reinstalls never clobber an operator's edits.
# /etc/whisper/sensor.json (system service) - what a fresh install actually
# writes. Nothing here is false: every collector and every detection engine
# takes its compiled-in default the moment it is absent from the file.
{
"sensor": {
"enabled": true,
"dirs": ["/var/lib/whisper-sensor/watch"]
}
}
That compiled-in default is on: every collector and every behavioral engine (ransomware, event-chain, Sigma, injection, and the rest of the twelve) observes out of the box under an enabled sensor. Only YARA needs the separate -tags yara build first. Config can only narrow the surface, so opting a specific engine back out means naming it explicitly:
{
"sensor": {
"enabled": true,
"eventchain": false, // opt OUT of the event-chain correlator;
// every other engine keeps its default
"dirs": ["/var/lib/whisper-sensor/watch"]
}
}
The enforce, response and respond sections each carry their own enabled and their own additional gates, all off by default; the containment page covers them in full.
The missing knobs are the design. Detector windows, thresholds, weights, canary sets, the config-watch path allowlist, collector cadences and buffer bounds, the response tier's action grammar: none of these are configurable. Config can only narrow the surface (fewer directories, fewer actions, fewer restartable units), never widen, retarget or defang it. Two reasons an evaluator will respect: a mis-tuned detector is a silently defanged detector, and every widening knob is an attacker-reachable surface. There is no config that turns the sensor into a different product on one host of your fleet.
Build tags: compiled, not configured
The whisper CLI and client tooling are open source (MIT) and publicly distributed; the sensor's detection engines are proprietary for now, to be opened later, which is why these pages explain the design rather than pasting the source. A build tag changes how the sensor binary is compiled, never which detections it runs.
| Build | Carries | Notes |
|---|---|---|
go build (default) | Pure Go, no cgo, no GPL object. Exec via the process-connector, files via fanotify, sockets via the socket-dump interface, DNS via a kernel-filtered packet tap. | What the one-liner installs. Pure Go keeps this build free of any GPL object. |
-tags ebpf | Swaps in the eBPF exec collector and adds the per-connect tap that catches each outbound dial the instant it starts. | Links a GPL-2.0 kernel object, so it is a deliberate, separately-licensed build kept apart from the MIT-licensed CLI and tooling. Same event shape either way. |
-tags yara | Compiles the YARA-X engine (a cgo static library). | The default build ships zero YARA. Even this build keeps scanning off until sensor.yara opts in. Two gates: the build and the config. |
Calm levers and vigilance bands
With an API key, the backend computes a per-endpoint advisory hint the sensor pulls on a band-adaptive cadence. Its vocabulary is four vigilance bands (neutral, low, elevated, high) and three burst advisories (allow, hold, now). The contract is one-directional and worth printing in full: a hint can make the governor smarter about when to lean in or lean out, and nothing else. It can never touch the hot path, never exceed the resource SLA (the one actuation it can request, a temporary lift of the deferred child cgroup, is hard-clamped at the unit's outer 25% quota by construction and restored the moment the hint lapses), never starve past-deadline work, and never survive its TTL. No key, no bound identity, a lapsed TTL, or a backend outage all mean the same thing: the shipped local governor runs byte-for-byte unmodified. The keyless tier is the baseline product, not a degraded one; the keyed hint is additive smartness.
Per-device policy, from the network side
Containment does not need a cooperative agent. Per-tenant and per-device resolution policy is a control-plane primitive (whisper policy, op:policy, with a device selector), so containment and egress policy act from the network side even where no sensor runs. The off-host kill-switch is the identity itself: revoking an endpoint's /128 cuts resolution and egress fleet-wide with no host cooperation, and it survives full host compromise. The details are in Containment.
Operate a fleet
Everything an operator does day to day is a short, boring command. Boring is the goal.
# enroll a host: the CLI, then a key to mint its /128, then the sensor
$ curl -H "X-API-Key: whisper_live_...redacted" https://get.whisper.online | WHISPER_SENSOR=1 sh
# the service commands; --sensor selects the host-sensor unit (omit it and you
# are operating the connection service instead)
$ whisper service status --sensor
$ whisper service stop --sensor # pause observation; config and spool remain
$ whisper service start --sensor
$ whisper service restart --sensor # apply a config edit
$ whisper service uninstall --sensor # the full off switch: stop + remove the unit
# read the local ground truth, no key, no service needed
$ whisper sensor exec # live exec stream to stdout, NDJSON
$ whisper sensor files --dir /srv # live file stream for a dir
$ whisper sensor flush # tell the running unit: ship this window now
- Upgrade is re-running the installer. The binary at
/dlis built from the exact tree we run in production, so a freshcurl | sh(or awhisper service install --sensorafter it) always lands the current release; the sha256 and PGP checks run on every fetch, upgrade included. Rollback is keeping the old binary around and pointing the unit back at it, plus a restart. No custom self-updater, no side channel. Re-runningwhisper service install --sensoris idempotent: it re-renders the unit, and when the unit actually changed it restarts the running process so the new definition takes effect. - Identity survives upgrades. A restart or binary swap always reuses the bound
/128, and the journal states which identity it reports as, so an upgrade that changed anything is a one-line spot. - Rollback is safe by shape. A single static binary plus deliberately minimal external state (a config file, an NDJSON spool, an identity marker). Old binary plus restart equals rolled back, and the observe posture means no rollback can leave a blocking hook behind, because nothing blocking is persistent in the kernel.
- Read logs on the box. The journal is one line per operator-relevant fact and silence otherwise: which identity it reports as, how many collectors started and where the spool is, every degrade (each naming what was lost and that the rest keeps serving), the per-window footprint heartbeat with the governor's counters, and every response action's terminal outcome.
- Read the fleet in the console. Detections land on the console task feed the moment they are found. The per-endpoint process census is
op:list{kind:'proc'}, posture iswhisper posture status, live activity iswhisper logsandwhisper monitor. Where the telemetry surfaces fleet-wide is in Fleet, logs & console.
Remove it completely, and leave nothing behind
whisper service uninstall --sensor is the off switch for the sensor alone. To take the whole thing off a host, including the identity:
# curl -fsSL https://get.whisper.online/uninstall | sh -s -- --yes
whisper: removed the host sensor service
whisper: removed the connection service
whisper: revoked 2a04:2a01:f804:fd8:4b5b:1a4b:4885:9c9c (identity, PTR and egress tokens withdrawn)
whisper: removed /usr/local/bin/whisper
whisper: uninstalled ✓
Afterwards the host holds none of it, and that is worth stating precisely because it has not always been true:
# command -v whisper -> (nothing)
# ls /etc/systemd/system/whisper-sensor.service -> No such file or directory
# systemctl is-enabled whisper-sensor -> No such file or directory
# ls -d /etc/whisper -> No such file or directory
# systemctl --failed | grep -c whisper -> 0
What changed. Earlier versions reached the service removal only through the macOS .pkg layout, so on a Linux curl | sh install it never ran: an uninstall took the binary and revoked the identity and left whisper-sensor.service on disk and enabled, pointing at a path that no longer existed. It then failed at every boot, on a machine whose owner believed they had removed us. /etc/whisper/sensor.json was never removed by anything, so a later reinstall silently inherited config written by whichever version had been there before. Both are fixed, and the sweep of /etc/whisper is deliberately conservative: it removes the file we wrote and drops the directory only if that leaves it empty, because it runs as root and an operator may keep something of their own there.
The revoke is the part that needs the host to still be able to reach us. If it cannot, the uninstaller says so and prints the exact command to revoke the identity centrally instead, rather than leaving you to discover later that a dead host still holds a live /128.
Verify it is working
Three independent proofs, and the first two need no key and no trust in us: the identity, checkable by anyone against the internet's own root; the detection engine, provable end to end on your own box; and the running sensor's own signal.
The identity, keyless. The /128 the installer minted is a real object in the public record. Anyone can check it with stock tools, with our API nowhere in the trust decision. The example below is a live identity, the agent scout, so you can run every line yourself:
# reverse DNS names the address - the identity, in public
$ dig -x 2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478 +short
ae3b051ff3bf7f478.tdc38e7c55bad3306a92b830f9bb1e4f9.agents.whisper.online.
# the name is DNSSEC-validated to the IANA root - we are not in the trust path
$ delv -x 2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478
; fully validated
# the DANE-EE key pin (usage 3, selector 1, matching 1) - no public CA needed
$ dig +short TLSA _443._tcp.ae3b051ff3bf7f478.tdc38e7c55bad3306a92b830f9bb1e4f9.agents.whisper.online
3 1 1 9EC1EF18A1F15E5480A0FC3C8D6E9690B2CCF1A7FA7146201940F28C422BB47D
The detection engine, keyless. Identity has its dig-and-delv proof; detection has its own, and it is just as reader-runnable. The sensor bundles a Whisper_EICAR_Test_File YARA rule for exactly this: opt in to the YARA engine ("yara": true in the config), write the standard EICAR test string into the watched directory as an executable, and the finding lands in the journal and the local spool, named by the rule. No API key, no account, no trust in us: the same harmless test file the antivirus industry has used as its proof-of-detection since 1996, caught end to end by the shipped engine on your own box. It is the detection analogue of the keyless identity proof above. The behavioral run is public too: we drove real Atomic Red Team atomics against the shipped sensor and captured 37 of 37 techniques as telemetry, live, with the signature layer naming 32 of 37 (86%); the verbatim captured events and the per-tactic matrix are in ATT&CK coverage. A public MITRE ATT&CK Evaluation is scheduled for 2027; until it runs, these proofs, all in your hands, stand in for the badge.
The running sensor. A fresh install ships its first summary in seconds, then on a five-minute cadence, so a new box never looks dead; whisper sensor flush ships the current window on demand for scripted checks. On the box, whisper service status --sensor reports plainly, and an enabled-but-idle sensor says so rather than pretending. The per-window heartbeat proves the footprint against the published ceiling, and the process census confirms the collectors are attributing:
$ curl -s https://graph.whisper.online/api/query \
-H 'X-API-Key: whisper_live_...redacted' \
-H 'content-type: application/json' \
-d '{"query":"CALL whisper.agents({op:\"list\", kind:\"proc\",
address:\"2a04:2a01:8dae:4521:45b8:b241:4f91:56e2\", limit:500})"}'
envelope: op=list ok=true status=200 elapsed_ms=66
processes returned : 500
hash-verdict known-good (fleet allowlist) : 52
known-good on THIS host : 54
carrying a signal : 62
The full keyless bundle and its is_whisper_agent, dane_ok and jws_ok fields are in the Quickstart, and the reader-runnable install-and-audit walk-through is in Install & verify.
Platform and maturity
This page documents Linux, and Linux deep detection is live: every collector and all five detection engines above ship in the Linux sensor today, default-on under an enabled sensor where the table says so. Windows emits telemetry through Event Tracing for Windows and macOS through an entitlement-gated EndpointSecurity bridge, both over the same wire contract, but their deep-detection pass is a later story. This page makes no Windows or macOS detection claim; Platform support owns that matrix.
Telemetry is not detection. The measured run covers 37 of 37 ATT&CK techniques for visibility; the signature and analytic layer covers 32 of 37 (86%). We keep the two strictly apart on every page. Confidence never reaches certainty on any finding, and certainty plus destination reputation are the graph second pass's to add, on the public 7.52B-node, 39.8B-relationship graph.
What should convince you here is not a count, it is the gates. The safety promises on this page are enforced by checks that fail the build the moment they are broken: if a detection engine ever references a kill or ptrace primitive, or a shell; if a filesystem-wide fanotify mark appears anywhere; if a sensitive read is not symlink-refusing. A unit test asserts the generated systemd unit carries the exact published cgroup ceiling, so the promise on this page and the floor in the kernel cannot drift apart silently. A promise a compiler enforces cannot quietly rot; underneath those gates sit 2,112 test functions and 93 fuzz targets, counted today with a plain grep over the sensor package, a supporting fact rather than the argument. The other numbers we publish are the kind that build confidence without handing an attacker an evasion map: the idle-baseline footprint, the kernel-enforced worst-case ceiling, the bounded queues and hash caps. What we deliberately do not publish is any detector's firing threshold, window size, signal weight, canary placement or rule contents, because those are the only numbers an attacker could actually use. And the dated expectation, stated plainly: a public MITRE ATT&CK Evaluation in 2027.