Fleet and runtime defense¶
Documentation home · Previous: Cloud posture · Next: AI triage review
The fleet is Synapse's distributed blue-team layer. Agents inventory hosts and Kubernetes clusters, run eBPF detections, and execute authorized work orders. A runtime detection is treated as evidence rather than an alert: it is attributable, hash-chained, and joined to the same asset, finding, and attack path the static pillars reason about.
The fleet is off by default and needs PostgreSQL plus synapse-worker. The development Compose stack does not enable fleet routes or run the worker, so /fleet shows an error there rather than representative coverage data. Capture a Fleet screenshot only from a deployment with the fleet flags, worker, enrolled demo agents, and sanitized inventory configured; do not publish an error state as product documentation.
SYNAPSE_FLEET_ENABLED=true # transport + agent-admin routes
SYNAPSE_FLEET_ASSETS_ENABLED=true # asset model + attack paths
SYNAPSE_FLEET_HOST_INGEST_ENABLED=true # accept host inventory
SYNAPSE_FLEET_CLUSTER_INGEST_ENABLED=true # accept Kubernetes inventory
SYNAPSE_FLEET_KEY_REGISTRATION_ENABLED=true # register purpose-bound agent signing keys
SYNAPSE_FLEET_DETECTION_INGEST_ENABLED=true # independently signed P1 detection delivery
SYNAPSE_FLEET_TELEMETRY_INGEST_ENABLED=true # signed raw telemetry + durable-gap transport
Telemetry transport needs the fleet transport plus key registration. Keep the telemetry ingest flag off if the deployment has not applied the telemetry migrations; the server otherwise fails closed when the required persistence or signing-key trust boundary is unavailable. Signed detection delivery similarly requires both key registration and detection ingest to be enabled.
Agents¶
| Binary | Runs on | Collects |
|---|---|---|
synapse-agent |
Linux, macOS, and Windows hosts | Host inventory and, on Linux, eBPF runtime detections |
synapse-cluster-agent |
In-cluster or with a kubeconfig | Kubernetes workload, exposure, and identity inventory |
eBPF detection needs Linux with root or the equivalent capabilities. On other platforms the detection engine stays off rather than degrading silently.
Enrollment and identity¶
An agent enrolls once with a one-time token, then holds a client certificate:
POST /api/v1/agents/enrolment-tokens mint a one-time token
POST /api/v1/fleet/enrol agent redeems it
POST /api/v1/agents/{id}/revoke revoke an identity
# preferred: a root-readable token file, removed after first enrolment
export SYNAPSE_FLEET_URL="https://synapse.example.com"
export SYNAPSE_FLEET_ENROL_TOKEN_FILE=/run/secrets/synapse-enrol-token
./synapse-agent
Prefer the token file over SYNAPSE_FLEET_ENROL_TOKEN, and never use the equivalent command-line flag in
production: an argument is visible in process listings and shell history. After enrollment the agent
authenticates with its certificate and the token is no longer needed.
Certificate issuance requires SYNAPSE_FLEET_CA_CERT and SYNAPSE_FLEET_CA_KEY; treat the CA key as a
production secret. SYNAPSE_FLEET_CERT_TTL (default 24h) bounds certificate lifetime.
Set SYNAPSE_FLEET_CLIENT_CERT_HEADER only behind a reverse proxy that terminates mTLS, verifies the
client certificate, and strips every client-supplied copy of that header before setting it. A proxy that
forwards an unverified header converts this into an authentication bypass.
HTTPS is required for the fleet URL except for a loopback host in development.
Agent lifecycle¶
| State | Meaning |
|---|---|
active |
Enrolled and reporting within the freshness window |
stale |
Last seen longer ago than SYNAPSE_FLEET_STALE_AFTER; computed by coverage, not self-reported |
revoked |
Identity withdrawn by an operator |
compromised |
Marked untrusted; its recent reports are suspect |
tampered |
Reported state failed integrity checks |
decommissioned |
Cleanly uninstalled and retired |
stale is derived rather than declared, so an agent that stops reporting cannot appear healthy. Retire an
agent explicitly so its absence is a recorded decision instead of an unexplained gap:
Inventory and heartbeat¶
POST /api/v1/fleet/heartbeat liveness plus agent-reported state
POST /api/v1/fleet/inventory/host host inventory snapshot
POST /api/v1/fleet/inventory/cluster Kubernetes inventory snapshot
GET /api/v1/fleet/agents operator view
GET /api/v1/fleet/agents/{id}
Configure a host agent with SYNAPSE_AGENT_ROOT (filesystem root to inventory), SYNAPSE_AGENT_NAME, and
SYNAPSE_AGENT_STATE_DIR. Protect the state directory: it holds the agent credential and offline buffer,
including the telemetry WAL under telemetry-spool/.
The cluster agent requires SYNAPSE_CLUSTER as a stable identity keyed into every asset, and accepts
SYNAPSE_CLUSTER_NAMESPACES to narrow scope and SYNAPSE_CLUSTER_RESYNC (default 5m) to set the
collection interval.
Host vulnerabilities¶
A host agent reports the installed OS packages (dpkg, apk, rpm) with distro-qualified package URLs in every inventory snapshot. The control plane records that list as the host's SBOM in a hidden per-host engagement (the fleet twin of a Project's analysis context) and runs the SCA imported-SBOM pipeline against it: the same advisory sources, OS version comparison, severity backfill, KEV/EPSS risk ranking and deduplication a repository or image scan gets. Findings are re-evaluated by the vulnerability reconciliation job when advisories change, so a host that never changes still picks up new CVEs.
Recording is idempotent per package set. An unchanged host does not re-import or re-scan on its next
sweep; a changed set is recorded once the previous scan has finished, and at most once per ten
minutes per host. An inventory above 50,000 packages is refused, and one agent identity may create at
most 16 host assets (a reimaged machine gets a new machine id; an agent varying its facts does not get
unbounded hosts, contexts and scans). The cap is checked before the write and enforced again inside
it: a fleet_assets trigger (migration 0132) serialises new host rows per agent and refuses the row
past the cap, so two syncs racing past the first check cannot both create a host. A refusal is audited
as host_inventory.host_cap_reached and returned as 403. The POST /api/v1/fleet/inventory/host response carries a
vulnerability_scan object with the outcome (engagement_id, job_id, components, or skipped
with a reason). A scan-pipeline failure is audited as host_inventory.vulnerability_scan_failed and
reported in that object; it never fails the inventory sync itself. The host asset's attributes carry the
coverage gaps the agent declared (coverage_gaps, coverage_gap_kinds, coverage_gap_details), and
the host page's Coverage gaps tab lists them with what each one means for the findings.
The VM agent also reports its running processes on the inventory-sweep cadence
(POST /api/v1/fleet/processes, read-only procfs: pid, comm, exe path). The control plane resolves the
host asset from the authenticated agent, stores the running-process projection, and folds the profile
into the asset's behavior baseline (#594 D), so the statistical baseline that scores a host's Behavior
risk factor finally has input. Set SYNAPSE_PROCESS_REPORT_ENABLED=false to disable it. The advisory-revision reconciler
visits host contexts alongside projects, so a host whose packages never change still gains a finding
when a new advisory names one of them.
GET /api/v1/assets/hosts every host with its vulnerability summary
GET /api/v1/assets/{assetID}/vulnerabilities one host: packages, latest scan, findings
The console lists hosts under Fleet, Hosts, worst first, and opens each host to its findings with package, installed and fixed version, severity, CVSS and KEV. The hidden context does not appear in the engagement list and is not reachable through the engagement routes.
Detections¶
SYNAPSE_DETECT_CLASSES=process,network,file,privilege
SYNAPSE_DETECT_CPU_CEIL_PCT=25
SYNAPSE_DETECTION_ENGAGEMENT_ID=engagement-id
An empty class list disables the engine. When CPU exceeds the ceiling, classes are shed in a defined order rather than dropped arbitrarily, and a shed class is recorded so coverage stays honest. Detections surface per engagement:
When SYNAPSE_DETECTION_ENGAGEMENT_ID is set, the agent generates a purpose-bound Ed25519 key,
persists the private half as detection-transport.json under the protected state directory, proves
possession to POST /api/v1/fleet/keys, and drains P1 independently to
POST /api/v1/fleet/detections. Enable both SYNAPSE_FLEET_KEY_REGISTRATION_ENABLED=true and
SYNAPSE_FLEET_DETECTION_INGEST_ENABLED=true on the control plane. The server derives the agent
identity from its credential, resolves the named key, verifies every content digest and signature,
then seals each detection exactly once.
A pending batch coordinate, membership, and engagement attribution are written before the network
request. If the agent restarts or loses the HTTP response, it retries the same sequence and membership;
the control plane idempotently skips what was already sealed. Changing the configured engagement while
a batch is pending fails closed instead of re-attributing it. The local P1 WAL is ACKed only after a
complete 2xx response, and per-epoch ACK history lets a reboot finish committing a batch whose WAL
records were already reclaimed. Keys rotate before expiry, and one 403 causes one new key registration
plus a retry of the same pending sequence. A second rejection stops that delivery lane instead of
generating keys indefinitely; the raw telemetry and durable-gap workers remain independent.
Rate rules¶
A rule matches per event or per burst. A windowed rule (Window{Count, Within, GroupBy}) counts the
events its predicates match inside a sliding span, partitioned by the grouped fields, and fires once when
the count is reached; the detection carries the burst as evidence (the last 64 events when it is longer)
and the count restarts. det.suspicious_dns_beacon v2 is the first: 120 outbound DNS datagrams to one
destination inside a minute, grouped by net.remote_addr. v1 fired on every DNS packet, which is name
resolution, not beaconing. The same evaluator replays stored telemetry in retro hunts and release
evidence, so a windowed rule fires on the same bursts offline as it does live. Per rule the evaluator
tracks at most 1024 groups and evicts the stalest, so a sensor's memory stays bounded whatever an
attacker varies.
Durable telemetry spool¶
Before the detection engine evaluates an eBPF event, the agent normalizes it to the canonical telemetry envelope and appends it to a checksummed priority WAL. Confirmed detections enter the same spool at P1. The shared WAL has four priority lanes; dedicated transports select only the lanes they own so one busy lane cannot consume another transport's read budget:
| Priority | Signals | Disk-pressure behavior |
|---|---|---|
| P0 | response verification, coverage, sensor state | never shed; producer backpressure plus a durable gap record |
| P1 | confirmed detections | never shed; independently signed detection delivery when an engagement is configured |
| P2 | privilege changes and critical-file telemetry | never shed; A3 raw telemetry delivery |
| P3 | background process and network telemetry | oldest P3 segment evicted first, only after its loss is durably journaled; A3 raw telemetry delivery |
SYNAPSE_TELEMETRY_SPOOL_BYTES sets the WAL-segment quota (default 512 MiB). The small state and gap
journals are outside that quota so a full data allocation cannot prevent the agent recording why data
was not retained. A restart reads both state generations, validates CRC32C frames, removes ACKed bytes,
repairs corrupt/torn segments, and continues the current (priority, epoch, sequence) coordinate. A
kernel reboot changes the Linux boot UUID, advances the epoch, and safely restarts sequence at one.
The WAL is the A2 durability boundary. Confirmed P1 detections have their own signed shipper when an engagement is configured. A3 independently drains raw P2/P3 telemetry into signed transport batches and ships the durable gap journal. Each worker owns only its lane/state and retains WAL on terminal failure, so a rejected detection batch cannot silently delete or stop unrelated raw telemetry evidence.
Set SYNAPSE_AGENT_METRICS_ADDR=127.0.0.1:9465 to expose /metrics. This listener is deliberately off
by default and has no authentication. Exported series have bounded labels (priority only):
synapse_agent_spool_recordsandsynapse_agent_spool_record_bytessynapse_agent_spool_oldest_unacked_age_secondssynapse_agent_spool_next_sequenceandsynapse_agent_spool_highest_acked_sequencesynapse_agent_spool_gap_recordsandsynapse_agent_spool_gap_bytessynapse_agent_spool_evicted_records_totalsynapse_agent_spool_corruption_events_totalsynapse_agent_spool_fsync_totalandsynapse_agent_spool_fsync_duration_seconds_total
Alerting¶
Set SYNAPSE_ALERT_WEBHOOK_URL and the control plane posts a signed JSON alert to that URL every time
correlation opens an incident. Correlation itself runs after every detection batch that seals new
detections, off the agent's request, one run per engagement at a time (batches that arrive during a run
are folded into a single rerun), and on demand through POST /api/v1/fleet/engagements/{id}/correlate;
detections completed later by provenance reconciliation are correlated the same way. So with
SYNAPSE_FLEET_DETECTION_INGEST_ENABLED, SYNAPSE_FLEET_CORRELATION_ENABLED and a webhook set, a
detection on an agent becomes an incident and a notification without anyone calling an endpoint.
SYNAPSE_ALERT_WEBHOOK_URL=https://hooks.example.com/synapse
SYNAPSE_ALERT_WEBHOOK_SECRET=$(openssl rand -hex 24) # required; >= 16 bytes
SYNAPSE_ALERT_MIN_SEVERITY=medium # critical | high | medium | low | info
When SYNAPSE_ALERT_WEBHOOK_URL is configured, the signing secret is required by default:
startup refuses a webhook without one. Set SYNAPSE_ALERT_WEBHOOK_ALLOW_UNSIGNED=true only for
a development receiver that cannot verify signatures; this explicitly permits delivery without
a secret, so the receiver cannot authenticate those alerts. Leave unsigned delivery disabled in
production.
The body is {"type": "incident.created", "sent_at": ..., "alert": {...}} with the incident id, asset,
engagement, severity, title, a short summary and a console link (/fleet/incidents/{id}). It carries no
raw telemetry. For signed delivery, X-Synapse-Signature is sha256=<hex HMAC-SHA256> over
<X-Synapse-Timestamp>.<body>; verify it and reject stale timestamps. Transient failures (network, 429,
5xx) are retried three times; a 4xx is final. Every attempt is audited as alert.delivered or
alert.failed with the sink and the error (the error never carries the webhook URL, whose path is the
credential for many chat hooks), so a missed page is in the audit log. Delivery runs on a bounded
worker set off the ingest path, so a slow receiver never holds an agent's request. A tenant is limited
to 60 delivered alerts per minute; the excess is audited as alert.suppressed, and a full queue as
alert.dropped. Delivery never blocks or rolls back the incident it reports.
POST /api/v1/alerts/test (administer) sends an alert.test alert that bypasses the severity floor and
returns how many sinks acknowledged it; use it after configuring the receiver. The webhook client refuses
private and link-local destinations unless SYNAPSE_ALERT_WEBHOOK_ALLOW_PRIVATE=true; http is accepted
only for a loopback receiver in development.
Coverage¶
Coverage answers what the fleet can actually see. It reports stale agents, missing classes, and shed
telemetry instead of implying complete visibility. SYNAPSE_FLEET_COVERAGE_FRESHNESS_TARGET (default
24h) sets the freshness objective.
Work orders¶
POST /api/v1/fleet/work/claim
POST /api/v1/fleet/work/{id}/progress
POST /api/v1/fleet/work/{id}/result
Agents claim signed work, report progress, then report a result. The lifecycle is
issued → claimed → running → succeeded | failed | refused | cancelled | expired. An agent
that declines work records refused rather than failing quietly. Bound server-side dispatch with
SYNAPSE_AGENT_CONCURRENCY, SYNAPSE_AGENT_QUEUE_DEPTH, SYNAPSE_AGENT_MAX_PARALLEL, and
SYNAPSE_AGENT_RECON_CONCURRENCY.
Response actions are governed, reversible, and audited. They run through the same scope and authorization enforcement as any other execution.
Wire the routes and a defender can drive the full loop: POST /api/v1/blueteam/engagements/{id}/response/plan
dry-runs an action (isolate_host, quarantine_file, stop_process) and its mandatory reversal and executes
nothing; .../response/apply applies it through the shared admission gate (server-side scope
authorization, a recorded human approver, a blast-radius check on the executed effect); POST
/api/v1/blueteam/response/{id}/revert reverses it; GET /api/v1/blueteam/response lists the
admitted-but-not-applied set the kill switch cancels. The action id is server-minted, and a second-approval
requirement answers 202. The default executor records the full admission -> approval -> apply -> verify ->
revert ledger without touching a host; a real host executor is a deliberate, review-gated extension point
(internal/usecase/response/simulation.go), so applying a real isolation still requires an explicit
execution-safety decision the platform does not make on its own. The POST /api/v1/redteam/halt kill switch
now cancels pending response actions as a fourth layer, so one operator action stops the whole estate.
Rollout and upgrades¶
GET /api/v1/agents/rollout
PUT /api/v1/agents/rollout
POST /api/v1/agents/rollout/promote
POST /api/v1/agents/rollout/pause
POST /api/v1/agents/rollout/resume
A rollout advances in stages and can be paused or resumed. SYNAPSE_FLEET_MIN_AGENT_VERSION sets a version
floor and rejects agents below it; empty means no floor.
Self-update artifacts are verified against a built-in Ed25519 release key before any binary is swapped.
SYNAPSE_UPDATE_PUBLIC_KEY overrides that key and should only be used for a controlled private release
channel. Rotating the update key is asymmetric: already-deployed agents reject a new key until they receive
it, so ship the new public key in a release signed by the old one first.
For packaging, service integration, and uninstall contracts, see Fleet agent packaging.
Telemetry transport¶
Raw telemetry is deliberately isolated behind persistence ports so the finding, judgment, and evidence
paths never wait on a high-volume store. ports.TelemetrySpool is the agent-side WAL boundary;
ports.TelemetryStore is the control-plane columnar boundary; the A3 transport store owns delivery
sequence commitments, highest-contiguous ACK state, and persisted coverage gaps.
With SYNAPSE_FLEET_KEY_REGISTRATION_ENABLED=true and
SYNAPSE_FLEET_TELEMETRY_INGEST_ENABLED=true, the host agent registers a persisted purpose-bound Ed25519
key and drains recovered raw P2/P3 telemetry without requiring the detection producer to be running. The
P1 detection lane remains owned by the independent detection shipper described above. The agent rotates
the telemetry signing key before expiry and retries transient registration failures instead of disabling
transport until restart.
POST /api/v1/fleet/keys register the purpose=telemetry-batch public key + proof of possession
POST /api/v1/fleet/telemetry signed telemetry batch or signed durable-gap report
Normal batches use JSON (gzip on the wire). Durable spool-gap reports use the same authenticated endpoint
with Content-Type: application/vnd.synapse.telemetry-gap+json; their Ed25519 commitment is domain-separated
from batch signatures. The control plane treats the authenticated fleet principal and its reconciled host
asset as authoritative: supplied agent, host, session, asset, stream, key ID, schema version, and signature
must all pass validation before persistence. Unsupported schema versions and identity/signature failures
fail closed and are audited.
Batch ingest is idempotent per stream incarnation and sequence. A reboot advances the epoch, so a legitimate reset to sequence one is distinct from replay. The response ACK is the highest sequence with no missing predecessor; received sequences above a hole are durable but cannot advance deletion past the hole.
The server persists two different forms of coverage evidence:
- inferred delivery gaps, which close only when their missing sequence range actually arrives;
- agent-origin spool gaps (quota eviction, backpressure, corruption, torn writes, I/O failure, unsynced tail, or state recovery), which are immutable loss provenance and are not resolved by a later delivery ACK.
Retro-hunt coverage reads both sources. Any overlapping open delivery hole or agent-origin loss makes the
window incomplete instead of silently returning Complete=true. Agent-origin gaps survive server restart
and are tenant-isolated by PostgreSQL RLS.
Network errors, HTTP 429, and 5xx responses retain the local WAL/gap journal and retry with bounded backoff;
Retry-After is honored when present. Terminal 4xx responses retain the durable local evidence but stop the
transport loop rather than hot-looping a request the server has rejected.
The retention, sampling, and ingest-budget behavior described in the telemetry store ADR remains the columnar-tier contract; A3 adds the live signed delivery path and the coverage-honesty bridge into that tier without changing A5's permanent evidence/Merkle scope.
Purple coverage¶
Emulation expectations are compared against observed detections, so a missing detection is reported as a coverage gap. See Governed assessments.
Next: AI triage review