# 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.

Measured on an idle host: well under a kernel-enforced ceiling (a quarter of one
core, 250M memory, hard) that is the worst-case guarantee, not a target the
sensor drifts toward; the current measured number lives on Calm footprint & SLA
(/docs/footprint), which republishes it from a live capture rather than repeating
it stale on every page. The safety promises are pinned by gates that fail the
build; 2,112 test functions and 93 fuzz targets sit underneath them, counted
today with a plain grep over the sensor package.

## 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 /128 out 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 Verify 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 /128 and 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.

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, 38.6B 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.

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 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.

Plain `sudo` is enough for the restart. It restarts the unit as installed and rewrites
nothing in it, so the sensor keeps reading the `HOME` baked in when the unit was
installed. That was measured too: with sudo's `HOME` at `/root`, the restart replaced the
process and the unit still read `HOME=/home/...`. `HOME` matters at install, where the
installing shell's is the one written into the unit.

A unit installed under plain `sudo` has `HOME=/root` baked into it, and reads its marker
there. `enroll` from your own shell checks that first, and says so in one line instead of
binding where the sensor never looks:

```
$ whisper enroll --name web-01
whisper: the whisper-sensor service runs as root and reads its identity from /root/.config/whisper, which this shell cannot write, so nothing was written. Run 'sudo whisper enroll --name web-01'
```

Run the line it names. If root has no key yet, it binds with yours and gives the service a
copy, so there is no second login. `whisper status` from your own shell cannot read
`/root` either, so it says that rather than judging the sensor by your home:

```
$ whisper status
sensor      running, but this shell cannot tell whether it reports - the service reads its identity from /root/.config/whisper, which this shell cannot read
    check: sudo whisper status
```

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 (/docs/footprint).

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 (/docs/architecture).

## 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 | opt-in, `-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 | opt-in, `-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 (/docs/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
(/docs/detections).

| 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
below). 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 (/docs/detections).

### 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: a hot path that never rides the
scheduler, and a throttled deferred child cgroup for everything that can wait. A
self-meter reads the sensor's own cgroup accounting and ships the measured footprint
against the ceiling as proof, every window.

- 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 (/docs/footprint).

## 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 (/docs/wire-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.

```
1 OBSERVE  ->  2 ENFORCE   ->  3 RESPONSE      ->  4 RESPOND
the sensor     the exec        reversible          bounded actions
default OFF     guard          freeze              default OFF, per-action
one flag on     default OFF    default OFF, then   allowlist + a never-auto
collectors on   its own        a 2nd "armed"       operator scope
once enabled    section        gate on top
```

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 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 (/docs/kill-switch).

## 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 `/dl` is built from the exact
  tree we run in production, so a fresh `curl | sh` (or a `whisper service install
  --sensor` after 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-running `whisper service install --sensor` is 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 is
  whisper posture status, live activity is whisper logs and whisper monitor. Where the
  telemetry surfaces fleet-wide is in Fleet, logs & console (/docs/fleet).

## 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 - our API is 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 (/docs/attack-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 same checks show up in the console (/shots/console-host.png): 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. The full keyless bundle and
its is_whisper_agent, dane_ok and jws_ok fields are in the Quickstart
(/docs/quickstart#verify), and the reader-runnable install-and-audit walk-through is in
Install & verify (/docs/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 (/docs/platforms) 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, 38.6B-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.
