Operator Runbook
Overview
This runbook covers operational procedures for the eBPF runtime reachability evidence collection system (StellaOps.Signals.Ebpf).
Implementation status (verify against
src/). The collection library (probe loading, NDJSON evidence writer, DSSE/Rekor chunk signing, chain verification, symbol/container/BTF resolution) is implemented insrc/Signals/__Libraries/StellaOps.Signals.Ebpf/. The kernel-attach path inCoreProbeLoaderis currently a simulated ring buffer (SimulatedRingBuffer) — it does not yet attach real probes via libbpf, so on-host CPU-overhead and ring-buffer-utilization figures are placeholder values (GetCpuOverheadreturns a fixed0.1,Utilizationreturns0.1). Treat the live-host collection sections below as the target operational model; the operator-facing surface that exists today is thestella signalsCLI (inspect,list,summary,verify-chain) and the in-process collection API.
Collection surfaces (what actually exists)
| Surface | Where | Notes |
|---|---|---|
stella config signals inspect/list/summary | src/Cli/StellaOps.Cli/Commands/SignalsCommandGroup.cs | Discoverable but not implemented: exit 9, no signal rows, until a persisted query client is wired. |
stella config signals verify-chain | same | Verifies a signed evidence chain on disk (real logic; see below). |
| In-process collection API | IRuntimeSignalCollector / RuntimeEvidenceCollector | StartCollectionAsync / StopCollectionAsync / GetStatisticsAsync. Wired via AddEbpfRuntimeEvidence(...). |
| DI registration | ServiceCollectionExtensions.AddEbpfRuntimeEvidence / AddEbpfRuntimeEvidenceAirGap | Configured through EbpfEvidenceOptions (see Configuration Reference). |
Auth scopes for the Signals API (StellaOpsScopes.cs): signals:read, signals:write, signals:admin.
FLAG — not implemented. A standalone
stellaops-signalsdaemon / systemd unit, astella signals start|stop|status|health|statscommand set, and a Prometheus/metricsendpoint for the collector are not present in the codebase. The earlier draft of this runbook described those surfaces; they have been removed or annotated as roadmap below. Re-add concrete procedures here only after the corresponding code lands.
Monitoring
Available instrumentation
The Signals service (src/Signals/StellaOps.Signals/) emits OpenTelemetry metrics only for the “unknowns” decay/rescan pipelines, via the meters StellaOps.Signals.Decay and StellaOps.Signals.Rescan:
| Metric | Source meter | Description |
|---|---|---|
stellaops_unknowns_decay_subjects_processed_total | StellaOps.Signals.Decay | Subjects processed by the decay job |
stellaops_unknowns_decay_unknowns_processed_total | StellaOps.Signals.Decay | Unknown findings processed |
stellaops_unknowns_decay_band_changes_total | StellaOps.Signals.Decay | Confidence-band transitions |
stellaops_unknowns_decay_batch_duration_seconds | StellaOps.Signals.Decay | Decay batch duration histogram |
stellaops_unknowns_rescans_triggered_total | StellaOps.Signals.Rescan | Rescans triggered |
stellaops_unknowns_rescans_succeeded_total / _failed_total | StellaOps.Signals.Rescan | Rescan outcomes |
stellaops_unknowns_{hot,warm,cold}_batches_processed_total | StellaOps.Signals.Rescan | Batch tier counters |
stellaops_unknowns_rescan_duration_seconds / _batch_duration_seconds | StellaOps.Signals.Rescan | Rescan duration histograms |
stellaops_unknowns_band_{hot,warm,cold}_count | StellaOps.Signals.Rescan | Observable gauges: current count of unknowns in each confidence band (set via UnknownsRescanMetrics.SetBandDistribution) |
FLAG — orphaned (roadmap). The eBPF collector does not currently expose Prometheus metrics. The in-process API surfaces equivalent counters as return values from two distinct stats records:
SignalStatistics(fromRuntimeSignalCollector.GetStatisticsAsync):TotalEvents,EventsPerSecond,DroppedEvents,BufferUtilization,CpuOverheadPercent,MemoryUsageBytes,UniqueCallPaths.EvidenceCollectionStats(fromRuntimeEvidenceCollector.GetStatsAsync):TotalEvents,ProcessedEvents,DroppedEvents,EventsPerSecond,BufferUtilization,CpuOverhead,MemoryUsage(note the shorter field names —CpuOverhead/MemoryUsage, not the*Percent/*Bytesforms — and that it has noUniqueCallPathsbut addsProcessedEvents).There are no
stellaops_signals_events_total,stellaops_signals_ringbuf_usage,stellaops_signals_drops_total,stellaops_signals_enrich_latency_p99,stellaops_signals_chunks_signed, orstellaops_signals_rekor_failuresmetrics in the codebase. Do not alert on those names.
Statistics that the collection API reports (when collection is running):
TotalEvents/EventsPerSecond— throughput counters.DroppedEvents— incremented by the rate limiter whenMaxEventsPerSecondis exceeded (seeRuntimeSignalCollector.ProcessEventsAsync).BufferUtilizationandCpuOverheadPercent— fixed simulated values from the probe loader (SimulatedRingBuffer.Utilizationreturns0.1,CoreProbeLoader.GetCpuOverheadreturns0.1; see status note above).MemoryUsageBytes— returns the configured ring-buffer size (CoreProbeLoader.GetMemoryUsage→SimulatedRingBuffer.Size, i.e.RuntimeSignalOptions.RingBufferSize), not a measured RSS figure.
Chain-state inspection
A running collector writes per-chain state files (chain-<key>.json) into the configured ChainStateDirectory and signed chunk sidecars (*.dsse.json) alongside NDJSON chunks (evidence-<yyyyMMddHHmmss>-<seq:000000>.ndjson[.gz]). Verify integrity with the CLI:
# Verify a signed evidence chain on disk (looks for *.dsse.json sidecars)
stella config signals verify-chain /var/lib/stellaops/evidence/
# Verbose, offline (skip Rekor), with a JSON report written out
stella config signals verify-chain /var/lib/stellaops/evidence/ --offline --verbose --report report.json
Common Issues
Issue: Probe failed to attach
Context. Applies once the live libbpf attach path is enabled. With the current simulated loader,
CoreProbeLoader.LoadAndAttachAsyncthrowsPlatformNotSupportedExceptionon non-Linux,FileNotFoundExceptionwhen the probe object (function_tracer.bpf.o) is missing from the probe directory, orInvalidOperationExceptionwhen no PIDs are found for the target container.
Symptoms (target model):
PlatformNotSupportedException: eBPF probes require Linux.
FileNotFoundException: eBPF probe object not found: /usr/share/stellaops/probes/function_tracer.bpf.o
Diagnosis:
# Confirm the probe object is present in one of the search locations
ls -l /usr/share/stellaops/probes/ /opt/stellaops/probes/
# Check BPF kernel config
grep CONFIG_BPF /boot/config-$(uname -r)
# Check BTF availability (collector resolves a BTF source on startup)
ls -l /sys/kernel/btf/vmlinux
# Check seccomp/AppArmor denials
dmesg | grep -iE "bpf|seccomp|apparmor"
Resolution:
- Ensure the compiled probe object is installed under one of the search paths (
/usr/share/stellaops/probes,/opt/stellaops/probes, or<AppContext.BaseDirectory>/probes). - Ensure the host process has the capabilities needed to load BPF programs (
CAP_BPF,CAP_PERFMON) or run as root. - Confirm a BTF source is resolvable — the collector requires one. Precedence (see
RuntimeBtfSourceSelector): kernel BTF (/sys/kernel/btf/vmlinux) → configured externalvmlinux→ split-BTF directories (/var/lib/stellaops/btf/split,/usr/share/stellaops/btf/split,/usr/lib/stellaops/btf/split). If none is found,IsSupported()returns false and collection throwsPlatformNotSupportedExceptionwith reasonno_btf_source_found.
Issue: Events dropped (rate limiting / buffer pressure)
Symptoms:
DroppedEventsclimbs inGetStatisticsAsyncoutput.
Cause. Events are dropped when the per-second rate limiter rejects them (MaxEventsPerSecond), or when the in-memory event queue exceeds its 100,000 cap and old events are trimmed (RuntimeSignalCollector.ProcessEventsAsync). The simulated ring buffer is a bounded channel with DropOldest semantics.
Resolution:
- Raise
MaxEventsPerSecondonRuntimeSignalOptions(default10000; there is no “unlimited” sentinel in code — set it high if needed). - Increase
RingBufferSizeonRuntimeSignalOptions(default262144bytes = 256 KB). - Narrow the trace scope with
TargetSymbolsand/or raiseSampleRate(default1= every call;10= every 10th call) to reduce volume.
Issue: High memory usage
Symptoms:
- Large RSS, OOM pressure on the collector host.
Diagnosis / contributing factors:
- Symbol resolution cache (
EnhancedSymbolResolver): per-process LRU with a 5-minute sliding TTL, capped viaEbpfEvidenceOptions.SymbolCacheSizeLimit(default100000). - In-memory event queue trimmed at 100,000 events.
Resolution:
- Lower
EbpfEvidenceOptions.SymbolCacheSizeLimit. - Reduce
RuntimeSignalOptions.MaxStackDepth(default16) to shrink per-event payloads. - Disable symbol resolution (
RuntimeSignalOptions.ResolveSymbols = false) if address-only evidence is acceptable.
Issue: Symbol resolution failures
Symptoms:
FunctionCallEvent.Symbol/Librarycome back null; only the rawaddris present in evidence.
Diagnosis:
# Check if the binary carries symbols
nm /path/to/binary | head
# Check if debuginfo is present (not stripped)
file /path/to/binary | grep -i "not stripped"
Resolution:
- Install debug symbols for the target binaries/libraries:
# Debian/Ubuntu apt install libc6-dbg # RHEL/CentOS debuginfo-install glibc - Accept address-only evidence. Unresolved calls still produce a
FunctionCallEventwithaddr; downstream correlation can still use the call-stack hash.
Issue: Container resolution failures
Symptoms:
- Evidence records have no
container_idenrichment, or the cgroup path does not match a known runtime.
Context. CgroupContainerResolver resolves container identity locally from /proc/<pid>/cgroup and /sys/fs/cgroup, matching containerd, Docker, CRI-O, and Podman scope paths. The emitted identity uses the form {runtime}://{id} (e.g. containerd://<64-hex>, docker://<64-hex>, cri-o://<64-hex>, podman://<64-hex>). This is local introspection — it does not require a runtime resolver such as Zastava.
Diagnosis:
# Inspect the cgroup path the resolver parses
cat /proc/<pid>/cgroup
# Confirm the container runtime
docker ps; crictl ps
Resolution:
- Confirm the cgroup driver/path matches one of the supported scope patterns (the resolver expects a 64-hex container id in the scope name).
- Image-digest and SBOM-component enrichment (
IImageDigestResolver,ISbomComponentProvider,IContainerStateProvider) is optional and, where wired, comes from a runtime state provider such as Zastava. If only digest enrichment is missing (not the container id), check that provider — not the cgroup resolver.
Issue: Evidence chain verification failure
Symptoms (from stella config signals verify-chain):
Evidence Chain Verification
===========================
Path: /var/lib/stellaops/evidence/
Chunks: 43
Mode: Online
Results:
Passed: 42
Failed: 1
Chain Status: ✗ INVALID
With --verbose, each failing chunk lists its errors, e.g. Chain broken: expected previous_chunk_id=..., got=..., Sequence gap: ..., Time overlap: ..., or No signatures found in envelope.
Diagnosis:
# Detailed report (per-chunk errors) + machine-readable output
stella config signals verify-chain /var/lib/stellaops/evidence/ --verbose --format json
The verifier checks (see SignalsCommandGroup.BuildVerifyChainCommand and EvidenceChunkFinalizer.VerifyChainAsync):
- chain linkage (
previous_chunk_idmatches the prior chunk’schunk_id), - sequence continuity (
chunk_sequenceincrements by 1), - time monotonicity (no overlap between chunk time ranges),
- presence of DSSE signatures.
Note. In offline mode the CLI verifies structural integrity only; full cryptographic signature verification requires the signing keys and is performed by the signer (
IEvidenceChunkSigner.VerifyAsync).
Resolution:
- Check for missing or out-of-order
*.dsse.jsonchunk sidecars. - Check for disk corruption / truncated chunks.
- If a gap is the result of an intentional restart, document it in the audit trail. The collector reloads chain state from
chain-<key>.jsonon restart (EvidenceChunkFinalizer.LoadChainStateAsync), so a clean restart continues the chain rather than breaking it.
FLAG — orphaned. There is no
stella signals reset-chaincommand. To start a fresh chain, point the collector at a new/emptyChainStateDirectory+OutputDirectory.
Issue: Rekor submission failures
Context. Transparency-log submission is performed by the production signer AttestorEvidenceChunkSigner, which routes through the internal Attestor service (IAttestationSigningService + IRekorClient / RekorBackend) — not by the collector calling a public Rekor endpoint directly. The default Rekor backend URL configured in Attestor is https://rekor.sigstore.dev, but it is configurable (e.g. an internal mirror) for on-prem/air-gap deployments. The local/dev signer (LocalEvidenceChunkSigner) signs the DSSE PAE with an HMAC (via ICryptoHmac; the algorithm follows the active compliance profile — HMAC-SHA256 on world/fips/kcmvp/eidas, HMAC-GOST3411 on gost, HMAC-SM3 on sm) and never submits to Rekor.
Symptoms:
- Signed chunks have a null
RekorUuid/RekorLogIndexwhen submission fails or is disabled.
Diagnosis:
# Check the Attestor service health (it owns Rekor submission)
# (use your deployment's Attestor health/status surface)
Resolution:
- Verify connectivity from Attestor to the configured Rekor backend.
- To run without transparency-log submission, set
EvidenceChunkFinalizerOptions.SubmitToRekor = false(or useLocalEvidenceChunkSigner/NullEvidenceChunkSignerin non-production). Chunks are still signed (DSSE) and chained; only the Rekor receipt is absent.
FLAG — orphaned. There is no
stella signer statuscommand and nostella signals resubmit-pendingcommand in the CLI.key_id: fulciofrom the earlier draft is not how signing keys are selected — the key id is theSigningKeyIdpassed to the finalizer / the Attestor signing service.
Operational Procedures
Procedures that drove a standalone collector daemon (
stella signals start|stop,systemctl ... stellaops-signals,stella signals cleanup,export,import,manifest,resubmit-pending,init,reset-chain) described commands that do not exist in the codebase and have been removed. The procedures below use surfaces that exist today.
Procedure: Verify and archive an evidence directory
# 1. Verify chain integrity before archiving
stella config signals verify-chain /var/lib/stellaops/evidence/ --report pre-archive-report.json
# 2. Archive evidence (NDJSON chunks + *.dsse.json sidecars + chain-*.json state)
tar -czvf evidence-$(date +%Y%m%d).tar.gz /var/lib/stellaops/evidence/
# 3. Move to long-term storage (use your own offline/on-prem object store;
# do not assume a cloud bucket on an air-gapped host)
Procedure: Air-gap probe bundle handling
The air-gap loader (AirGapProbeLoader) consumes a sealed probe bundle (ebpf-probes.bundle, a zip with a manifest.json) or embedded resources, and verifies each probe’s SHA-256 against the manifest before use. Default bundle search locations: <AppContext.BaseDirectory>/ebpf-probes.bundle, /usr/share/stellaops/probes/ebpf-probes.bundle, /opt/stellaops/probes/ebpf-probes.bundle.
# Place the pre-built bundle where the loader looks for it
cp ebpf-probes.bundle /usr/share/stellaops/probes/
# Enable air-gap mode in the host that registers the collector
# services.AddEbpfRuntimeEvidenceAirGap(...);
AirGapProbeLoader.VerifyIntegrityAsync() recomputes each probe hash and reports mismatches; CreateBundleAsync(...) builds a bundle during CI for offline deployment.
Procedure: Verify an exported evidence chain offline
# On the air-gapped verifier, after transferring the evidence directory:
stella config signals verify-chain --offline /imported/evidence/ --verbose
Offline mode skips Rekor lookups and verifies chain/sequence/time/signature- presence structurally (see the verification issue above for the full check set).
Configuration Reference
The collector is configured in code via EbpfEvidenceOptions (passed to AddEbpfRuntimeEvidence / AddEbpfRuntimeEvidenceAirGap). There is no free-standing signals: YAML config schema in the codebase; the YAML in the earlier draft (ring_buffer_size, max_events_per_second, filters.paths.allowlist, signing.key_id, metrics.port, etc.) did not map to any real option keys. The authoritative option types are listed below.
EbpfEvidenceOptions (DI registration)
| Property | Default | Meaning |
|---|---|---|
ProcRoot | /proc | procfs root for /proc introspection |
CgroupRoot | /sys/fs/cgroup | cgroup root for container resolution |
ProbeDirectory | (auto: /usr/share/stellaops/probes, /opt/stellaops/probes, <base>/probes) | compiled probe object location |
OutputDirectory | /var/lib/stellaops/evidence | NDJSON evidence output |
UseAirGapMode | false | use offline AirGapProbeLoader |
SymbolCacheSizeLimit | 100000 | symbol-resolver cache entry cap |
WriterOptions | see below | NDJSON writer/rotation options |
CollectorOptions | EventChannelCapacity = 10000 | internal event channel size |
BtfSelectionOptions | empty | extra vmlinux / split-BTF candidate paths |
NdjsonWriterOptions (rotation / output)
| Property | Default | Meaning |
|---|---|---|
MaxChunkSizeBytes | 104857600 (100 MB) | size-based rotation threshold |
MaxChunkDuration | 1 hour | time-based rotation threshold |
BufferSize | 65536 (64 KB) | write buffer |
UseGzipCompression | false | gzip chunks (.ndjson.gz) |
RuntimeSignalOptions (per-collection)
| Property | Default | Meaning |
|---|---|---|
TargetSymbols | [] | symbol patterns to trace (empty = all; not recommended) |
MaxEventsPerSecond | 10000 | rate limit (no “unlimited” sentinel) |
MaxDuration | null | collection time cap |
RuntimeTypes | Native, Node, Python | runtimes to instrument |
ResolveSymbols | true | resolve addresses to symbols |
MaxStackDepth | 16 | captured stack depth |
RingBufferSize | 262144 (256 KB) | ring buffer size |
SampleRate | 1 | 1 = every call, N = every Nth call |
EvidenceChunkFinalizerOptions (signing / chaining)
| Property | Default | Meaning |
|---|---|---|
SigningKeyId | default | key id used by the signer |
CollectorVersion | 1.0.0 | recorded in the predicate |
KernelVersion | null | recorded in the predicate |
SubmitToRekor | true | submit DSSE envelope to Rekor (via Attestor) |
ChainStateDirectory | null | where chain-<key>.json state is persisted |
HostId | null | recorded in the predicate |
Evidence record schema
NDJSON evidence records use schema runtime-evidence/v1 (RuntimeEvidenceRecord): ts_ns (ns since boot), src (probe name), pid, tid, cgroup_id, container_id ({runtime}://{id}, enriched post-collection), image_digest (sha256:..., enriched), comm, and a polymorphic event object (file_open, process_exec, tcp_state, net_connect, ssl_op, function_call). Signed chunks are wrapped in an in-toto DSSE statement with predicate type stella.ops/runtime-evidence@v1 and payload type application/vnd.in-toto+json.
Probe sources
The probes/event sources the parser recognises (EventParser, RuntimeEvidenceRecord.Source):
| Source string | Kind |
|---|---|
sys_enter_openat | tracepoint (file open) |
sched_process_exec | tracepoint (process exec) |
inet_sock_set_state | tracepoint (TCP state) |
uprobe:connect | libc connect/accept uprobe |
uprobe:SSL_read / uprobe:SSL_write | OpenSSL uprobes |
uprobe:function_entry | generic function-entry uprobe |
Supported probe types reported by CoreProbeLoader.GetSupportedProbeTypes() on Linux: Uprobe, Uretprobe, and Usdt (when /sys/kernel/debug/tracing/uprobe_events exists).
Support
For issues not covered here:
- Confirm behaviour against the source of truth:
src/Signals/__Libraries/StellaOps.Signals.Ebpf/andsrc/Cli/StellaOps.Cli/Commands/SignalsCommandGroup.cs. - Capture the output of
stella config signals verify-chain <dir> --verbose --format json. - Provide kernel and BTF context (
uname -a,ls -l /sys/kernel/btf/vmlinux). - Provide the relevant
EbpfEvidenceOptions/RuntimeSignalOptionsconfiguration (sanitized).
Related
- eBPF Reachability overview — quick start and architecture
- Evidence Schema — NDJSON record shape and determinism invariants
- Probe Reference — per-probe attach points and limitations
- Security Model — threat model, hardening, and compliance mapping
