Skip to content

SSE k8s_event change filter is namespace-coarse + uses a kind denylist (not exact-GVR RBAC) #913

Description

@nadaverell

Status

The original broadcast-to-all leak is fixed by PR #984 (fix(sse): RBAC filtering on the topology + k8s_event streams, merged 2026-06-24). What remains is a precision gap: the SSE change filter is namespace-coarse and uses a kind denylist rather than exact per-(group, resource) authorization. This issue is re-scoped to that remaining gap. See the comment below for the original framing.

What's already fixed

The diff-bearing k8s_event path (internal/server/sse.go) no longer calls Broadcast (broadcast-to-all). It calls broadcastResourceChange, which filters per client via clientCanSeeChange:

  • Namespaced changes — delivered only if the namespace is in the client's RBAC-resolved namespace set (ClientInfo.Namespaces; nil = all-access).
  • Cluster-scoped changes — delivered only if the kind isn't in the client's DeniedKinds set.
  • Both resolved once at Subscribe() time (handleSSEdeniedClusterScopedTopoKinds + namespace intersection), not per-event — the closure-style approach that keeps the SAR cost off the hot broadcast path.
  • Namespace itself is denied for users who can't list namespaces.
  • Regression tests in internal/server/sse_rbac_test.go.

Remaining gaps

Both are rooted in ResourceChange carrying only Kind (a string), not the exact GVR — so the broadcast path can't authorize per (group, resource). Documented in the broadcastResourceChange doc comment in sse.go.

  1. Namespaced kinds the user can't read within an allowed namespace — e.g. Secrets/Roles for a list-pods-only viewer. Namespace membership is the only gate; per-kind RBAC inside the namespace is not checked, so those diffs still pass.
  2. Cluster-scoped kinds outside topology.ClusterScopedKinds (pkg/topology/cluster_scoped_kinds.go) — ClusterRole, cluster-scoped CRDs, and MutatingWebhookConfiguration / ValidatingWebhookConfiguration (admissionregistration.k8s.io) are not in that table, so they're never added to DeniedKinds and their diffs still leak. Kind-string matching also misses CRD variants (e.g. EC2NodeClass vs synthesized NodeClass).

Suggested fix

  • Carry the exact GVR on ResourceChange (not just Kind).
  • In broadcastResourceChange, authorize each client against the exact (group, resource, namespace) via the cached per-user canRead — covering both namespaced-kind-within-namespace and arbitrary cluster-scoped kinds, instead of the namespace-set + topology-denylist heuristic.
  • Regression tests: a list-pods-only client must not receive Secret/Role diffs in a namespace it can otherwise see; a client without admissionregistration RBAC must not receive MutatingWebhookConfiguration/ValidatingWebhookConfiguration diffs.

Discovery

Surfaced by Codex cross-model review during PR #912 (admission webhook config tracking). Partially addressed by #984.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions