Repository navigation
Encrypt the telemetry path, and stop it discarding data in silence - #41
Merged
Merged
Conversation
TLS_ENABLED was documented in three places and read by nothing; the ingest protocol had no reply frame, so four separate paths were dropping rows while every layer reported success.
| if not INGEST_TLS: | ||
| return raw | ||
| context = ssl.create_default_context(cafile=INGEST_CA or None) | ||
| return context.wrap_socket(raw, server_hostname=SERVER_HOSTNAME or SERVER_IP) |
| return filters | ||
| .filter((f) => f.field && f.value) | ||
| .map((f) => { | ||
| const value = /[\s"]/.test(f.value) ? `"${f.value.replace(/"/g, '\\"')}"` |
d3vhex
added a commit
that referenced
this pull request
Sep 8, 2026
Encrypt the telemetry path, and stop it discarding data in silence
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLS was documented, not implemented
TLS_ENABLED,TLS_CERTandTLS_KEYappeared in.env.example, in theproduction guide, and in the instructions
certs/generate_certs.pyprintswhen it finishes. No line of code read any of them. Setting
TLS_ENABLED=1produced a console that came up cleanly on plain HTTP while its operator had
every reason to believe it was HTTPS — login form and session cookie in clear
text, with no symptom at all.
That is the worst shape a security control can have: not missing, but
documented. A missing feature gets noticed the first time someone looks for
it; a documented one gets trusted.
core/tls.pymakes it real, on one rule: asking for TLS and not getting itis a startup failure, never a fallback. A certificate that cannot be read,
half a configuration, a key that does not match its cert — each refuses to
start and names the file.
SESSION_COOKIE_SECUREnow followsTLS_ENABLEDwhen unset. The two had toagree and nothing made them, so turning on TLS and leaving the flag alone gave
an HTTPS console whose cookie was still allowed onto plain HTTP.
Telemetry moves to a second port (5011) inside TLS. Two ports rather than a
flag on one: the existing protocol opens with a 4-byte length so its first
byte is always
0x00, a ClientHello opens with0x16, and telling them apartmeans reading a byte that cannot be handed back to the TLS layer. The
plaintext listener stays open and names each agent still using it, once each;
INGEST_TLS_REQUIRED=1closes it when the fleet has moved. Cutting straightto TLS would blackhole every un-rebuilt agent, and a host that stopped
reporting looks exactly like a host with nothing to report.
The ingest protocol had no reply
The agent marked rows sent when
sendallreturned — which means the bytesreached a socket buffer, and nothing more. A server that accepted a batch and
stored none of it was indistinguishable from one that stored all of it.
There is a receipt frame now, and three cases stay distinguishable: rows
stored (mark sent), an error (keep the rows, retry, say why), and no receipt
at all (an older server — mark sent, and record that it is belief). Collapsing
any two recreates the bug.
Four live data-loss bugs, found by making failure visible
The insertion failure had been printed behind
if debug. Moving it out turnedup four separate paths that were dropping rows while every layer above
reported success:
docker_containers— the agent ships a row per container per cycle, soone batch carries the same container several times;
UNIQUE(container_id)raised 1062 and the whole batch rolled back. Rows are inserted individually
now (a batch is fifty independent facts, not one transaction) and snapshots
take the last write.
registry_logs,process_events,security_audit— named inALLOWED_TABLESandDEDUP_TABLES, collected by the agent, and absent fromdb/init.sql. 7,788 rows held on one endpoint against a table that did notexist.
siem_events— the console's Clear button truncated the rows and leftevery ingest fingerprint behind, so each future row was skipped as a
duplicate of a row that no longer existed. One click permanently blinded the
table.
critical_files—check_permissionswrites a row only when a file'sowner, group or mode changed. It is a delta stream, and it was classified as
a server-side snapshot, so every batch deleted the twelve hundred rows it
held and kept the handful that had changed.
A rejected row now releases its dedup fingerprint. Without that, the batch
commits, the fingerprint survives, and the row can never be stored even after
the reason it failed is fixed.
The channel could starve itself
GET /config/rulestimed out at 5s against an agent that was connected,healthy, and shipping telemetry. The channel multiplexes control traffic and
stream payloads down one websocket behind one send lock, a screen capture has
no natural end, and nothing told an orphaned pump to stop — so it competed for
that lock for the life of the connection on behalf of nobody.
PING_INTERVAL_Swas also serving as the socket timeout, so one slow send could hold the lock
for thirty seconds against a five-second deadline.
Stream payloads are droppable and yield to a queued reply; control frames are
not. The socket deadline is its own constant. The server tells the agent when
a stream is gone, and the pump stops when its stream is closed under it.
The console has one visual language
Charts reached eleven pages — File Integrity, SOAR, Threat Intel, Assets,
Audit and Login logs, and Agent Detail's tabs. Every one derives from rows the
page already holds: a chart with its own request is a chart that can disagree
with the table beside it.
Two places deliberately have no chart, and say so in the code: OSV gives an
advisory id and no CVSS score, so there is no severity to plot; and
resource_usageis a snapshot table, so a CPU line would be drawn through asingle sample.
Tests
2,225 passing. New structural invariants: a table the agent deduplicates
locally can never be a server snapshot (this catches
critical_fileswithoutreading the collector), every table in
ALLOWED_TABLESmust have somewhere toland, and clearing a deduplicated table must clear its fingerprints.
The nine skips in
test_encrypted_column_widthsare gone — they were skippingbecause three tables had no schema at all, so the suite reported "nothing to
check here" about tables the agent was filling.