Skip to content

Scout — enrollment & identity

Enrollment is the one-time exchange in which a new Scout gets its identity: it generates a private key on the host, sends a certificate request to the Hub with a one-time token, and receives back its own client certificate signed by the Hub's CA. From then on every connection between this Scout and the Hub is mutually authenticated (mTLS) — each side proves who it is with a certificate.

You normally never run this by hand: the installer performs it during install (step 4 of its run). This page covers what enrollment produces, how the exchange works, and how the identity stays valid afterwards. The overview has the sequence diagram.

What enrollment produces

Four files in the Scout's data directory (default /opt/cyphers/data):

File Mode What it is
agent.key 0600 The Scout's private key — generated locally, never leaves the host.
agent.crt 0644 The Scout's client certificate, signed by the Hub's CA. Its presence is literally how Scout decides it is enrolled: absent → first run.
ca.crt 0644 The Hub CA chain the Scout trusts — captured from the pin-verified enrollment TLS handshake (not from the reply; see step 3 below).
state.json 0600 Agent id, re-enrollment token, resource SPIFFE URI, the pinned CA fingerprint, and segment/location/labels.

The data directory itself is forced to mode 0700.

The exchange, step by step

Preconditions — Scout refuses to enroll, before generating a key or touching the network, unless all four hold:

  • a one-time enrollment token (--token, minted by the Hub, expires in 1 h),
  • a CA fingerprint pin (--ca-fingerprint sha256:… — the hash of the Hub CA certificate, baked into the installer),
  • an acceptance-policy signer fingerprint pin (--policy-signer-fp sha256:… — required alongside the CA pin; see Security),
  • an https:// Hub URL. There is no unpinned or plain-HTTP enrollment path.
# What happens Technical detail
1 Pre-check the Hub's identity. Scout dials the Hub and confirms the CA it presents matches the pin. VerifyCAFingerprint — live TLS dial before any CSR. The pin comes from the flag, or from state.json left by a previously aborted attempt.
2 Generate the identity locally. A fresh key pair and a certificate request. ECDSA P-384 key; CSR with CN = the Scout's name (--name, defaulting to the hostname), no SANs. The private key never leaves the host.
3 Verify and capture the Hub CA from the connection itself. When Scout connects, the Hub presents its certificate chain; Scout finds the CA matching the pin, confirms it is a CA and that the served certificate chains to it, and keeps that chain — this is what becomes ca.crt. pinnedCACert on the enrollment listener (default 7443). The subsequent request trusts only this pinned CA — never the OS trust store, never unverified TLS.
4 Send the request. CSR + one-time token, over the connection verified in step 3. POST /enroll carrying csr_pem, enrollment_token, hostname, segment, location, labels.
5 Receive the client certificate, the signed acceptance policy, and the policy signer's public key. The Hub always signs the CSR and replies with the Scout's own certificate, the initial signed acceptance policy for its fleet, and the acceptance-policy signer's PEM-encoded public key. The CA is not in the reply because Scout already captured and verified it in step 3. Scout verifies the signer key's fingerprint against the --policy-signer-fp pin, persists it as its local verifier key, then applies the policy through the signature-verified, anti-rollback cache before its first NATS connect — enforcement has a valid policy from the start and a fresh install never deadlocks. Reply = {agent_id, cert_pem, resource_spiffe_uri, re_enrollment_token, acceptance_policy, acceptance_policy_signer}. Both acceptance_policy and acceptance_policy_signer are always present — decoding is strict on both sides.
6 Persist the identity. The four files above are written with the modes shown; the key is written atomically. See What enrollment produces.

The pin is of the intermediate CA

Scout pins the fingerprint of the CA that directly issues its certificate — the intermediate, the first certificate of the Hub's served chain. That is the same fingerprint the installer generator derives, so the two always agree.

How the identity stays valid

On every restart

State on disk What Scout does
Credentials present Offline pin check (VerifyStoredCAFingerprint): the on-disk ca.crt must still match the pinned fingerprint recorded in state.json, or Scout refuses to start. Fail-closed against a swapped CA — even one swapped by an attacker with filesystem access.
No credentials, no --token Refuses with a clear error: enrollment needs a token.
No credentials, --token given First enrollment, as above.

Automatic rotation

Scout renews its own identity certificate before it expires (separate from the managed-certificate renewals it performs for other domains):

  • When it checks: at startup, then every 6 hours.
  • When it rotates: inside the window lifetime ÷ 4, clamped between 7 and 30 days — a 365-day cert rotates in its last 30 days, a 47-day cert in its last ~12.
  • How: re-verifies the Hub's live identity against the stored pin, then re-enrolls using the re-enrollment token from state.json (never the original one-time token). The same refuse-on-pin-mismatch rule applies, so a swapped Hub CA mid-rotation means no new certificate is written.
  • After: credentials reload and the NATS connection re-establishes with the fresh certificate — a clean in-process reconnect, no service restart. The rotation reply re-delivers a fresh signed acceptance policy, which Scout applies to its cache — so rotation also recovers a Scout whose policy had drifted toward staleness.

Retroactive pinning (legacy agents only)

An agent enrolled before pinning existed has no stored fingerprint. The first successful rotation pins the verified CA into state.json, making every subsequent restart and rotation fail-closed. This is the only remaining trust-on-first-use path — a new enrollment always requires an explicit pin.

Which ports identity operations need

Identity operations depend on only the enrollment port. In particular, the Hub's PKI-reads port (default 8443) being blocked does not break enrollment, rotation, or the agent (every default port and interval is collected in Defaults at a glance):

Operation Needs If unreachable
Enrollment (install) 7443 (TLS) Fails cleanly before anything is written — re-run when reachable.
Rotation 7443 (TLS) The attempt fails and is retried at the next 6-hour check; the current certificate keeps working until it actually expires.
Restart pin check nothing — offline Reads the local ca.crt only.
Commands, reports, heartbeats NATS, default 4222 (mTLS) See Transport & health.
Hub PKI reads, default 8443 (HTTP) Never used by enrollment or rotation. The running agent contacts it only as the CRL fallback inside acceptance-policy revocation checks — a secondary path (the primary is the OCSP staple carried in the NATS handshake), bounded to a 5 s fetch. The acceptance policy is mandatory, so the verifier always enforces: a blocked 8443 yields a revocation status of "undetermined", which under revocation_check=required rejects the connection (under soft-fail/none it accepts). Enrollment and cert rotation are unaffected either way.

Pin checks, summarized

Check When Behavior on mismatch
Live dial (VerifyCAFingerprint) Before first enrollment and before every rotation Abort — nothing sent, nothing written
Offline file check (VerifyStoredCAFingerprint) Every restart with existing credentials Refuse to start
Handshake resolve (pinnedCACert) During every enrollment / re-enrollment request Abort — the request only ever trusts the pinned CA

Fingerprints are normalized before comparison (the sha256: prefix, colons, and letter case are ignored), so the usual openssl x509 -fingerprint -sha256 output matches as-is.

Resource SPIFFE URI

At enrollment the Hub assigns the agent a SPIFFE URI — a standardized identity name of the form spiffe://…. Scout persists it in state.json and uses it as its local identity when checking signed acceptance policies: it is the key the verifier looks up to decide which peer identities this agent should accept (see Transport & health).