# Containment: on-host response and the off-host kill-switch

Response happens in two layers. A bounded, opt-in tier acts on the host. The lethal
lever lives off it, on our own infrastructure, so it holds even when the host is
fully owned.

A host-only sensor runs its brain and its response inside the machine it watches.
When an attacker or a rogue agent owns the host, the off-switch you were counting on
is now under their control. Whisper Endpoint keeps a near lever for fast, reversible
action when the host is still yours, and a far lever the host can never reach. The
far lever is the one that matters at incident time.

In the seven-layer architecture this page is the top two layers: policy, the everyday
governance expressed against the /128 from off the host, and response, the containment
that fires when governance is not enough. They share one design: the identity is the
handle, and the enforcement point is our network, not the host's software.

## Two layers, and why

The default posture is observe-only: the sensor watches and reports, and it does not
sit in the critical path. Response is something you add, deliberately, in two
independent places. A host-only EDR keeps its brain and its kill command inside the
host, so owning the box takes the response with it. Whisper keeps the revoke lever on
its own network and acts on the host's identity from outside it.

## The far side: the off-host kill-switch

Every host and agent carries a routable IPv6 /128 out of 2a04:2a01::/32 (announced by
AS219419), named in DNSSEC-anchored reverse DNS and pinned in DANE. That identity is
the handle. Revoking it stops two things at once: its resolution (it can no longer
look names up through Whisper) and its egress (traffic sourced from its /128 is cut).
The change propagates worldwide at DNS-TTL.

You trigger it from the CLI or the control plane, against the identity, not the host:

```
# the simple form
whisper kill --revoke 2a04:2a01:8dae:4521:45b8:b241:4f91:56e2
whisper: 2a04:2a01:8dae:4521:45b8:b241:4f91:56e2 - released

# the same act on the control plane (API key redacted; use a throwaway, never a live host)
curl -s -X POST https://graph.whisper.online/api/query \
  -H 'content-type: application/json' \
  -d '{"query":"CALL whisper.agents({op:\"revoke\", address:\"2a04:2a01:8dae:4521:45b8:b241:4f91:56e2\"})","apiKey":"whisper_live_...redacted"}'
```

You verify the teardown the same way you verified the identity: with stock tools,
keyless, against the IANA root. A revoked or never-held /128 does not return an empty
answer, it returns authenticated non-existence, DNSSEC-signed denial you can check for
yourself:

```
dig +dnssec -x 2a04:2a01:dead:beef::1 @9.9.9.9
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 26376
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 8   # ad is the resolver's claim about the denial; delv re-derives it

;; AUTHORITY SECTION:
1.0.a.2.4.0.a.2.ip6.arpa. 300 IN SOA   ns1.whisper.online. hostmaster.whisper.online. 1787608004 ...
1.0.a.2.4.0.a.2.ip6.arpa. 300 IN RRSIG SOA 13 10 300 20260826210000 20260824200000 58485 ...
d8pbc0d1ermlg13osn18rbvi7e9la6i1.1.0.a.2.4.0.a.2.ip6.arpa. 300 IN NSEC3 1 0 0 - D8PJBQUAPIJCKDFBND55H0F75GLA64HG
```

Why it holds when the host is owned: the revoke does not ask the host to do anything.
It changes what the network and the authoritative DNS will do for that identity. A
fully-owned host cannot un-revoke itself, because the decision was never on the host.

Containment is scoped, reversible, and leaves a durable audit trail: every contain
action is written to the transparency ledger and exposed as a keyless third-party
proof at /api/contain/action/<ledgerId>, so an auditor can confirm a containment
happened without being handed any trust. Reversal is a scoped, deliberate act of its
own.

## Agent trees: cut the parent, cut the branch

Agents nest. A machine that is a Whisper agent runs a browser agent that egresses
through it, which spawns a scraper of its own, and each of those gets its own governed
/128, to a depth of 32. The tree is measured, not claimed: the egress plane observes a
child's connections physically arriving from its parent's /128 and records that edge,
so "who is nested behind this identity right now" is ground truth, never a
self-declared field an attacker could point at a victim. The details of the identity
model are in /docs/wire-identity; what matters here is what it does to containment.

Policy composes down the measured chain, most-restrictive-wins: a descendant's
effective egress is its own rule-set intersected with every live ancestor's, so a deny
at any layer binds everything transiting below it, and a parent can never widen what a
child may do. Which yields the containment property this section is named for: kill
the parent and every descendant behind it is cut on its next connection. You never
chase the children individually.

The walk, per connect, in our egress plane:

```
scraper -> its own rules: allow -> ancestor build agent: KILLED -> DENY
deploy agent -> no killed ancestor on its measured chain -> ALLOW
# bounded to 32 hops; fails closed past the bound; the refusal is uniform,
# so a probe learns nothing about the tree
```

Two levers cut a subtree, in escalating finality:

- Kill-switch cascade (instant, reversible). Flip the kill on the parent's budget:
  whisper.agents({op:'budget', address:<parent>, kill:true}). The parent's live
  tunnels are cut immediately, and on the next connection every descendant transiting
  it is denied, because the per-connect policy walk finds a killed ancestor on the
  measured chain. Lifting the kill restores the branch.
- Hard revoke (identity teardown). op:revoke on the parent tears down its /128:
  authenticated NXDOMAIN, an RDAP record showing no assignment, DANE gone, and the transit path every child
  egressed through with it. The children's own identities are then revoked separately
  if they should not return elsewhere.

