GitHub
11/08/2026, 1:46 PMsha1(host_identifier + enrollment_time_ns), with nothing random in it. Someone who knows a machine's hardware UUID (often in inventories) and roughly when it enrolled could recover the key by trying timestamps. With it they could pull that node's queries and send forged results.
2. Query results piled up in memory. QueryWriteHandler acknowledged every distributed-write request at once and processed the results in an unbounded goroutine. Each request can be up to 100 MiB. A fleet-wide query returning large results, say 5,000 nodes at 20 MB each, could hold all of them in memory at once and crash an osctrl-tls instance, losing every result it held. The log endpoint already had a byte budget; this one did not.
What changes
• generateNodeKey() returns 32 bytes from crypto/rand, hex-encoded (64 characters). Nothing about the host or the time goes into the key.
• QueryWriteHandler waits for room in a new 256 MiB in-flight budget (queryIngest) before any side effect: carve scheduling, the last-seen update, activity recording.
• The background processing releases the budget when it finishes.
• If the agent drops the connection while waiting, the handler answers 503 and nothing has changed.
• The budget is separate from the log budget, so large results can't starve log ingestion.
• One request (at most 100 MiB) always fits, so a request can never wait forever.
Security and operational impact
• New enrollments get keys that can't be derived. Existing nodes keep their current keys until they re-enroll. Re-enroll the fleet if those keys must be rotated.
• Under heavy result load, agents now wait at the HTTP layer instead of osctrl-tls growing in memory. The budget is a fixed constant, marked shortcut: in the code; make it a flag if a fleet needs more parallelism.
• No schema, API or configuration changes.
Validation
• TestGenerateNodeKey now checks for 64 hex characters and that two keys differ. It replaces the old test that pinned a fixed hash.
• New TestQueryWriteWaitsForIngestBudget checks two things:
• with the budget full, a request whose context ends gets 503;
• after a normal request, the background processing returns the whole budget.
• Both failed before the change.
• go test ./... passes, and go test -race ./cmd/tls/... is clean.
• Not verified: whether osquery resends distributed results after a 503. The 503 only happens once the agent has already gone away while waiting.
jmpsec/osctrlGitHub
11/08/2026, 1:52 PM