PQCrypta Discovery Agent — Complete Operator Guide
1. Overview
The Discovery Agent is a single, read-only, cross-platform Rust binary that inventories every cryptographic asset it can reach — certificates and keys on disk, SSH keys, live TLS endpoints, database columns, F5/NetScaler appliance certificate stores, and weak-crypto calls in source code — then classifies each by post-quantum readiness and remediation ownership. It has no installer, no daemon, and no dependencies: download it, point it at a config, run it once.
It operates in one of three modes: submit results to the PQCrypta API, write a raw JSON file, or produce a complete self-contained assessment entirely offline (§7).
2. Install
Binaries are public — no account or API key required to download. Pick your platform:
curl -o pqcrypta-discovery https://api.pqcrypta.com/stream/downloads/discovery-agent/linux
chmod +x pqcrypta-discovery
curl -o pqcrypta-discovery https://api.pqcrypta.com/stream/downloads/discovery-agent/macos
chmod +x pqcrypta-discovery
curl -o pqcrypta-discovery https://api.pqcrypta.com/stream/downloads/discovery-agent/macos-intel
chmod +x pqcrypta-discovery
Invoke-WebRequest https://api.pqcrypta.com/stream/downloads/discovery-agent/windows -OutFile pqcrypta-discovery.exe
curl -sSL https://api.pqcrypta.com/stream/downloads/discovery-agent/install.sh | bash
The Windows binary is built on a Windows CI runner and both macOS binaries on genuine Apple
hardware — the arm64 build natively, the Intel build cross-compiled to x86_64 on that same Apple
Silicon runner. The Linux binary is built on PQCrypta's own Linux build host. Every one of them is
covered by the signed SHA256SUMS manifest below, which is what actually pins what you
received. The agent needs no runtime, no admin rights to run, and touches nothing it
scans — it only reads.
2a. Verify Your Download
Every release is hybrid-signed — a classical Ed25519 signature and a
post-quantum ML-DSA-65 (NIST FIPS 204) signature over a SHA256SUMS manifest.
The install.sh path verifies the Ed25519 signature (against a key pinned in the script) and
the checksum before it ever marks the binary executable — it fails closed. For the removable-media /
air-gap workflow, verify by hand on the destination host after transfer:
# Fetch the binary + integrity files
BASE=https://api.pqcrypta.com/stream/downloads/discovery-agent
curl -o pqcrypta-discovery $BASE/linux
curl -o SHA256SUMS $BASE/verify/SHA256SUMS
curl -o SHA256SUMS.sig $BASE/verify/SHA256SUMS.sig
curl -o SHA256SUMS.mldsa.sig $BASE/verify/SHA256SUMS.mldsa.sig
# Fetch the public keys from a DIFFERENT origin than the binary — the
# out-of-band key repo at github.com/PQCrypta/discovery-agent-keys
KEYS=https://raw.githubusercontent.com/PQCrypta/discovery-agent-keys/main
curl -o signing.pub $KEYS/pqcrypta-agent-signing.pub
curl -o signing-mldsa.pub $KEYS/pqcrypta-agent-signing-mldsa.pub
# 1. Classical: verify the Ed25519 signature over the manifest (any OpenSSL)
openssl pkeyutl -verify -pubin -inkey signing.pub -rawin \
-in SHA256SUMS -sigfile SHA256SUMS.sig
# 2. Post-quantum: verify the ML-DSA-65 signature (OpenSSL 3.5+ / FIPS 204).
# OPENSSL_CONF=/dev/null is set here on purpose: if your openssl.cnf loads an
# oqsprovider it hijacks ML-DSA and the key fails to load with
# "ossl_store_handle_load_result:unsupported". Ignoring the config file forces
# OpenSSL's own FIPS 204 implementation. Harmless if you have no such provider.
OPENSSL_CONF=/dev/null \
openssl pkeyutl -verify -pubin -inkey signing-mldsa.pub -rawin \
-in SHA256SUMS -sigfile SHA256SUMS.mldsa.sig
# 3. Confirm the binary matches the now-trusted manifest
sha256sum -c SHA256SUMS --ignore-missingWindows (PowerShell)
$BASE = "https://api.pqcrypta.com/stream/downloads/discovery-agent"
Invoke-WebRequest "$BASE/windows" -OutFile pqcrypta-discovery.exe
Invoke-WebRequest "$BASE/verify/SHA256SUMS" -OutFile SHA256SUMS
Invoke-WebRequest "$BASE/verify/SHA256SUMS.sig" -OutFile SHA256SUMS.sig
Invoke-WebRequest "$BASE/verify/SHA256SUMS.mldsa.sig" -OutFile SHA256SUMS.mldsa.sig
# Public keys come from a DIFFERENT origin than the binary — the
# out-of-band key repo at github.com/PQCrypta/discovery-agent-keys
$KEYS = "https://raw.githubusercontent.com/PQCrypta/discovery-agent-keys/main"
Invoke-WebRequest "$KEYS/pqcrypta-agent-signing.pub" -OutFile signing.pub
Invoke-WebRequest "$KEYS/pqcrypta-agent-signing-mldsa.pub" -OutFile signing-mldsa.pub
# Signature verification (needs openssl on PATH; OpenSSL 3.5+ for the ML-DSA step)
openssl pkeyutl -verify -pubin -inkey signing.pub -rawin -in SHA256SUMS -sigfile SHA256SUMS.sig
openssl pkeyutl -verify -pubin -inkey signing-mldsa.pub -rawin -in SHA256SUMS -sigfile SHA256SUMS.mldsa.sig
# Checksum — compare against the SHA256SUMS entry for the .exe
(Get-FileHash -Algorithm SHA256 pqcrypta-discovery.exe).Hash.ToLower()
Select-String -Path SHA256SUMS -Pattern 'pqcrypta-discovery.exe'$BASE/verify/ on api.pqcrypta.com for environments that
can't reach GitHub — if you use that mirror, fingerprint checking below is essential, since key
and binary then share an origin.) Before trusting a fetched key, cross-check its fingerprint
against the other anchors, each on separate infrastructure:
(1) this documentation page (served from pqcrypta.com);
(2) the GitHub repo above; and
(3) a DNSSEC-signed DNS TXT record on
_pqcrypta-signing.pqcrypta.com — the zone is DNSSEC-signed (DS in the .com
root), so that record is cryptographically authenticated end-to-end, not spoofable by an on-path
attacker:
delv TXT _pqcrypta-signing.pqcrypta.com # expect: "fully validated"# The three published fingerprints (all three must agree):
# Ed25519 (classical) sha256(pqcrypta-agent-signing.pub)
c46d2fac1a3039e9a8aee8637e21eb39d6558da25a5814b97a29f7fb9237dee0
# ML-DSA-65 (FIPS 204, PQC) sha256(pqcrypta-agent-signing-mldsa.pub)
68ceb2dc10b4789eabef1f903d879408a15b3fe8cf2dc15eec3cb839fb981ddf
# verify a downloaded key matches (Unix): sha256sum signing.pub
# (PowerShell): (Get-FileHash -Algorithm SHA256 signing.pub).Hash.ToLower()The scanner ships its own CBOM
The agent publishes a CycloneDX 1.6 Cryptographic Bill of Materials of itself at
verify/agent-cbom.json
(covered by the same signed SHA256SUMS). It documents, honestly, the cryptography the
scanner itself uses: its transport is standard classical TLS 1.2/1.3 (via rustls), and its
release supply-chain signatures are hybrid classical + ML-DSA-65. So the tool that inventories your
cryptography discloses its own — answering the FAR-rule question about your vendors' cryptography before
it's asked.
3. Run Modes
Exactly one of these three is required each run:
| Mode | Flag | Use when |
|---|---|---|
| Submit | --api-url + --api-key (or in config) | Normal operation — results flow into the Compliance Dashboard. |
| Raw JSON | --output results.json | You want the raw inventory as a file to process yourself. |
| Offline / air-gap | --offline-report ./assessment | Isolated network — a complete report + CBOM computed locally, no network (§7). |
# Submit to the dashboard
./pqcrypta-discovery --config agent-config.toml \
--api-url https://api.pqcrypta.com --api-key YOUR_KEY
# Raw JSON, no submission
./pqcrypta-discovery --config agent-config.toml --output results.json
# Offline air-gap bundle, zero network
./pqcrypta-discovery --config agent-config.toml --offline-report ./assessmentCommand-line flags
| Flag | Meaning |
|---|---|
--config <file> | Config file (default agent-config.toml). Its scan_paths are authoritative — they replace the built-in catalog rather than adding to it. |
--with-defaults | Use with --config to scan your paths and the built-in per-OS catalog (256 paths on Linux). Without it, a config listing three site-specific directories scans only those three. |
--api-url / --api-key | Submission endpoint and key (also settable in config or the PQCRYPTA_API_URL/PQCRYPTA_API_KEY env vars). |
--targets <t> | Replaces the config's scan_paths (or the built-in defaults) for this run — the given targets become the authoritative list, not an addition to it. |
--deep-scan | Slower, more thorough traversal. |
--algorithms <list> | Comma-separated algorithm filter — report only matching assets. Same as algorithms_filter in the config. |
--no-binary-scan | Skip reading binaries for embedded crypto libraries for this run. Reported as a gap in coverage, never as a clean result. |
--full-disk-scan | Add the filesystem root (/ or C:\). The built-in exclusions (pseudo-filesystems, temp dirs, WinSxS) still apply, so this widens the walk without churning through them — but if your config overrides excluded_paths, make sure it keeps them. |
--output <file> | Write raw JSON instead of submitting. |
--offline-report <dir> | Air-gap bundle: report.html + cbom.json + inventory.json, no network. |
--submit-batch-size <n> | Assets per submission request (default 100). Lower it only for a link that cannot carry the larger request bodies. |
--submit-concurrency <n> | Submission requests in flight at once (default 6). Lower it to ease load on a busy API server. |
--allow-bulk-retirement | Confirm that assets missing from this scan really are gone. A scan reports an inventory, and anything a device previously reported but no longer does is retired — but a scan run with narrowed scan_paths or --targets looks identical to a device that lost those assets. When a scan would retire more than half of what a device previously reported, the server refuses and leaves the stored inventory standing; this flag is how you say the assets really are gone. Small, ordinary retirements always proceed without it. Judged per device rather than per scan, so an appliance-only run is measured against that appliance's own store and not against the scanning host's untouched filesystem inventory. |
--verbose | Debug logging (including per-connection TLS timing). |
4. Configuration Reference
The agent is fully config-driven. With --config, that file's scan_paths are
exactly what gets scanned — nothing hidden. With no --config it falls back to a built-in
per-OS default set, which is the same reference-{linux,windows,macos}.toml you can download
and edit. Start from a role preset and adjust.
| Key | Type | Purpose |
|---|---|---|
api_url / api_key | string | Submission target (omit for --output/--offline-report). |
scan_paths | [string] | Directories/files to scan for certs, keys, SSH keys, keystores. Authoritative. |
binary_scan | bool | Read executables and shared objects for the crypto library compiled into them (default true). The only surface that sees inside a closed-source vendor binary — and the only one whose cost scales with installed software rather than deployed cryptography, since it reads the string sections of every binary in scope. Set false where that budget does not exist; coverage then reports it as not configured, never as clean. |
excluded_paths | [string] | Trees to skip. The built-in catalog already excludes pseudo-filesystems and temp dirs (plus WinSxS on Windows); setting this key replaces that list rather than extending it, so keep those entries when you override it. |
deep_scan | bool | Thorough traversal. |
code_scan_paths | [string] | Source trees to scan for weak-crypto calls (CBOM). Off by default. |
[[network_targets]] | table | host + port — a live TLS handshake inspecting the cert actually served. Ports that begin in plaintext and upgrade (SMTP 25/587, IMAP 143, POP3 110) are negotiated via STARTTLS automatically; implicit-TLS ports (443/465/993/995) handshake directly. |
[[database_connections]] | table | name, db_type, dsn — parses cert/key column values, never storing the raw value. |
[[appliance_targets]] | table | F5/NetScaler management-API scan (§6). Keys: name, appliance_type (f5 or netscaler), mgmt_url, username, password, verify_tls. |
excluded_tables | [string] | Per [[database_connections]]. Tables to skip entirely. Point this at the inventory tables of a database that stores this tool's own results (crypto_assets, asset_scans) — every column in those reflects something already discovered elsewhere, so scanning them yields duplicate self-referential findings rather than new information. |
excluded_columns | [string] | Per [[database_connections]]. Bare column names skipped across all tables, for when only specific columns are self-referential rather than a whole table. |
submit_batch_size | int | Assets per submission request (default 100). Request-body size is the one submission limit that depends on the scanned material, so a host full of large certificate chains may want a smaller value. |
submit_concurrency | int | Submission requests in flight at once (default 6). |
keystore_passwords | [string] | Extra passwords tried against locked PKCS#12/PFX/JCEKS containers, on top of the built-in defaults (empty, changeit, password). Order does not matter and none are mapped to a specific store — the first that opens a container wins. A password is never submitted to the API, never logged, and never leaves the machine; only the already-cleartext certificate metadata inside is reported. A container that stays locked is reported as a gap in coverage rather than a clean result — its risk is raised and the finding names this setting as the fix, because an unopened keystore and an empty one otherwise look identical in an inventory. |
keystore_passwords_file | string | Path to a newline-separated password file (blank lines and # comments ignored), so the main config stays shareable while secrets sit in a tighter-permissioned file. |
keystore_passwords_command | string | Command whose stdout supplies passwords — e.g. aws secretsmanager get-secret-value --query SecretString --output text, op read op://vault/item/password, or gpg -d passwords.gpg. Resolved once at startup. A fourth source, the PQCRYPTA_KEYSTORE_PASSWORDS env var (newline-separated), merges with all of the above. |
algorithms_filter | [string] | Report only assets matching these algorithms. Equivalent to --algorithms. |
5. What It Scans
Nine distinct sources, each real and tested:
- Filesystem certs & keys — PEM/DER/CRT/CER and RSA/ECC/Ed25519 private keys, parsed for full X.509 detail. Multi-certificate bundles are expanded entry by entry, including OpenSSL's trust-augmented form (
BEGIN TRUSTED CERTIFICATE) thatca-bundle.trust.crtandobjsign-ca-bundle.pemare made of. Pending certificate requests (PKCS#10.csr/x509Req.pem) and standalone key-exchange parameter files (DH groups, named EC curves) are inventoried too — a CSR is a certificate mid-issuance, so it is the cheapest thing you own to make quantum-safe, and a parameter file fixes the group every handshake that loads it negotiates in. - Native OS certificate store — on Windows, the CryptoAPI stores (
MY,Root,CA,TrustedPublisher, in bothLocalMachineandCurrentUser— whatcertmgr.mscshows); on macOS, the System and login Keychains. Runs automatically on a matching platform, no configuration: it is not a path you choose but part of the OS. - SSH keys — host and user keys (openssh-key-v1, authorized_keys, known_hosts), picked up from
.sshdirs automatically. - Keystores — JKS/JCEKS/PKCS#12/BKS/UBER/Oracle Wallet/NSS, opened and each entry inventoried at
store#alias. - Live TLS endpoints — a real handshake against
host:port, inspecting whatever certificate is actually being served right now. - Database columns — sampled cert/key column values are parsed for structural metadata, then discarded (never stored).
- Appliance stores — F5/NetScaler certificate stores over their management APIs (§6).
- Binaries (embedded crypto libraries) — executables and shared objects are searched for the version banner a crypto library compiles into itself, so a statically linked or vendored stack is identified without source, symbols, or a package manager. This is the only source that sees inside a closed-source vendor binary: OpenSSL, LibreSSL, BoringSSL, GnuTLS, Libgcrypt, mbedTLS, wolfSSL, NSS and libsodium are recognised across ELF, PE and Mach-O. It answers a question the host cannot answer about itself —
openssl versionreports the command-line tool, which is routinely not the library your applications actually load, andldconfigproves only that a library exists somewhere, with no version and no link to the binary using it. Findings are graded on post-quantum capability rather than as algorithms: OpenSSL 3.5+ carries ML-KEM/ML-DSA/SLH-DSA and is marked compliant, 3.0–3.4 is supported but cannot negotiate PQC, and the out-of-support 1.1.1 series is flagged critical. Detection is by embedded banner, so a stripped, renamed or packed binary can still hide one — a finding here is evidence of presence, never proof of absence. - Source code (CBOM) — calls into weak primitives (MD5, SHA-1 for security, DES/3DES, RC4, ECB, undersized RSA keygen).
Every asset is then classified by post-quantum status, risk, and remediation ownership — who can actually fix it (you, a public CA, an OS vendor, a package manager, or a hardware vendor). Ownership is not decided by location alone: a copy of a public CA certificate is still the CA's wherever it sits on disk, so a final pass checks the certificate itself — roots by SHA-256 against the known trust anchors, intermediates by whether they chain to one. That pass only ever moves work off your list; it never marks something actionable that the location rules did not. Five compliance statuses are possible: compliant (standardized PQC or symmetric), legacy (classical and quantum-vulnerable — RSA, ECC, Ed25519, DH), deprecated (quantum-resistant but pre-standard, e.g. round-3 Dilithium on the pre-FIPS OID arcs), forbidden (broken regardless of the quantum timeline — MD5, SHA-1, DES, RC4, ECB, DSA, sub-2048-bit RSA), and unknown (an algorithm the scan could not determine, such as a passphrase-protected key, reported as unknown rather than guessed).
6. F5 / NetScaler Appliances
F5 BIG-IP and Citrix NetScaler are closed devices — no agent runs on them. Instead the agent queries each device's own management API remotely, from any host that can reach the management interface (a jump host is typical):
- F5 BIG-IP — iControl REST (
/mgmt/tm/sys/file/ssl-cert) - NetScaler / ADC — NITRO API (
/nitro/v1/config/sslcertkey)
A read-only account is sufficient (F5 Auditor role / NetScaler read-only command policy). Beyond the certificate store, the agent collects the device's own facts — software version, platform (Virtual Edition detection), CPU/memory, management IP, self-IPs, subnets, gateway, and interfaces with MACs — so each appliance appears as its own first-class device in the dashboard. It also resolves, per certificate, which virtual servers actually serve it, or flags it as an unbound spare — the difference between a live cert and a forgotten one, which a live TLS scan of the VIP can't see. Factory certs (F5's ca-bundle/f5-irule/default, NetScaler's ns-server-certificate) classify as hardware-vendor; certs your org installed classify as internal.
[[appliance_targets]]
name = "F5 BIG-IP Production"
appliance_type = "f5" # "f5" or "netscaler"
mgmt_url = "https://f5-mgmt.example.com"
username = "readonly-user"
password = "your-password-here"
verify_tls = true # false only for a self-signed mgmt certnsroot password is randomly
generated per container — read it with docker exec <container> cat /var/deviceinfo/random_id.7. Offline / Air-Gap Mode
For isolated or classified networks, one flag produces a complete assessment entirely on-host with zero network access — no API key, no callback:
./pqcrypta-discovery --config agent-config.toml --offline-report ./assessmentThis writes a self-contained bundle to ./assessment/:
| File | What it is |
|---|---|
report.html | A single self-contained compliance report (all styling inlined, no external requests) — safe to open on an air-gapped host. Discovery coverage (which of the nine surfaces were assessed, and any location the scan was refused), post-quantum readiness, risk breakdown, remediation ownership, full inventory. |
cbom.json | A CycloneDX 1.6 Cryptographic Bill of Materials, validated against the published bom-1.6.schema.json. assetType selects the shape: certificates carry certificateProperties, key material relatedCryptoMaterialProperties, and source-code findings algorithmProperties. metadata.component names the scanned host, so the file identifies its own subject if it travels separately from the rest of the bundle. Every bom-ref is unique as the spec requires, scoped by location — one certificate legitimately appears at many paths on a host (each vendored cacert.pem, the OS trust store, and the live endpoint serving it), so each occurrence is its own component, carrying evidence.occurrences[].location and a shared pqcrypta:sha256 that correlates them back to one certificate. |
inventory.json | Every asset with full detail and its local assessment, plus a coverage object stating which discovery surfaces stand behind that list and every location the scan could not read — so a consumer ingesting the file knows its scope, not just its contents. Field-by-field breakdown → |
7a. Self-Hosted / On-Prem Platform
Between hosted SaaS and single-binary offline mode is a third option: run the entire platform — API, PostgreSQL, and the Compliance Dashboard — inside your own network, and point agents at it. No inventory, CBOM, or telemetry leaves your environment.
./pqcrypta-discovery --config agent-config.toml --api-url http://your-host:3003The agent is unchanged — only the target URL differs. The self-hosted tier ships as a
docker compose stack (database + API + dashboard) with both a connected installer and a
fully air-gapped bundle (images + schema + signed agent artifacts staged into one tarball, zero registry
pulls on the destination). The dashboard's API base URL, CSP connect-src, and download links
all follow your local API.
| Piece | Where |
|---|---|
| Deployment package | deploy/onprem/ — compose, Dockerfiles, .env.example, DB schema, scripts, README |
| Connected install | docker compose up -d --build after filling .env and dropping your license.json |
| Air-gap install | scripts/build-airgap-bundle.sh → carry tarball → scripts/install-airgap.sh |
| Point the API base | PQCRYPTA_API_BASE (dashboard) and --api-url (agents) |
8. Output & CBOM
However you run it, the inventory maps to standards-based output. In offline mode you get the
CycloneDX 1.6 CBOM directly (cbom.json), covering exactly the host that was scanned.
Online, the dashboard's Reports tab and /crypto-assets/export/cbom emit
the same format across your whole estate, filtered by any combination of source, device, algorithm,
certificate role, compliance status, and more. CSV and JSON report exports share the identical filter
set, so a selection always describes the same rows.
The two are the same standard and the same assessment, not the same bytes: the online export keys
bom-ref on the stored asset id, while the offline CBOM — which has no database
behind it — keys on fingerprint plus location. Both link every asset to a shared algorithm
component through the dependencies graph, and both put the quantum verdict in the
spec's own nistQuantumSecurityLevel (0 for anything a quantum computer
breaks), so a reader that has never heard of PQCrypta can assess either file. Both
carry the certificate's SHA-256 in the standard hashes field, over the complete DER, so
the same certificate is identifiable across the two and against openssl x509 -fingerprint
-sha256.
9. Security & Least Privilege
- Read-only. The agent never writes to, modifies, or deletes anything it scans.
- No secrets leave the host in offline mode. Database column values and private-key secret material are never transmitted; only structural metadata (algorithm, size, expiry) is kept.
- Appliances need only a read-only role (F5 Auditor / NetScaler read-only command policy).
- Run as an unprivileged user where possible — it only reads the paths you list. Elevated rights are needed only to read protected key directories.
- Credentials in config are plaintext —
chmod 600any config holding appliance or database passwords.
10. Troubleshooting
| Symptom | Cause / fix |
|---|---|
| "Specify one of --api-url, --output, or --offline-report" | No mode chosen — pass one of the three (§3). |
| Appliance scan returns HTTP 401 | Wrong credentials, or (F5/NetScaler) a freshly created account whose password is flagged expired — reset it once. For CPX, read the generated nsroot password (§6). |
| Appliance scan can't connect | Run from a host that can reach the management interface; set verify_tls = false only for a self-signed mgmt cert. |
| Scan finds nothing | scan_paths is authoritative — confirm the paths exist and aren't all under excluded_paths. A path named with --targets is scanned even if a built-in exclusion covers it. |
| Scan finds far less than expected | A --config file replaces the built-in catalog. If yours lists only site-specific paths, everything the product knows about by default is out of scope — re-run with --with-defaults to scan both. On one real host the difference was 2,047 assets against 6,224, including 253 private keys the configured scan never saw. |
| Too much noise | Add to excluded_paths (build trees, vendored dependencies). Package caches named InstallerCache, PackagesCache, DownloadCache and SoftwareDistribution are already pruned wherever they appear — every certificate in one is a duplicate of one found where it is deployed. Remember excluded_paths replaces the built-in list, so restate the pseudo-filesystems and temp dirs when you set it. |
| One appliance is unreachable but the rest matter | That's fine — an unreachable appliance only warns; the rest of the scan completes. |