Skip to content

Add hot-path indexes and attribute osquery log batches to the authenticated node - #1072

Merged
javuto merged 1 commit into
developfrom
hot-path-indexes-osquery-nodes
Oct 5, 2026
Merged

javuto merged 1 commit into
developfrom
hot-path-indexes-osquery-nodes

Conversation

@javuto

@javuto javuto commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Add hot-path indexes and attribute osquery log batches to the authenticated node

Indexes

Adds 13 secondary indexes, each backing a query verified to run on every poll or check-in:

Table Columns Serves
osquery_nodes environment; hostname; localname stats, paged node list, node detail/lookup
node_queries query_id, status query completion check (osctrl-tls, per result)
distributed_queries environment_id, type, created_at query lists, active counts
osquery_status_data / osquery_result_data uuid, created_at node log pages, activity buckets
osquery_query_data name, created_at query results page, CSV export
audit_logs created_at audit page
carved_files environment_id, query_name carves list
tagged_nodes node_id; admin_tag_id tags per node page, tag counts

These are created by a new dbutil.EnsureIndexes helper rather than GORM struct tags. On MySQL, tagging an existing untagged string column flips its type from longtext to varchar(191), and AutoMigrate then ALTERs the column on every install. That rebuilds the table, and fails or truncates on values longer than 191 characters.

How the helper builds them:

  • Postgres: CREATE INDEX CONCURRENTLY, so writes are never blocked. An invalid index left by an interrupted build is dropped and rebuilt, unless another session is still building it.
  • MySQL: online DDL. A prefix length is applied only to columns that are TEXT on the live schema; a prefix on a short varchar is an error.
  • Where it runs: in the background at osctrl-api and osctrl-tls startup, never from osctrl-cli, where an exit mid-build would leave an invalid index. Log-table indexes are built when the logger DB is opened, since it can be a separate database.

last_seen, ip_address and bytes_received are deliberately left unindexed. Every check-in rewrites them, and keeping them out of indexes preserves Postgres HOT updates. A guard test enforces this.

Security: log batches trusted the sender-written hostIdentifier

osctrl-tls authenticates each log POST by node_key, but then attributed the batch to the hostIdentifier inside it, which the sender controls. Any enrolled node could:

  • overwrite another node's metadata (hostname, user, hashes) and inflate its bytes_received
  • plant status/result rows under another node's UUID in the DB log sink
  • fire alerts attributed to another node, or evade node-scoped rules targeting itself

ProcessLogs now takes the authenticated node and uses it for:

  • the metadata update, which is now UpdateMetadata(nodeID, …) by primary key
  • the UUID handed to every sink, including the S3 object key
  • DB log row attribution
  • alert attribution and node-scope checks; AlertMatcher.MatchResultLogs/MatchStatusLogs now take the sender's UUID

External sinks still receive the raw batch, whose entries contain the sender-written hostIdentifier. Consumers should key on the exporter-supplied identity.

Also fixed

Alert detail for status logs was chosen via Go map iteration. The node page's "errors reported by this node" preset therefore reported an empty detail ~25% of the time, and its test was flaky (8/30 failures). Fields are now matched in a fixed order (message → filename → version); 0/200 failures.

Validation

  • go build ./..., go vet ./... clean; all packages pass go test ./....
  • golangci-lint reports only a pre-existing goimports issue in pkg/queries/events_test.go.
  • New tests cover:
    • index creation: idempotence, table prefixes, continuing past a failed index, and every declared index building against its real model
    • the HOT guard on osquery_nodes
    • the end-to-end attribution regression (node ATTACKER sending a batch claiming VICTIM), mutation-checked to fail against the old sink behaviour
    • alert framing/evasion
  • Not yet run: the Postgres/MySQL index tests (CONCURRENTLY build, invalid-index rebuild, MySQL TEXT prefix). They are gated on OSCTRL_TEST_POSTGRES_DSN / OSCTRL_TEST_MYSQL_DSN; run with:
    go test ./pkg/dbutil/ ./pkg/nodes/ -run 'ExternalBackend|MySQLTextPrefix|InvalidPostgresIndex' -v

Notes for reviewers

  • On upgrade, index builds run once in the background; large log tables may take a while on first start. Builds never block writes.
  • The single-column uuid indexes on the status/result log tables are now redundant with the new (uuid, created_at) ones. They're left in place because dropping the struct tag would trigger the MySQL column-type ALTER described above.

@javuto javuto added ✨ enhancement New feature or request backend Backend related issues 🔐 security Security related issues labels Oct 5, 2026
@javuto
javuto merged commit ec27ae2 into develop Oct 5, 2026
7 checks passed
@javuto
javuto deleted the hot-path-indexes-osquery-nodes branch October 5, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend related issues ✨ enhancement New feature or request 🔐 security Security related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant