Shipping reports to a server¶
--server <url> POSTs Delta’s JSON to a collector — once in one-shot
mode, every cycle in monitor mode — so a fleet can stream its integrity /
drift state to a central endpoint.
delta --cfg policy.json --processes --ports --netif \
--server http://collector.internal:8080/ingest
delta --processes --netif --interval 60 \
--server http://collector.internal:8080/ingest --spool /var/spool/delta
One-shot sends the full report (the same document as JSON output);
--serverimplies report generation even without--json.Monitor (Continuous monitoring) sends one compact NDJSON object per cycle.
A non-2xx response or transport failure prints a warning but does not change the exit code (which reflects the checks); a monitor keeps running.
Identifying the source (--token)¶
--token <token> attaches an identification token so the collector can
attribute the signals to a specific device / agent. The token is sent two
ways:
embedded in the JSON report as a top-level
"token"field (and in every monitor NDJSON cycle), so it travels with the data — including through the retry spool;as an
Authorization: Bearer <token>HTTP header, so a gateway or the server can authenticate / route the request before parsing the body.
delta --processes --netif --interval 60 \
--server https://collector.example.com/ingest --token "$DEVICE_TOKEN"
The token is opaque to Delta — use a per-device key, a fleet/tenant id, or a bearer credential as your collector expects.
Reliable delivery (--spool <dir>)¶
By default a failed POST is dropped. With --spool <dir> an undeliverable
report is written there and retried on later cycles and runs, surviving
transient collector outages without losing data. Each run drains the backlog
oldest-first, then sends the current report; on failure the current report is
spooled too. The directory is bounded (oldest dropped past a cap).
HTTPS / TLS¶
Build with the libcurl backend for https:// (see Build- & run-time features):
cmake -S . -B build -G Ninja -DDELTA_HTTP_BACKEND=curl
delta --processes --server https://collector.example.com/ingest
TLS is verified by default (peer + hostname).
--cacert <file>— verify against a custom CA bundle (private / embedded CAs).--insecure— skip verification. Unsafe; self-signed testing only.
The default socket backend has no TLS and rejects an https:// URL with a
clear message; on an untrusted network with it, front Delta with a
TLS-terminating reverse proxy.
NAT reachability beacon (--stun)¶
--stun <host[:port]> turns a public STUN server into a liveness / reachability
beacon. Delta’s reports already traverse NAT outbound, so STUN is not used for
transport — it answers “can this device still reach the outside, from what
public address, and how fast”. Each probe is a single UDP Binding Request
(RFC 5389/8489, retransmitted within ~3 s); the reply yields the
server-reflexive (public) ip:port and the round-trip time:
delta --interval 60 --server https://collector/ingest \
--stun stun.l.google.com:19302 --json
The result is reported as a stun block (one-shot) and on every monitor
cycle (so the collector sees the device is alive and behind which NAT):
"stun": { "reachable": true, "public": "[2001:db8::1]:54321", "rtt_ms": 21 }
Reachability and the public address are part of the monitor keyset
(nat:reachable=… / nat:public=…), so a lost beacon or a changed public IP
shows up as drift; the RTT feeds the behavioral model, so a latency spike can
surface as an anomaly. It is a signal, never a gate — an unreachable STUN
server does not add a violation or change the exit code. Zero-dependency
(raw UDP), so it works in any build, independent of DELTA_FEATURE_HTTP.
In monitor mode --stun upgrades the beacon to a persistent keepalive +
wake channel (next section).
Server-driven control (commands + wake)¶
In monitor mode the sync is bidirectional: the collector answers each report with a JSON body of commands, which the agent executes. This turns Delta into a remotely-steerable agent without any inbound listener on the telemetry path — commands always ride the authenticated, token-bearing outbound POST.
Response schema:
{ "commands": [
{ "action": "disable" },
{ "action": "enable" },
{ "action": "reconfigure", "config": { "collect": { "processes": true } } },
{ "action": "reconfigure", "config_url": "https://srv/policy.json" },
{ "action": "collect", "domains": ["processes", "conns"] },
{ "action": "stop" }
] }
enable / disable — toggle telemetry collection. While disabled the agent still sends a tiny
{"status":"disabled"}heartbeat each cycle, so the command channel (and a laterenable) keeps working.reconfigure — apply a new policy, either
configinline or fetched fromconfig_url; written to a local file and used from the next cycle (no restart).collect — run an immediate out-of-schedule cycle, optionally widening the collected
domains.stop — exit cleanly.
Signed commands. By default commands are trusted because the transport is
(HTTPS + a bearer token). Where that is not enough — a device that must not
obey a compromised or impersonated collector — --verify-key <hex> pins an
Ed25519 public key and the agent then acts only on commands wrapped in a
signed envelope:
{ "signed": "<hex of the exact command-JSON bytes>",
"sig": "<hex Ed25519 signature over those bytes>" }
The agent verifies sig against the pinned key over the exact payload bytes
(no canonicalization gap), then parses the inner JSON for commands. A seq
field inside the signed payload gives replay protection: the agent
remembers the highest sequence it has accepted and drops any envelope at or
below it, so a captured “disable” cannot be re-played later. When the key is
set, a reconfigure that fetches config_url requires the fetched
config to be a signed envelope too — otherwise a host at that URL could inject
arbitrary policy. Anything unsigned, stale or forged is dropped and the agent
carries on; the switch is fail-closed.
Only verification runs on the device (vendored TweetNaCl, ~8 KB). Generate the
keypair and sign envelopes server-side with tools/delta-sign.py:
tools/delta-sign.py keygen --out-prefix delta-ctl # -> .sk (secret) + .pk
echo '{"commands":[{"action":"disable"}]}' \
| tools/delta-sign.py sign --key delta-ctl.sk --seq 7 > envelope.json
# deploy: delta --interval N --server … --verify-key $(cat delta-ctl.pk)
Active response¶
The commands above only steer collection. A separate, destructive set lets the operator act on a host through the agent, each action naming its target explicitly:
{ "action": "kill", "pid": 1234 }
{ "action": "kill", "process": "evilagent" }
{ "action": "quarantine", "path": "/tmp/dropper" }
{ "action": "restore", "path": "/tmp/dropper" }
{ "action": "block", "ip": "203.0.113.5" }
{ "action": "unblock", "ip": "203.0.113.5" }
{ "action": "fetch", "path": "/etc/ld.so.preload" }
kill — SIGKILL a pid, or every process with a given name.
quarantine — move a file into the quarantine directory (
--quarantine-dir, default/var/lib/delta/quarantine), renamed to a non-executable<name>.<inode>.quarand chmod’d0000. The move is recorded in a.manifestsidecar.restore — undo a quarantine: move the file back to its original path and mode, using the manifest. The inverse of
quarantine.block — install a drop rule for an IP via
nft(iptables/ip6tablesfallback). The IP is validated as a bare literal first, since it reaches a shell. Note: the rule is not persistent — a reboot clears it.unblock — remove the drop rule for an IP (nft by handle,
iptables -D). The inverse ofblock.fetch — read a file (size-capped at 4 MiB) and upload it to the collector as an
{"artifact": …}object with the bytes hex-encoded, for off-device forensics.
These are remote-administration primitives, not detections. Delta never decides that something is malicious and acts on it: nothing the agent observes — a policy violation, an indicator match, a drift finding — triggers any of these. The operator reads the report, decides, and names the pid, path or IP in a command. Detection and action are separate code paths and separate decisions, the second of which is a human’s.
Because these change or destroy host state, they are gated twice and fail-closed:
the command must arrive in a valid signed envelope (
--verify-key) — an unsigned or impersonated server can never trigger one; andthe operator must explicitly opt in with
--allow-response.
With either missing, a response command is logged and dropped; the read-only
commands keep working regardless. Response actions are POSIX-only and are
refused under --root (kill/block have no meaning against an offline image).
Both gates are runtime settings. To remove the capability from the binary
altogether — a build in which no code can modify host state at all — compile
with DELTA_FEATURE_RESPOND=OFF (see Report-only builds).
Acks. Each executed response is reported back to the collector as an
acks array, so the operator sees programmatically whether it took effect:
{ "acks": [
{ "action": "quarantine", "target": "/tmp/dropper",
"ok": true, "detail": "quarantined '/tmp/dropper' -> …" } ] }
The array is POSTed to --server right after the batch runs (a fetch also
uploads the file itself as a separate {"artifact": …} object).
STUN wake. With --stun the monitor holds one UDP socket open (kept alive
by STUN keepalives) and listens on it. An inbound datagram — a “poke” the
collector sends to the device’s last-reported public ip:port — makes the
agent sync immediately instead of waiting out the interval. The poke only
triggers a sync; the actual commands still come back over the authenticated
HTTPS response, so a forged poke can at worst cause an extra sync (and, when a
--token is set, the poke datagram must contain it). The poke reaches the
device only through cone / endpoint-independent NAT; behind symmetric NAT the
interval is the fallback — the agent syncs on its own timer regardless.
End-to-end, the operating model is: the server cross-builds Delta for the target
arch (Cross-compilation), copies and ssh-launches it as
delta --interval N --server https://… --stun … --token …; the agent reports
periodically, holds the STUN keepalive, syncs immediately on a poke, and applies
whatever the server returns.
Notes¶
Connect / send / receive are each bounded (~5 s), so an unreachable collector cannot stall a monitor cycle. The STUN probe is likewise bounded (~3 s).
Compiled only when
DELTA_FEATURE_HTTP=ON(default); an OFF build rejects--server.--stunhas no such dependency.