Skip to content

Bureau — Overview

Signing Key Handling

Operators reasonably expect that system.shutdown() removes signing-key material from process memory. V8's string immutability makes that guarantee weaker than C-style memset_s – and the gap matters for regulated workloads (SCIF, healthcare, classified). This page documents the actual posture of Bureau key handling, the limits inherent to running inside V8, and the playbook operators must follow when they need provable zeroize.

The Privacy / Compliance review surfaced this gap with a one-line probe:

"Show me the disk dump from this server contains no signing-key material after shutdown()."

→ Pluck cannot answer this. V8 string immutability prevents in-place zeroize.


What shutdown() does today

When an operator calls system.shutdown() on any Bureau program:

  • Closure references to the PEM string are nulled, enabling the V8 garbage collector to reclaim the underlying string allocation on its next sweep.
  • Observability connections are torn down (devtools WebSocket, OTLP exporter, Sentry transport).
  • Pause-poll timers are cleared. The kill-switch sentinel watcher exits.
  • Resolvers are cancelled via the engine's cancellation tokens. In-flight Rekor publishes are aborted.
  • Output-dir lock is released so a follow-on workload can take it.

For most operators on most workloads this is sufficient. The signing key was loaded for the duration of the workload and is reclaimed shortly after shutdown.


What shutdown() does not do

The V8 runtime imposes hard limits on what an in-process JavaScript routine can guarantee about the memory layout of strings:

  • It does not in-place overwrite the PEM bytes. V8 strings are immutable, and the underlying allocation may have been copied across heap regions (young gen → old gen, internalized string table, JIT-compiled inline caches) during the workload, leaving multiple potentially-readable copies. Marking the closure null does not reach those copies.
  • It does not zero the V8 isolate's persistent caches. JIT-compiled code regions may retain references to the PEM through inline caches and feedback vectors that the GC cannot prove unreachable until a code-cache flush.
  • It does not coordinate with the OS page cache or swap. If the host paged the V8 heap to swap, the swapped pages contain the key until they are overwritten or the swap partition is wiped.
  • It does not protect against a memory dump taken DURING the workload. A gcore against a running process, a kernel crash dump, or a hypervisor introspection capture taken at any point between loadOperatorKey and shutdown() will include the PEM.

In short: shutdown() is a clean shutdown signal, not a memset_s-equivalent zeroize. Operators with regulated workloads must treat the key as resident in memory for the lifetime of the process, not just the lifetime of the system.


Crypto-shred playbook

For operators who need provable zeroize after a workload completes:

1. One workload per process

Run the Bureau program in a short-lived process – fork-exec, container, or systemd transient unit per workload. After shutdown(), exit the process with code 0. Linux frees the heap allocations on _exit(2) and the kernel reclaims the pages. The next workload runs in a fresh address space.

Shell
# Per-workload systemd transient unit
systemd-run --uid=bureau --gid=bureau --scope \
  node ./run-dragnet.mjs --workload-id "$WORKLOAD_ID"

This is the recommended path for the overwhelming majority of operators. It does not require kernel-level tooling, holds up against the threats most operators actually face, and aligns with the per-workload audit story Bureau already encourages.

2. Post-process kernel zeroize (SCIF / regulated)

For SCIF, classified, or other workloads where memory contents must be provably scrubbed before the host can be reused:

  • AMD SEV-SNP / Intel TDX. Run the workload inside a memory-encrypted enclave. After teardown, the enclave key is destroyed, rendering the encrypted memory pages unrecoverable even with physical RAM access. This is the gold-standard answer for "can the disk dump contain key material" – the disk dump is ciphertext under a key the platform has destroyed.
  • Linux kernel mem_encrypt + init_on_free=1. The kernel zeroes pages on free. Combined with one-workload-per-process and a swap partition encrypted with cryptsetup, this gets you to a similar guarantee without requiring SEV / TDX hardware.
  • Post-process shred utility. A small C utility that reads /proc/PID/maps (or a snapshot of the freed page set) and mlock + memset over the regions before exit. Out of scope for @sizls/pluck-bureau-core but trivial to compose.

3. Rotate the key after the workload

Even with the above, if the workload was a long-running daemon that loaded a single key for many hours, the right defensive move at shutdown is to rotate the operator key with the ROTATE program. The old key may have been captured from a hot core dump; the new key was never resident in the previous process. Pair shutdown with rotation for the tightest possible posture.


What we're working on

Tracked but not yet shipped:

  • Buffer-based key holding. Buffer instances can be zeroed in place (buf.fill(0) is a real memset on the underlying ArrayBuffer). The current loadOperatorKey returns a string PEM because node:crypto accepts strings everywhere. We're investigating an opt-in loadOperatorKey({ asBuffer: true }) mode that holds the PEM as a Buffer for the lifetime of the program and zeros it on shutdown. The node:crypto signing path can consume Buffers without converting back to string.
  • WebAssembly enclave for the signing path. A scheme where the Ed25519 private key is sealed inside a WebAssembly module's linear memory and never enters the V8 string heap. The signing operation crosses the Wasm boundary as a digest in / signature out, and the linear memory is freed (and zeroed by the Wasm runtime) on shutdown. Higher engineering cost; reserved for a future track once the Buffer path is shipped.

If you have an operational need for either of these and would help validate them, please open an issue against the pluck repo with your threat model.


See also

Edit this page on GitHub
Previous
Audit Trail of Dispatch

Ready to build?

Install Pluck and follow the Quick Start guide to wire MCP-first data pipelines into your agents and fleets in minutes.

Get started →