Reachability Runtime Ingestion Runbook
This runbook guides operators through ingesting runtime reachability evidence (EntryTrace, probes, Signals ingestion) and wiring it into the reachability evidence chain, both online and in air-gapped installations. It is written for operators of the Signals service and assumes the runtime-facts schema referenced under Prerequisites.
Source of truth. The endpoint, auth, payload, and storage facts below are reconciled against
src/Signals/StellaOps.Signals(Program.cs,Services/RuntimeFactsIngestionService.cs,Storage/*RuntimeFactsArtifactStore.cs,Routing/SignalsPolicies.cs) and the CLI insrc/Cli/StellaOps.Cli/Commands(SignalsCommandGroup.cs,ReachabilityCommandGroup.cs). When this doc andsrc/disagree, the code wins. Several legacy steps in earlier revisions referenced endpoints, headers, events, and CLI commands that do not exist insrc/; those are corrected or flagged inline below.
Imposed rule (policy intent — partially aspirational vs. current code): runtime traces must never bypass CAS/DSSE verification; ingest only CAS-addressed NDJSON with hashes logged to Timeline and Evidence Locker.
Reality check against
src/Signals: the operator-facing JSON/NDJSON ingest endpoints (§2 step 4) persist per-fact evidence to PostgreSQL (reachability_facts); they do not CAS-address the body, do not require DSSE on the trace, and do not emit Timeline events. CAS addressing (cas://reachability/runtime-facts/<hash>) and BLAKE3 verification exist only in the service-internal batch store (IngestBatchAsync, no HTTP surface — §2 step 2), and DSSE chains are produced/verified separately via the eBPF evidence pipeline +stella config signals verify-chain. Treat this rule as the target posture; the inline §4 “NOT IMPLEMENTED” flag tracks the missing Timeline wiring.
1. Prerequisites
- Services:
SignalsAPI,Zastava Observer(or other probes),Evidence Locker, optionalAttestorfor DSSE. - Reachability schema:
docs/modules/reach-graph/guides/function-level-evidence.md,docs/modules/reach-graph/guides/runtime-facts.md,docs/modules/reach-graph/schemas/evidence-schema.md. - Auth scopes (
src/Signals/StellaOps.Signals/Routing/SignalsPolicies.cs):signals:read— read facts/status/list.signals:write— ingest runtime facts, callgraphs, unions, beacons, execution evidence.signals:admin—POST /signals/reachability/recompute.- Scopes are carried in the OpTok
scopeclaim (Bearer). WhenSignals:Authority:AllowAnonymousFallbackis enabled (development/local-harness only), the service instead accepts a space-separatedX-Scopesheader; this fallback is off in production.
- CAS: configured runtime-facts artifact storage (
Signals:Storage, filesystem or RustFS driver). The service-internal batch store addresses objects atcas://reachability/runtime-facts/<hash>(see §5; note there is no batch HTTP endpoint — §2 step 2). The operator-facing JSON/NDJSON ingest endpoints persist per-fact evidence to PostgreSQL and do not exercise this CAS store. - Time sync: AirGap sealed-mode evidence (
Signals:AirGap:SealedMode) if sealed; otherwise NTP. When sealed-mode enforcement is enabled, every ingest endpoint returns503with{ "error": "sealed-mode evidence invalid", "reason": ... }until valid, fresh evidence is present (SignalsSealedModeMonitor).
2. Ingestion workflow (online)
- Capture traces from Observer/probes → NDJSON with one runtime-fact event per line. Each event carries
symbolId(required) plus optionalcodeId,symbolDigest,purl,buildId,loaderBase,processId,processName,socketAddress,containerId,evidenceUri,hitCount(default 1),observedAt, andmetadata{}. (Model:RuntimeFactEventinModels/RuntimeFactsIngestRequest.cs.) - Stage to CAS (batch path — service-internal only): the batch artifact store computes a BLAKE3-256 digest of the uploaded NDJSON, stores it immutably, and returns
cas://reachability/runtime-facts/<blake3-hex>. (RuntimeFactsIngestionService.IngestBatchAsync+RustFs/FileSystemRuntimeFactsArtifactStore.) Batch ingest refuses to run unless both a crypto-hash provider and the CAS artifact store are configured.Flag (NO HTTP SURFACE):
IngestBatchAsyncis not wired to any route inProgram.cs— there is noPOST /signals/runtime-facts/batch(or equivalent) endpoint. The batch CAS path is a service-internal capability exercised byRuntimeFactsBatchIngestionTests, not an operator-driven ingest path. Operators ingest runtime facts via the JSON or NDJSON endpoints in step 4 (which do not produce a batchcasUri/batchHash). Treat the batch CAS layout in §5 as a description of the internal store, not a runbook step you can drive over the API. - Optionally sign: DSSE signing of runtime evidence chunks is produced by the eBPF evidence pipeline (
src/Signals/__Libraries/StellaOps.Signals.Ebpf/Signing/), which emits*.dsse.jsonsidecars. There is nostella attest runtimeCLI command; do not script against one. - Ingest via the Signals API. Two endpoints exist (both require
signals:write):- Structured JSON —
POST /signals/runtime-facts:
Body:curl -H "Authorization: Bearer <optok>" \ -H "Content-Type: application/json" \ --data @runtime-facts.json \ "https://signals.example/signals/runtime-facts"{ "subject": { ... }, "callgraphId": "<id>", "events": [ ... ], "metadata": { ... } }. - Streaming NDJSON —
POST /signals/runtime-facts/ndjson:
The subject is supplied via query parameters (curl -H "Authorization: Bearer <optok>" \ -H "Content-Type: application/x-ndjson" \ -H "Content-Encoding: gzip" \ --data-binary @runtime-trace.ndjson.gz \ "https://signals.example/signals/runtime-facts/ndjson?callgraphId=<id>&scanId=<scan>"callgraphIdrequired; optionalscanId,imageDigest,component,version,purl— seeRuntimeFactsStreamMetadata). Gzip is decoded only whenContent-Encoding: gzipis set.
Correction: there is no
POST /api/v1/runtime-factsendpoint and nograph_hashquery parameter — the keying field iscallgraphId. The/api/v1/signalsgroup is read-only (GET /api/v1/signals,GET /api/v1/signals/stats); both acceptsignals:readororch:read, and (perCompatibilityApiV1Endpoints.cs) they serve a fixed five-row static fixture (sig-001…sig-005), not live runtime-fact data. Ingest is not tenant-header driven: handlers do not readX-StellaOps-TenantIdor the resolved tenant context (see the comment block inProgram.cs). Tenant attribution for emitted events comes from requestmetadata: thesignals.fact.updated.v1envelope resolves tenant frommetadata.tenantthenmetadata.subject.tenantthen the configured default (ReachabilityFactEventBuilder.ResolveTenant), while theruntime.updatedevent resolves it frommetadata.tenant_idthen"default"(RuntimeFactsIngestionService.EmitRuntimeUpdatedEventAsync). - Structured JSON —
- Read the response. On success the endpoints return
202 Acceptedwith aLocationof/signals/runtime-facts/<subjectKey>and a JSON body (RuntimeFactsIngestResponse):factId,subjectKey,callgraphId,runtimeFactCount,totalHitCount,storedAt.Correction: the response does not carry
Content-SHA256,X-Graph-Hash, orX-Ingest-Idheaders — those do not exist in the handler. Use the JSON body fields for downstream correlation. ThecasUri/batchHashfields belong toRuntimeFactsBatchIngestResponse, which is returned only by the service-internalIngestBatchAsync(no HTTP endpoint — see step 2 flag); the operator-facing JSON/NDJSON endpoints never return them. - Side effects on ingest (
RuntimeFactsIngestionService.IngestAsync): events are aggregated per(symbolId, codeId, loaderBase, purl, symbolDigest, buildId)key (summinghitCount); the fact document is upserted toreachability_facts, the Valkey reachability cache is refreshed, asignals.fact.updated.v1envelope is published, aruntime.updatedevent is emitted for policy reanalysis, and reachability is recomputed. - Verify the stored fact:
GET /signals/facts/{subjectKey}(requiressignals:read). To verify a signed DSSE evidence chain on disk, usestella config signals verify-chain <evidence-dir> --offline(see §5/§7).
3. Ingestion workflow (air-gap)
- Receive a runtime bundle containing the NDJSON trace, a manifest with hashes, and optional
*.dsse.jsonevidence chunks. - Validate hashes against the manifest; if DSSE chunks are present, verify the chain structure (see step 4).
- Import the trace through the same
POST /signals/runtime-facts/ndjsonendpoint while the installation is sealed. Sealed-mode enforcement (Signals:AirGap:SealedMode) gates every ingest endpoint with503until valid evidence is present, so confirm the seal is healthy first (GET /signals/statusreportssealedMode.compliant). - Verify the DSSE evidence chain offline:
This walksstella config signals verify-chain /path/to/evidence-dir --offline*.dsse.jsonsidecars, checks chunk linkage/sequence/time-monotonicity and signature presence, and exits non-zero on any failure (SignalsCommandGroup.BuildVerifyChainCommand).--offlineskips Rekor lookups.Correction: there is no
signals-offline ingest-runtimebinary and nostella graph verify --runtime ...command.stella reachability graph verifyexists only as a stub that returns “requires Attestor signature verification” with a not-implemented exit code (ReachabilityCommandGroup.BuildGraphCommand). Do not depend on either in offline runbooks. - Export the ingest receipt (the
202JSON body from the NDJSON endpoint —factId,subjectKey,callgraphId,runtimeFactCount,totalHitCount,storedAt) and add it to the Evidence Locker. (There is no operator-facing batchcasUri; the batch CAS path has no HTTP surface — see §2 step 2.)
4. Checks & alerts
- Sealed-mode gate: when air-gap enforcement is on, ingest and read endpoints return
503withreasonuntil sealed-mode evidence is valid and withinMaxEvidenceAge(default 6h). Treat persistent503as a stale/missing-evidence condition, not an ingest failure. - Hash mismatch (batch CAS, RustFS driver):
RustFsRuntimeFactsArtifactStorerecomputes the BLAKE3-256 digest of the upload, then re-reads the stored object and re-verifies; a mismatch throwsInvalidDataException. The filesystem driver (FileSystemRuntimeFactsArtifactStore) does not recompute or re-verify — it simply requires a non-blank hash and stores the body at the CAS path keyed by that hash. (Note: this only applies to the service-internalIngestBatchAsync; there is no batch HTTP endpoint — see §2 step 2.) - Validation failures: missing
symbolId, empty event list, blankcallgraphId, or an unresolvable subject raiseRuntimeFactsValidationException→400 Bad Requestwith{ "error": ... }.Flag (NOT IMPLEMENTED): earlier revisions referenced a
staleness_secondsfield,reach.runtime.ingested/runtime.ingest.failedTimeline events, and areachability.orphan_tracescounter. None of these exist insrc/Signals. The events actually emitted aresignals.fact.updated.v1(streamsignals.fact.updated.v1, DLQsignals.fact.updated.dlq) andruntime.updated. Re-add Timeline/metric wiring here only once it lands in code.
5. Storage and CAS layout
- Per-fact runtime evidence is stored in PostgreSQL (
reachability_facts) and cached in Valkey; the cache is invalidated/refreshed on each ingest. - Batch NDJSON artifacts (produced by the service-internal
IngestBatchAsync— no HTTP surface, see §2 step 2) are content-addressed by the CAS URIcas://reachability/runtime-facts/<hash>, where<hash>is the BLAKE3-256 hex digest (noblake3:prefix) of the exact (possibly gzipped) batch body. Filesystem layout:<RootPath>/cas/reachability/runtime-facts/<first2hex>/<hash>/runtime-facts.ndjson[.gz]. RustFS emits the same CAS URI but its object key omits thecas/reachability/prefix —<RuntimeFactsRootPrefix>/<first2hex>/<hash>/runtime-facts.ndjson[.gz]under the configured bucket (RuntimeFactsRootPrefixdefaults toruntime-facts), written immutable (X-RustFS-Immutable) with a default 90-day retention. - For deterministic replay, runtime traces are referenced from the ReachGraph replay manifest under the CAS namespace
runtime_traces(e.g.cas://runtime_traces/<hh>/<sha>.tar.zst; seedocs/modules/reach-graph/guides/runtime-facts.mdanddocs/modules/reach-graph/guides/replay-verification.md). This is the ReachGraph replay-side namespace and is distinct from thecas://reachability/runtime-facts/<hash>URI that the Signals batch store emits above. - A separate internal endpoint,
POST /api/v1/runtime/observations, ingests deployment-observation rows tagged withrelease_id(agent path, mTLS / internal network trust, anonymous fallback). It requires durable PostgreSQL and returns503otherwise. It is not the runtime-facts trace path and is out of scope for operator-driven trace ingest.
6. Troubleshooting
- 400 Bad Request: re-check the payload against §2 (required
symbolIdper event, non-empty events,callgraphIdpresent, subject resolvable). The error body’serrorfield states the exact validation failure.Flag (NOT IMPLEMENTED): there is no
scripts/reachability/validate_runtime_trace.pyvalidator in the repo. Validate against the field list indocs/modules/reach-graph/guides/runtime-facts.mdinstead. - 401 / 403: 401 means no valid bearer (and no
X-Scopesfallback) — present an OpTok withsignals:write; 403 means the token authenticated but lackssignals:write(orsignals:adminfor recompute). - 503 on ingest: sealed-mode evidence is missing/stale (air-gap), or the service is not yet ready (
/readyz). For the agent observation endpoint,503also means PostgreSQL persistence is not configured. - Batch digest mismatch (service-internal batch path / RustFS driver only — no operator HTTP surface): recompute the BLAKE3-256 digest of the exact bytes (gzip included if applicable) and compare to the manifest/
batchHash. - Missing symbols: ensure the referenced callgraph/union bundle is ingested first (
POST /signals/callgraphsorPOST /signals/reachability/union);callgraphIdmust resolve to a stored graph. Seedocs/modules/symbols/api-reference.mdfor the symbol surface.
7. Artefact checklist
- Runtime trace NDJSON (
.ndjson/.ndjson.gz) submitted via the JSON or NDJSON ingest endpoint (§2 step 4). (The batch BLAKE3-256casUri/batchHashis only produced by the service-internalIngestBatchAsync, which has no HTTP surface — see §2 step 2 — so it is not part of an operator-driven receipt.) - Optional DSSE evidence chunks (
*.dsse.json) verified withstella config signals verify-chain. - Ingest receipt: the
202JSON body (factId,subjectKey,callgraphId,runtimeFactCount,totalHitCount,storedAt). - Emitted events recorded:
signals.fact.updated.v1andruntime.updated. Evidence Locker record (bundle + receipt).
8. References
docs/modules/reach-graph/guides/DELIVERY_GUIDE.mddocs/modules/reach-graph/guides/function-level-evidence.mddocs/modules/reach-graph/guides/runtime-facts.mddocs/modules/reach-graph/schemas/evidence-schema.mddocs/modules/reach-graph/guides/replay-verification.mddocs/modules/symbols/api-reference.md
