Skip to content

Scout — certificate discovery

Alongside scanning the wire, Scout inventories the certificates on disk and reports them as local_certs. This is how the Hub knows what's installed, and it seeds several proof collectors and the auto-scan surface.

Where it looks

In agent mode, Scout inventories certificates from three directory sources — there are no other defaults:

  • {data-dir}/certsalways included. data-dir is the agent's own data directory (the --data-dir flag; default /opt/cyphers/data). It also holds agent.key / agent.crt / ca.crt / state.json; its certs/ subdirectory is where Scout writes the certs it issues or delivers ({data-dir}/certs/<domain>/), so discovery reports back what Scout itself installed.
  • --cert-dira flag, repeatable. Any directory of certs to inventory; default /etc/letsencrypt/live (a standard certbot layout). Pass --cert-dir again to add more.
  • --cert-dir-filea flag naming an operator-maintained file (the installer wires <install-dir>/cert-dirs.conf): one absolute directory path per line, # comments and blank lines allowed. Additive to --cert-dir. Re-read on systemctl reload cyphers-scout (SIGHUP), which also starts an immediate scan cycle (or joins one already in flight — the new directories then land in the next cycle).

/etc/ssl/certs is not a discovery source

Only the three above are walked for the certificate inventory. /etc/ssl/certs and the OS CA bundles are read elsewhere — as trust stores for proof collection — never as discovered end-entity certs.

"Fully recursive" means Scout walks the entire tree beneath each source (filepath.Walk): every nested subfolder and every regular file, not just the top level. There's no extension filter — each file is read and fed to a PEM decoder, keeping only CERTIFICATE blocks (keys, CSRs, and non-PEM files are ignored), so a fullchain.pem yields one DiscoveredCert per block. Symlinked files are followed (the reported path resolves to the real target — matters for Let's Encrypt's live/ symlinks); symlinked directories are not descended into.

What it extracts per cert

Per certificate block:

  • Subject & issuer — CN
  • Serial — plus its DER-hex form and the ARI cert id (RFC 9773)
  • SHA-256 fingerprint
  • Authority Key Identifier (AKI)
  • ValidityNotBefore / NotAfter, IsExpired, days-to-expiry
  • Key — type and size
  • Signature algorithm
  • Chain length — count of cert blocks in the same file
  • SANsDNS names only

Known extraction limits

  • Only DNSNames are captured — IP, email, and URI SANs are silently dropped.
  • No leaf/CA flag is extracted; IsCA isn't read. Downstream code infers "leaf" from a non-empty SAN list — a convention, not a parsed attribute.

What it does with them

  1. Reports every discovered cert as local_certs (the Hub ingests these into inventory).
  2. Localhost/SNI-probes each non-wildcard SAN against 127.0.0.1:443 — to verify what's actually served locally matches what's on disk (catches behind-CDN and out-of-date or undeployed certs).
  3. Auto-scans each non-wildcard SAN via public DNS — expanding the scan surface beyond explicit --targets to every domain named in an installed cert.

Quirks worth knowing (for analysis)

Discovery de-dups by certificate fingerprint, so the same leaf appearing in cert.pem, fullchain.pem, and chain.pem in a Let's Encrypt live/ dir is reported once, not 2–3 times.

Known gap

  • Wildcard SANs are excluded from probing/auto-scan (a literal *. host can't be dialed), so wildcard-covered hosts aren't auto-discovered unless listed in --target.

Maturity: the PEM walk, metadata extraction, fingerprint de-dup, local_certs reporting, and both probing paths are all live and run every scan cycle. The wildcard / non-DNS-SAN items are gaps, not failures.