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 (handleSSE → deniedClusterScopedTopoKinds + 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.
- 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.
- 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.
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_eventpath (internal/server/sse.go) no longer callsBroadcast(broadcast-to-all). It callsbroadcastResourceChange, which filters per client viaclientCanSeeChange:ClientInfo.Namespaces;nil= all-access).DeniedKindsset.Subscribe()time (handleSSE→deniedClusterScopedTopoKinds+ namespace intersection), not per-event — the closure-style approach that keeps the SAR cost off the hot broadcast path.Namespaceitself is denied for users who can'tlist namespaces.internal/server/sse_rbac_test.go.Remaining gaps
Both are rooted in
ResourceChangecarrying onlyKind(a string), not the exact GVR — so the broadcast path can't authorize per(group, resource). Documented in thebroadcastResourceChangedoc comment insse.go.topology.ClusterScopedKinds(pkg/topology/cluster_scoped_kinds.go) — ClusterRole, cluster-scoped CRDs, andMutatingWebhookConfiguration/ValidatingWebhookConfiguration(admissionregistration.k8s.io) are not in that table, so they're never added toDeniedKindsand their diffs still leak. Kind-string matching also misses CRD variants (e.g.EC2NodeClassvs synthesizedNodeClass).Suggested fix
ResourceChange(not justKind).broadcastResourceChange, authorize each client against the exact(group, resource, namespace)via the cached per-usercanRead— covering both namespaced-kind-within-namespace and arbitrary cluster-scoped kinds, instead of the namespace-set + topology-denylist heuristic.Secret/Rolediffs in a namespace it can otherwise see; a client withoutadmissionregistrationRBAC must not receiveMutatingWebhookConfiguration/ValidatingWebhookConfigurationdiffs.Discovery
Surfaced by Codex cross-model review during PR #912 (admission webhook config tracking). Partially addressed by #984.