Scouts & Provisioning
An Scout is a probe agent you deploy wherever you want a vantage point. It dials out to the server over SignalR — no inbound firewall rules needed.
Enrolling a Scout (UI flow)
-
Scouts → Add Scout returns a Scout ID (a GUID) and lets you mint a one-time registration key.
-
The registration-key dialog then generates a ready-to-run deployment snippet you can copy and run where you want to target from. A small configurator lets you tailor it before copying:
- Deployment format — a
docker composefile (default) or a singledocker runcommand. - Local storage — SQLite (zero-config file database, no extra container — best for a
single Scout) or PostgreSQL. For Postgres you additionally choose to either run a Postgres
container alongside the Scout (a dedicated
scout-dbservice with its own data volume and a generated password is added to the compose file) or connect to an existing server (enter its host, port, database, username, and password). - Data location — for SQLite, the named volume or host path mounted at the Scout's
/data.
The snippet updates live as you change any option. Secrets it needs — the Scout's
SCOUT__KeyEncryptionKey(the AES-256-GCM key protecting its private key at rest) and, for a bundled Postgres, the database password — are generated in your browser and baked into the snippet, so you never have to invent one. Keep the copied snippet safe and never reuse the key across Scouts. - Deployment format — a
An equivalent single docker run command (SQLite storage) looks like:
docker run -d --name snitcher-scout --restart unless-stopped \
-e SCOUT__URL=https://your-snitcher-host \
-e SCOUT__ScoutId=<scout-id-from-ui> \
-e SCOUT__RegistrationKey=<registration-key-from-ui> \
-e SCOUT__KeyEncryptionKey=<generated-key-encryption-key> \
-e SCOUT__NAME=scout-1 \
-e SCOUT__Database__Provider=Sqlite \
-v scout-data:/data \
--cap-add=NET_RAW \
--sysctl net.ipv4.ping_group_range="0 2147483647" \
hutch079/scout:latest
--cap-add=NET_RAW (and the ping_group_range sysctl) are required only for ICMP. The Scout then shows online/offline, last seen, version,
location, and clock skew. From the detail page you can Ping it for a round-trip health check,
Revoke to drop its connection immediately, or Delete — which optionally reassigns its
resources to a replacement Scout instead of leaving them unmonitored.
When you enrol a Scout you can also give it freeform labels (the same kind used on targets) to group and filter Scouts — for example by region or environment. Start typing in the labels field and it suggests labels already in use, or type a new one and press Enter to create it. Labels aren't fixed at enrollment: open the Scout's detail page and click Edit to change its name, location, or labels at any time. The Scouts list also has a label filter (a tag icon button next to the search box) once at least one Scout carries a label.
On first connect the Scout generates its own keypair and registers the public key with the server;
the private key is stored — encrypted with SCOUT__KeyEncryptionKey — in its own local database
(SQLite by default, or Postgres via SCOUT__Database__Provider/SCOUT__Database__ConnectionString).
SCOUT__KeyEncryptionKey can also be supplied via a Docker secret with
SCOUT__KeyEncryptionKey_FILE=/run/secrets/<name> (takes precedence over the plain env var). See
Scout Authentication & Key Rotation for how the
enrolment handshake and steady-state reconnects actually work.
One-time registration key binding
The registration key issued by Add Scout is one-time: the first time the Scout successfully registers its public key, the key is consumed and the Scout is bound to this single Snitcher instance. The detail page shows the binding state ("Not yet bound" → "Bound <timestamp>"). The same agent keeps reconnecting normally with its stored keypair; binding simply records that this UI registration key belongs to exactly one instance. To point one Scout deployment at several Snitcher instances, use a config-managed Scout instead (below) — those use a reusable key and are never bound.
Config-driven first Scout
To bootstrap without the create-in-UI / copy-key dance, provision a Scout directly from server config:
| Variable | Purpose |
|---|---|
Snitcher__FirstScout__RegistrationKey | Reusable, non-expiring registration key — activates the feature (share it with the Scout's SCOUT__RegistrationKey). |
Snitcher__FirstScout__ScoutId | Scout ID (GUID) this key registers under (share it with the Scout's SCOUT__ScoutId). |
Snitcher__FirstScout__Name | Scout display name (default scout-1). |
Snitcher__FirstScout__Location | Optional location. |
On startup the server upserts a config-managed Scout with that Scout ID and registration key.
Config-managed Scouts are read-only in the UI — they can't be renamed, revoked, or deleted, and
their registration key is never shown (a "config-managed" badge marks them). The bundled compose
wires this to the shared SCOUT__SCOUT_ID / SCOUT__REGISTRATION_KEY so the server and the bundled Scout
use one Scout ID and one registration key.
Config-managed Scouts use a reusable, non-expiring registration key and are never bound to a single instance — see below.
One Scout, multiple Snitcher instances
A single Scout deployment can serve several Snitcher instances at once, but only for config-managed Scouts. Configure a list of upstreams — each with its own URL, Scout ID, and registration key — using indexed env vars:
docker run -d --name snitcher-scout --restart unless-stopped \
-e SCOUT__Upstreams__0__Url=https://snitcher-a.example \
-e SCOUT__Upstreams__0__ScoutId=<scout-id-for-a> \
-e SCOUT__Upstreams__0__RegistrationKey=<registration-key-for-a> \
-e SCOUT__Upstreams__1__Url=https://snitcher-b.example \
-e SCOUT__Upstreams__1__ScoutId=<scout-id-for-b> \
-e SCOUT__Upstreams__1__RegistrationKey=<registration-key-for-b> \
-e SCOUT__KeyEncryptionKey=<scout-key-encryption-key> \
-e SCOUT__NAME=scout-1 \
--cap-add=NET_RAW \
hutch079/scout:edge
The Scout opens an independent connection (and check scheduler), and registers/stores a separate Ed25519
keypair, for each upstream — so the check sets pushed by different Snitcher instances stay isolated. On
each Snitcher instance, seed a matching config-managed Scout (via Snitcher__FirstScout__RegistrationKey /
Snitcher__FirstScout__ScoutId) whose registration key equals the upstream's — the same reusable key may
legitimately be reused across instances because config-managed Scouts are never bound.
The single SCOUT__URL + SCOUT__ScoutId + SCOUT__RegistrationKey form is still supported and maps to
exactly one upstream; that is what the UI's one-time-key snippet uses, binding the Scout to that single
instance. A registered public key is required for every upstream — an upstream without a valid Scout ID
and registration key is skipped.
Clock skew
If a Scout's clock drifts too far from the server, Snitcher surfaces a warning on that Scout (and can alert on it) — monitoring with untrustworthy timestamps isn't trustworthy monitoring.
Key rotation
A trusted Scout's Ed25519 key rotates automatically on a schedule (Settings → Scout Key Rotation, default every 7 days) without ever dropping its connection, and can also be triggered on demand from a Scout's detail page (Regenerate key). For how the rotation handshake actually works — and why Scouts use per-relationship keys instead of a shared secret in the first place — see Scout Authentication & Key Rotation.
If rotation keeps failing (the Scout stays reachable but never completes it) a KeyRotationFailed alert fires once the retry window elapses, so you notice a stuck Scout instead of it silently drifting toward Untrusted.
Untrusted Scouts
If a Scout's key gets older than the configured max key age (Settings → Scout Key Rotation, default 30 days) — typically because it was offline through several missed rotation cycles — it flips to Untrusted. An ScoutUntrusted alert fires, and the Scout's detail page shows an Untrusted badge.
An Untrusted Scout's connections are refused outright until it's re-enrolled. To reactivate it:
- From the Scout's detail page, mint a new one-time registration key (the same action as initial enrolment).
- Update the Scout's
SCOUT__RegistrationKey(it keeps the sameSCOUT__ScoutId) and restart or reconnect it. - The Scout generates a fresh keypair and registers it exactly as it would on first enrolment. The old, untrusted key is discarded and the Scout's history/configuration/assignments are untouched — only its key changed.
Reliable result delivery
Check results are queued locally and never silently dropped, even through a network interruption
between a Scout and the server — only bounded by SCOUT__ResultRetentionHours (default 24
hours). For the batching, acknowledgement, and retry mechanics, see
Reliable Result Delivery.
Session lifetime
An Scout connection stays open indefinitely under normal operation. As a liveness backstop, the server asks any Scout whose connection has been open longer than the configured max session time (Settings → Scout Session, default 24 hours) to gracefully reconnect — it cycles its own connection at a moment of its choosing, so no check or in-flight result is interrupted, and no false "Scout down" is raised. Session age is measured per-Scout from its own connect time, so a fleet self-staggers its reconnects rather than all reconnecting at once.
This is a staleness/liveness safeguard, not a security control — it exists so a connection that looks alive but has gone stale in some unforeseen way still gets refreshed periodically. Key rotation (above) is what governs credential freshness and is independent of this setting. The minimum is 1 hour (to guard against pathological reconnect churn).