WhisperDocs
Endpoint

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.

~1%
of a core, idle baseline
~11 MB
resident, idle baseline
250M
hard memory ceiling
0
kernel modules
4
build-failing safety gates

The identity is not optional. The sensor is.

Whisper is two tiers, and the first one needs nothing installed on the host.

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.

i

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.

  1. 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_PACKET tap 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
i

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.

i

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:

CollectorAnswersMechanismBuild
Process execevery execve, with image hash and stable identityNETLINK proc-connector (pure Go)default
Process exec (deep)same, earlier and lower overheadeBPF tracepoint-tags ebpf
File changescreate / modify / move / delete in a watched dirfanotify (notification class)default
Critical-config watchwrites to classic persistence pathsfanotify + a compiled-in allowlistdefault
Sockets, both directionsestablished out and accepted in, per processNETLINK_SOCK_DIAG (no privilege)default
Per-connect net-tapeach outbound dial at SYN_SENTeBPF tracepoint-tags ebpf
DNS attributionwhich name each process asked forAF_PACKET on port 53default
Inventory + posturepackages, open CVEs, SBOM-litedistro security metadatadefault

A few points of real care, because they are the difference between a telemetry firehose and something an analyst can trust:

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.

EngineCatchesATT&CKDefault
Ransomware correlatorencrypt-and-destroy behavior, recovery destructionT1486, T1490on
Process-injection classifiershellcode staging, hollowing, deleted backingsT1055, T1620on
Event-chain correlatorwrite a file, execute it, dial outT1105on
Sigma evaluatorreal SigmaHQ rules over all four streamsfrom the ruleon
YARA-X scannerbyte patterns in unknown executablesfrom the ruleon 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:

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 kernel-enforced cgroup ceiling holds a hot path that never rides the scheduler and a throttled deferred child cgroup for background work; a self-meter ships the measured footprint against the ceiling as proof. KERNEL-ENFORCED CGROUP CPUQuota 25% · MemoryMax 250M · swap 0 HOT PATH kernel taps · local verdict · the connection never rides the scheduler, outside the governor by construction DEFERRED CHILD CGROUP · cpu.max 20% posture · deep scans · injection sweep · ship summary nice-19, idle IO; admitted only when the host is calm; deadline floor self-meter reads its own cgroup counters SHIPPED PROOF measured vs ceiling, every window
The hot path stays in the parent cgroup and never waits on anything. Deferrable work runs in a throttled child cgroup only at calm moments, self-lowered to the lowest priority the kernel offers. The self-meter makes calm a measured property, shipped as proof.

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:

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.

StreamCollected into the local spoolWhat actually ships
execpid, ppid, uid, comm, exe path, a bounded command line, image sha256, stable identitynever-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.
fileper-event verb with the exact path, plus a settled digest for executables onlyfolded 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.
connevery outbound and accepted-inbound leg, pid-attributedone record per novel (process, destination, port) leg. No payloads, ever.
dnsevery stub query, pid-attributedone record per novel (process, name) leg.
posturepackage list and open CVE ids from distro metadatathe CVE ids and package rows; the backend keeps only the package count.
detectionsthe full finding linetechnique, 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.

Four independent opt-in tiers in escalating power: observe, enforce, response, respond. Each is a separate config section, each defaults off, and none ever implies the next. 1 · OBSERVE the sensor default OFF · one flag on collectors on once enabled 2 · ENFORCE the exec guard default OFF its own section, fails open 3 · RESPONSE reversible freeze default OFF, then a 2nd "armed" gate on top 4 · RESPOND bounded actions default OFF, per-action allowlist + operator scope
Observe, then enforce, then response, then respond. Each lives in its own config section with its own switch. Every escalation is an explicit edit; nothing escalates itself.

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 varWhat it doesDefault
WHISPER_SENSORAlso 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_AUTOBINDWhether 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_KEYThe 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.

i

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.

BuildCarriesNotes
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 ebpfSwaps 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 yaraCompiles 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

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
Whisper Endpoint console, Host detail: the same /128 with reverse DNS and RDAP, an Identity verified panel reading 4 checks passed keyless, and a copy-evidence button.
The same checks, in the console: the host's /128 with an Identity verified panel, four checks passed keyless, and a copy-evidence button. The console reads the public record; it is not the trust root. lab fleet

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.

i

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.