feat(observe): watch Secrets metadata-only — credential bytes never enter memory (JEF-268)#140
Merged
thejefflarson merged 1 commit intoJul 1, 2026
Conversation
…enter memory (JEF-268) protector's graph models Secrets purely by identity (namespace + name via `SecretMeta`), yet it fetched full Secret objects — the reflector watched `Secret` and the initial list called `.list()` — so every credential's `.data` crossed the wire and sat in the in-memory reflector store though nothing read it. Switch both Secret reads to metadata-only: - Reflector now reflects `PartialObjectMeta<Secret>` via `watcher(Api::<PartialObjectMeta<Secret>>, _)` — the non-deprecated equivalent of `metadata_watcher` in kube 4.0.0 (metadata_watcher is deprecated in favor of this exact form; using it directly would trip clippy warns-as-errors). - Initial list uses `Api::<Secret>::list_metadata(..)` instead of `.list(..)`. `ObjectMeta` carries name+namespace, so this is behavior-preserving: `SecretMeta` construction and the graph's secret-objective nodes are identical, but `.data` never enters memory. A comment at the watch site records why the RBAC grant stays (vanilla RBAC can't express metadata-only on secrets; this is voluntary client restraint, not a narrowed grant — dropping the grant is a separate ticket). Tests: two new tests pin the metadata-only guarantee to the reflected type — one asserts `metadata_api()` is true, one proves a full Secret payload deserializes as `PartialObjectMeta<Secret>` with `.data`/`stringData` dropped and identity kept. Existing graph/reachability tests stay green (461 tests pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
protector's graph models Secrets purely by identity (
observe::SecretMeta { namespace, name }), yet it fetched full Secret objects: the reflector watchedSecretand the initial list called.list(). Every credential's.datatherefore crossed the wire and sat in the in-memory reflector store — even though nothing ever read it. This shrinks protector's own blast radius by never holding secret values it doesn't need.Change (behavior-preserving)
PartialObjectMeta<Secret>viawatcher(Api::<PartialObjectMeta<Secret>>, cfg), so the watch stream carries identity only.Api::<Secret>::list_metadata(..)instead of.list(..), so the apiserver returnsPartialObjectMeta<Secret>(no.data/stringData).ObjectMetacarries name + namespace — allSecretMetaneeds — soSecretMetaconstruction and the graph's secret-objective nodes are identical; only the in-memory footprint changes (.datanever enters memory).metadata-watcher approach / DECISION
The ticket specified
kube::runtime::metadata_watcher. In kube 4.0.0 that function is deprecated in favor ofwatcher(Api::<PartialObjectMeta<K>>::all(client), config)— the deprecation note names this exact form, andApi<PartialObjectMeta<K>>automatically issues metadata-only requests. Since this repo treats clippy warnings as errors, I used the non-deprecated equivalent (same on-the-wire behavior, no deprecation warning). Noted in a comment at the watch site.RBAC caveat (recorded in-code)
Vanilla k8s RBAC can't express "metadata-only on secrets" —
get/list/watchonsecretsis all-or-nothing — so protector's grant necessarily still permits reading values. This change removes the exposure (what protector holds in memory), a voluntary client-side restraint; it does not narrow the grant. Dropping the grant entirely (deriving secret nodes from mounts + RBAC) is deliberately out of scope (a separate Tier-2 ticket). A comment at the watch site records this.Tests
Two new tests in
engine/src/engine/run_loop.rspin the guarantee to the exact reflected type:secret_informer_requests_metadata_only— asserts<PartialObjectMeta<Secret> as Resource>::metadata_api()is true (the property that makes both watch and list metadata-only).reflected_secret_drops_data_keeps_identity— feeds a full Secret payload (withdata/stringData), deserializes asPartialObjectMeta<Secret>, and proves.data/stringData+ the credential bytes are dropped while namespace+name survive.Existing graph/reachability tests stay green, proving behavior-preservation.
Gates (from
engine/)cargo fmt— cleancargo build— cleancargo clippy --all-targets— clean (no warnings)cargo test— 461 passed, 0 failed, 1 ignoredManual self-review (
/simplify+/soundcheck:pr-reviewnot installed).datais never fetched, stored, or logged — reflector holdsPartialObjectMeta<Secret>(structurally no data field), list useslist_metadata, snapshot construction reads.metadataonly. Zero-egress and shadow invariants untouched (no new outbound paths, no actuation change).run_loop.rs(556 lines) andobserve/mod.rs(476 lines) stay under the 1,000-line cap.Coordination
Only the Secret reflector line + list changed in
run_loop.rs; JEF-266/JEF-269 also touch that file's wiring — a trivial conflict there is expected and left for the architect. No other watcher changed; JEF-269's Behavior/ingest untouched.Closes JEF-268
🤖 Generated with Claude Code