Skip to content

feat: namespace filters and opt-out annotations with coverage counters - #11

Merged
kfeofantov merged 1 commit into
mainfrom
feat/namespace-filters
Aug 5, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
feat/namespace-filters

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

The filter layer, landed before the agent ever writes anywhere — filters must exist before the first payload does.

What

  • internal/config: first agent configuration — a YAML file via -config flag (in-cluster this becomes the Helm-rendered ConfigMap mounted as a file, per ADR 0001). Parsing is strict: unknown fields error out, so a typo cannot silently disable a filter
  • internal/collector/filter.go: PodFilter with namespace allow/deny lists (* globs only; empty allow = admit all; deny always applies and wins on conflict) and both opt-out annotations (rebuildstack.co/collect: "false" on namespace or pod), honored regardless of configuration. Namespace annotations are read live from a new namespace informer, so annotating a namespace mid-run takes effect
  • The filter gates every pod-derived signal before callbacks: an excluded pod produces no pod observed and no oom kill observed. Nodes are deliberately not gated (security.md §8)
  • Coverage counters instead of names (invariant 6): exclusions are counted per filter type, attributed to the first rejecting filter in fixed order (namespace filter → namespace annotation → pod annotation), and logged as a periodic + shutdown coverage line — the seed of the §11 coverage report

Scope

Implements exactly what docs/security.md already promises (§4 namespaces RBAC, §8 aggregate-counts rule, §11 opt-out) — no doc change needed. sigs.k8s.io/yaml moves from indirect to direct dependency.

Verified

  • Unit: glob matcher table, allow/deny/annotation semantics and attribution order, watcher-level gating incl. OOM suppression and exact coverage counts; strict config parsing
  • e2e on kind: denied namespace, opted-out namespace, opted-out pod all invisible in output while counted once each; collected pod reported
  • Live binary with a deny config over kind system namespaces: pods_observed:0, excluded_namespace_filter:9, no excluded names anywhere in the log

🤖 Generated with Claude Code

Add the collection filter layer promised in docs/security.md: namespace
allow/deny lists ('*' globs; empty allow admits all, deny wins) from a
new YAML config file (-config flag; in-cluster this will be the
Helm-rendered ConfigMap, ADR 0001), plus the rebuildstack.co/collect:
"false" opt-out annotation on namespaces and pods, honored regardless
of configuration and evaluated live via a namespace informer.

The filter gates every pod-derived signal before it reaches a callback:
an excluded pod is neither reported nor scanned for OOM kills. Nodes
are deliberately not gated (security.md §8). Excluded pods surface only
as aggregate counts per filter type — never by name — logged as a
periodic coverage line, the seed of the §11 coverage report. Exclusions
attribute to the first rejecting filter in fixed order: namespace
filter, namespace annotation, pod annotation.

Config parsing is strict: an unknown field is an error, so a typo
cannot silently disable a filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kfeofantov
kfeofantov merged commit 9afee70 into main Aug 5, 2026
3 checks passed
@kfeofantov
kfeofantov deleted the feat/namespace-filters branch August 9, 2026 12:22
kfeofantov added a commit that referenced this pull request Aug 22, 2026
An audit of security.md. Roughly a third of its 750 lines was a second copy of
the decision records: §10.1 restated ADR 0016 including its argument about
recording rules; §10.2 restated ADR 0015's Decision; the on-node scanning
subsection restated five ADRs across 89 lines. CLAUDE.md rightly requires this
file updated in the same PR as any collection change, and what each slice did
with that rule was append its ADR's summary.

The cost is the same one ADR 0022 addressed, and it is measurable. "Four
aggregate counters" was stale in ADR 0009 §5 and in security.md §8 — one wrong
claim, two files. The false eligible-set claim existed in three copies: ADR 0011
§3, security.md §7.2, and the §5 network table; fixing the code comments in #48
left two standing. Both are corrected here.

Three further defects, all of the kind a mechanism catches and a reader does
not. The section claiming to enumerate what leaves the cluster never named
usage_snapshot, usage_window or ebpf_profile — the two highest-volume payloads
appeared only as "hourly rollup histograms". A cross-reference sent the reader
to "§11, your controls" while §11 was titled Agent self-reporting and listed no
controls. And the first design principle, "no write verbs on any Kubernetes
resource", was contradicted by §4's identity-Secret grant and then repeated a
third time in §9 — ADR 0008 had said explicitly that the categorical version
must go.

Two conventions now carry the file. Reasoning moves to the ADRs and the document
asserts and links instead of re-arguing. A claim that is not built is marked
[planned] at the sentence, not in a footnote: backend transport and mTLS, the
identity Secret grant, enrollment, the pprof profile and its pod probing, the
shipped NetworkPolicy, the startup self-audit, the filter-fingerprint payload.

The label selector is removed as a decision, not an omission. It appeared twice
in this file and once in ADR 0020 §2, was never decided in any ADR, and never
existed in the configuration — the only claim the audit found with no source at
all. The controls are the namespace allow/deny lists and the opt-out annotation.

docs/security_test.go holds the parts that are lists: the new payload table must
mirror internal/sink/registry.go exactly in both directions, every internal
anchor must resolve to a heading, and every link into adr/ must resolve to a
file. Both checks were negative-tested; the anchor check reproduces the broken
#11-your-controls link that shipped in this file.

Honest result on size: the document did not get shorter. About 130 lines of
duplicated reasoning came out and about the same went in — the payload table
that was missing, the controls table that was scattered, and the eligible-set
paragraph that was wrong. What changed is what the length is made of. ADR 0024
records that rather than claiming a reduction.

Closes two more of ADR 0022 §5's five open items. Remaining: eligible-set
enforcement, and the controller Deployment vs StatefulSet question.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant