Skip to content

feat(observe): watch Secrets metadata-only — credential bytes never enter memory (JEF-268)#140

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-268-watch-secrets-metadata-only-protector-must-never-hold
Jul 1, 2026
Merged

feat(observe): watch Secrets metadata-only — credential bytes never enter memory (JEF-268)#140
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-268-watch-secrets-metadata-only-protector-must-never-hold

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What & why

protector's graph models Secrets purely by identity (observe::SecretMeta { namespace, name }), yet it fetched full Secret objects: the reflector watched Secret and the initial list called .list(). Every credential's .data therefore 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)

  • Reflector now reflects PartialObjectMeta<Secret> via watcher(Api::<PartialObjectMeta<Secret>>, cfg), so the watch stream carries identity only.
  • Initial list uses Api::<Secret>::list_metadata(..) instead of .list(..), so the apiserver returns PartialObjectMeta<Secret> (no .data/stringData).

ObjectMeta carries name + namespace — all SecretMeta needs — so SecretMeta construction and the graph's secret-objective nodes are identical; only the in-memory footprint changes (.data never enters memory).

metadata-watcher approach / DECISION

The ticket specified kube::runtime::metadata_watcher. In kube 4.0.0 that function is deprecated in favor of watcher(Api::<PartialObjectMeta<K>>::all(client), config) — the deprecation note names this exact form, and Api<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/watch on secrets is 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.rs pin 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 (with data/stringData), deserializes as PartialObjectMeta<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 — clean
  • cargo build — clean
  • cargo clippy --all-targets — clean (no warnings)
  • cargo test461 passed, 0 failed, 1 ignored

Manual self-review (/simplify + /soundcheck:pr-review not installed)

  • Security: .data is never fetched, stored, or logged — reflector holds PartialObjectMeta<Secret> (structurally no data field), list uses list_metadata, snapshot construction reads .metadata only. Zero-egress and shadow invariants untouched (no new outbound paths, no actuation change).
  • Simplify: diff is two call-site changes + comments + tests; no dead code, no duplication, no unrelated refactor. run_loop.rs (556 lines) and observe/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

…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
@thejefflarson thejefflarson merged commit f0eb838 into main Jul 1, 2026
4 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-268-watch-secrets-metadata-only-protector-must-never-hold branch July 1, 2026 09:40
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