Skip to content

Scout — overview

Scout is the endpoint agent: a single Go binary you deploy on each server whose certificates Cyphers manages. It has three modes.

A single TLS probe of a target, printed to stdout. Used ad hoc from the command line and by the Hub's own probe engine. No enrollment, no state.

The long-running service. It enrolls once for an mTLS identity, then continuously scans its configured targets, discovers local certificates, reports posture to the Hub, and executes the commands the Hub dispatches (issuance, delivery, revocation). This is what installs as cyphers-scout.service.

The Hub's own Certificate Transparency client, run as a local subprocess by the Master Hub — never deployed to customer endpoints. No enrollment, no identity, no NATS; it streams NDJSON on stdout. See CT log access.

Why Go, not Rust

Scout must negotiate legacy TLS 1.0 / 1.1 to score endpoints honestly. Modern OpenSSL refuses those versions outright, but Go's crypto/tls stack still speaks them — so Scout is the component that can probe legacy endpoints and feed the Cyphers Score (which the Hub actually computes — Scout supplies the findings).

Trust position (read the security page)

Scout holds a private key, carries an mTLS identity, runs as root, and executes commands the Hub sends it. (Lifecycle hooks are the one exception: they are executables the operator places on the Scout host, never code the Hub ships.) Its safety therefore rests entirely on being able to trust the Hub's identity. How that trust is established and defended — CA-fingerprint pinning, per-agent NATS ACLs, and independent verification of the Hub's identity against a signed policy — is the subject of Security.

How it talks to the Hub

Two channels: a one-time enrollment at install, then a continuous mTLS channel for everything after. The default ports and intervals used throughout are collected in Defaults at a glance.

sequenceDiagram
    autonumber
    participant S as Scout
    participant H as Hub
    Note over S,H: Enrollment (once, at install) — TLS, port 7443 default
    S->>H: connect
    H-->>S: certificate chain (leaf, intermediate, root)
    S-->>S: CA matches pinned fingerprint? else refuse
    S->>H: CSR + one-time token
    H-->>S: signed client certificate + signed acceptance policy + signer public key
    S-->>S: signer key matches --policy-signer-fp? else refuse
    Note over S,H: Ongoing — NATS over mTLS, gated on a verified policy
    H->>S: commands
    S->>H: reports, acks, heartbeats, CSRs

Enrollment — once, at install

The installer runs this for you in one step (seconds). The order is what makes it safe: the Scout trusts the Hub before it sends anything.

# What happens Technical detail
1 The Scout already knows the Hub's CA fingerprint and its acceptance-policy signer fingerprint. Baked into the installer as --ca-fingerprint sha256:… and --policy-signer-fp sha256:… — both required, both hashes, not certs.
2 It connects; the Hub presents its certificate; the Scout checks the CA in it against that pinned fingerprint (match → trust; mismatch → refuse, send nothing). TLS handshake to the enrollment listener (:7443, default — Hub --enroll-port / ENROLL_PORT). Hub presents [leaf, intermediate, root]; the one place the Scout obtains the CA.
3 The Scout generates its key + request locally and sends them with a one-time token. Private key never leaves the host; POST /enroll over the now pin-verified TLS.
4 The Hub signs and returns the Scout's own client certificate, a signed acceptance policy, and the acceptance-policy signer's public key. The Scout verifies the signer key's fingerprint against its --policy-signer-fp pin and applies the policy before ever dialing NATS. Reply = {agent_id, cert_pem, resource_spiffe_uri, re_enrollment_token, acceptance_policy, acceptance_policy_signer}no ca_pem (the Scout already has the CA from step 2); both acceptance-policy fields are always present.

The Hub presenting its cert ≠ returning it in the reply

A TLS server shows its certificate — with its CA — the instant you connect (step 2), before any request is sent. The reply (step 4) carries only the Scout's own new certificate. The Scout gets the Hub's CA from the handshake, never the reply.

After this the Scout has its identity and never enrolls again (it renews automatically before expiry).

After enrollment — the ongoing channel

All interactive traffic runs over one mutually-authenticated NATS channel (mTLS) — the old HTTP poll transport is gone, and both sides prove identity with certificates on every message. The subject name encodes who and what: {id} is the agent's own ID, the last segment (command, report, …) is the message type, and * / > are NATS wildcards over a side's subjects. The full list — all seven subjects and the per-agent access rules — lives in Transport & health → Subjects.

Interactive channel (NATS · mTLS) Direction Subject
Commands — scan, issue, deliver, revoke Hub → Scout cyphers.hub.{id}.command
Reports, acks, heartbeats, CSRs, verification telemetry Scout → Hub cyphers.scout.{id}.*

The Hub also runs a plain HTTP listener on one of its own ports (default :8443; the Hub flag is --scout-rest-port / SCOUT_REST_PORT — a legacy name, but it's a Hub port, not a Scout one) serving a few public, read-only PKI endpoints. These live on the Hub, not the Scout — they're the Hub's own CA material. The running Scout is only a client here, and fetches just one of them: the CRL, as a revocation-check fallback when there's no OCSP staple. The rest are for the installer, ops/DR checks, and anything validating a Cyphers-issued cert — see Hub PKI endpoints for the full table.