Why the cascade cannot be escaped: the enforcement walk follows only the measured
chain, never a caller-declared parent pointer, so a hostile agent can neither hide
behind a fake parent nor name a victim's identity as its parent to read that victim's
policy state. Past the 32-hop depth bound the walk fails closed, and the refusal is
uniform on every path, so probing the cascade leaks nothing about the tree.

## Before the kill-switch: policy on the same handle

Revocation is the last resort. The everyday layer below it is a set of policy shapes,
all expressed through the same one control verb (whisper.agents), all keyed by the
agent's /128, and all enforced off the host, in our resolver and egress plane, so a
compromised agent cannot disable its own rules the way it could flush its own
iptables:

| Shape | What it governs | Enforced | Status |
|---|---|---|---|
| Egress firewall | Per-identity allow/deny rules on destination ip, cidr, host (exact or any-subdomain) or port; ordered, first match wins, then a default action. Every rule-set write lands on the same audit trail as a revoke. | egress plane, once per connect | live |
| Process-trust graded egress | Maps the graph's per-process trust tiers T0..T4 onto the freedom ladder none < dns-only < mediated < direct < unrestricted; the endpoint's worst observed process tier decides. Only a confirmed graph known-bad verdict reaches T0, never a fail-open guess. | egress plane, per connect | live |
| Byte caps and budget kill | Per-identity byte, connection and request ceilings folded into the live meter, tightest cap wins; kill:true cuts live tunnels immediately. | egress plane, live meter | live |
| Per-run scope | A windowed, deny-default destination allowlist bound to one run's duration: only the listed destinations are reachable while the window is open. | egress plane | live |
| Resolution geo policy | Country allow-lists and deny-lists evaluated live per resolution against the graph's view of where a name resolves; deny wins on conflict. | the resolver | live |
| Jurisdiction-tagged allocation | The /128 itself allocated inside a country-tagged block, so the address encodes its jurisdiction, published in RDAP and an RFC 9092 geofeed. | at allocation | opt-in, off by default |
| Per-device policy | The same policy verb with a device selector routes rules to one identity instead of the tenant; a device override only ever escalates, never downgrades a graph-critical block. A revoke drops the override, so a recycled /128 never inherits one. | the resolver | live |

The trust ladder in one picture, since it is the shape that turns detection into
graded restraint instead of a binary kill:

```
graph trust tier            egress freedom (decided per connect, off-host)
T4 verified          ->  unrestricted
T3 neutral           ->  direct
T2 unverified        ->  mediated
T1 suspicious        ->  dns-only       <- refusal enforced live
T0 known-bad         ->  none           <- only a CONFIRMED graph verdict lands here
```

Maturity, per lever: the read-back of every policy write tells you whether that
namespace is enforced or merely stored. The egress levers above answer
enforcement=live; console namespaces that are recorded but not yet enforced answer
enforcement=stored, and a stored token is never rendered as live policy.
Jurisdiction-tagged allocation is opt-in and disabled by default; until you enable it,
allocations are byte-identical to untagged ones, and a jurisdiction lookup can never
block an allocation.

## The near side: bounded on-host response

When the host is still yours, a complementary tier can act on it directly. It is
armed and opt-in, never on by default: a host that opted into observe telemetry has
not opted into acting. When you arm it, the actions are drawn from a fixed allowlist,
run over a poll-only channel keyed to the host's own /128, and each one is audited.

| Action | What it does | How it is kept safe |
|---|---|---|
| process.stop | Stops a process | pidfd_open + pidfd_send_signal, immune to PID reuse; a safe signal, never a blind SIGKILL mid-transaction |
| quarantine.file | Defangs a file in place to a quarantine store | Neutered to mode 0000, the sha256 recorded so the move is verifiable and reversible |
| service.restart | Restarts a unit | Allowlist-only; no shell, no $PATH lookup, absolute program paths only |
| package.update | Patches a package | The package-manager transaction is left intact; a build guard fails compilation if a shell literal ever appears |

An opt-in local-policy exec guard can also close the loop from observe to verdict to
block on the host itself. And a reversible freeze, a SIGSTOP across the offending
process tree that SIGCONT lifts, ships in observe-first mode: it detects and can hold,
off by default and enforcement-gated, so nothing acts on your fleet until you say so.

Maturity: the on-host response tier and the reversible freeze are real and
live-fire proven, but they are a complement, not the primary line of defense,
precisely because they live on the host. Process-trust graded egress refusals (the
none and dns-only rungs) are enforced live in the egress plane, and the write's
read-back says so per lever. Ransomware copy-on-write rollback is designed and on the
roadmap, not shipped. The adversary-proof lever remains the off-host revoke.

## The containment rail, in the console

In the operator console, containment is a rail with a durable audit trail: arm, sever,
a keyless proof of the sever, and a reversal path. Every step is recorded, and the
proof is one anyone can re-check.

![The Whisper EDR console containment rail: arm, sever, keyless proof, reversal.](/shots/console-containment.png)

## The assets a host agent can never contain

Because the far lever is network-layer, it reaches things that will never run a kernel
sensor:

- OT / IoT: a PLC, a camera, a printer gets a routable identity and default-deny
  egress; one call cuts it off, with nothing installed on the device.
- AI agents and sub-agents: each spawned agent has its own /128, its own logs, and its
  own revoke, and a nested tree is cut at the parent: one kill severs the branch. This
  is the containment control a host-only EDR cannot give a summoned process.
- Personal and contractor devices: a partner device resolves through you and carries a name you
  can check, and you can cut it off without pushing software onto someone else's laptop.

Calm is the composure of power held in reserve. The sensor does not cry wolf, because
the lethal lever is already outside the attacker's reach.
