<#1147 Granular RBAC part 3: sensitive nodes> Pull...
# osctrl
g
#1147 Granular RBAC part 3: sensitive nodes Pull request opened by javuto Granular RBAC part 3: sensitive nodes Problem After part 2, query and carve tiers controlled which nodes a user could reach. But there was no way to keep machines like domain controllers, key-management hosts or executive laptops away from everyone except the most trusted responders. The spec calls these sensitive nodes, reachable only at the full tier (#704). Part 1 (global roles) merged in #1144 and part 2 (tiers and visibility) in #1146. What changes Sensitive tags.
AdminTag
gains
Sensitive bool
(column
sensitive
, not null, default false). • A node is sensitive if any live tag on it is sensitive. Deleting the tag, or removing it from the node, stops the node counting as sensitive. • Environment admins set the flag through the existing tag add/edit endpoint (
"sensitive": true
), which is already env-admin only. The tag action audit entry names the change. One filter.
handlers.NodeScope.Apply
now excludes sensitive nodes at the standard and tags tiers. Full is unchanged. • The exclusion is NULL-safe, and the boolean is bound as a parameter so it works on both sqlite and Postgres. • Every path built on the filter in part 2 picks this up: targeting, node lists, node detail, logs, posture, cancellation and hosted MCP. Query results. The log readers' include-only UUID list becomes
logging.NodeFilter{Only, Except}
. This applies to both the DB and S3 readers, for JSON and CSV. • At standard tier,
Except
lists every sensitive node UUID. This includes deleted sensitive nodes, which are matched through a new
ArchiveOsqueryNode.NodeID
column. • An empty
Except
drops nothing. An empty
Only
returns nothing. • Results are judged against the caller's lowest tier across the query's environment and every environment it targets. So a query in env A that targets env B can't show B's sensitive rows to someone who is only standard in B. Carves. At standard tier, files from sensitive nodes are hidden. Deleting a node keeps its tag rows, so a deleted sensitive node's carves stay hidden. Grants and pending work. • A sensitive tag can't be a scope tag (400
sensitive tag cannot be a scope tag
). • Marking a tag sensitive removes it from every scope grant. • Marking a tag sensitive, or tagging a node with a sensitive tag, cancels pending queries and carves from creators who can no longer reach that node. Env admins' and full-tier users' pending work is kept. Other routes. • Per-node activity and tiles stats now resolve by UUID only and return the same 404 for hidden and missing nodes. The batch versions drop hidden nodes. • The env error-node list is filtered. • The cross-environment node lookup (read-only admin) is filtered per environment. • Query and carve detail blank UUID/hostname targets at standard tier unless they name a live, non-sensitive node. Part 2 gap fixed. Node detail, logs and posture (through
visibleNode
), plus the unpaged active/inactive/all node lists, used to filter only at the tags tier. They now filter whenever the tier is below full. Docs and clients. OpenAPI is updated for the tag
sensitive
field. The frontend tag types gain
sensitive
(types only; the UI comes in part 5). The MCP
get_query_results
description is updated. Security and operational impact • Nothing is sensitive on upgrade day, so nobody loses or gains access until a tag is marked. • Everything is enforced in osctrl-api. osctrl-tls is not changed. • A sensitive node returns the same 404 as a node that doesn't exist on every node-scoped route below full tier. • Schema changes are additive: •
admin_tags.sensitive
(Postgres:
ADD COLUMN ... NOT NULL DEFAULT false
). •
archive_osquery_nodes.node_id
, with an index built at startup on a table that only grows. • Aggregate counts (dashboard stats, platform and version counts, query execution counts) still include sensitive nodes. Known limits • Archives from before this change: nodes archived earlier have no
node_id
. If one of their tags is later marked sensitive, their old query results stay visible at standard tier. • Multi-environment queries: a user with no access to one of a query's target environments sees no results for that query. • Duplicate UUIDs: a UUID enrolled in two environments can be over-hidden. • CLI: the CLI can't set the sensitive flag yet. Tagging through the CLI's direct-database mode doesn't cancel pending work, although results are still hidden when read. • Audit log and alert history: these stay fully visible to read-only admins, sensitive node identities included, because the role exists for oversight. • Environment rename: earlier queries' stored environment targets keep the old name, so their results show zero rows after a rename. This fails closed and will be fixed in a follow-up. Validation •
go build ./...
and
go test ./...
pass. • New tests cover: • the filter: standard and tags exclusion, deleted sensitive tags, soft-deleted taggings, deleted nodes; •
NodeFilter
empty and combined cases on the DB and S3 readers; • results and carves at standard tier, including deleted nodes, environment rename and cross-environment queries; • tag add/edit with the flag, grant removal, pending cancellation and scope-tag rejection; • 404 parity on node detail, logs, posture and per-node stats, batch filtering, error nodes and lookup; • target blanking. •
make openapi-check
passes. •
cd frontend && npm run check
passes. • `golangci-lint`: no new findings. •
git diff --stat -- cmd/tls
is empty. • Not run: Playwright and in-browser checks. There is no visible UI change, so there are no screenshots. jmpsec/osctrl