feat(observe): ingest k8s audit-log secret GETs as a corroborating runtime signal (JEF-269)#141
Merged
thejefflarson merged 1 commit intoJul 1, 2026
Conversation
…ntime signal (JEF-269)
A pod reads a secret two ways: a mounted-file read the eBPF agent sees
(Behavior::SecretRead) or an API GET via its ServiceAccount RBAC — a TLS call
to the apiserver eBPF can't attribute. protector models the API path as an RBAC
CanRead [RBAC-GRANTED] chain but couldn't observe it live. This ingests the
apiserver audit log as the missing "corroborated-now" signal.
- behavior crate: add a `SecretReadSource {Mounted, Api}` discriminator to
`Behavior::SecretRead`, defaulted to Mounted and omitted on the wire so the
eBPF agent's contract is byte-for-byte stable. Keeps the crate cluster-agnostic.
- observe::audit: an authenticated in-cluster HTTP endpoint the apiserver's audit
webhook POSTs to (mirrors observe::runtime — bearer auth, per-peer rate limit,
body cap, TTL'd store, wake-on-new-observation only). Parses allowed
get/list/watch on secrets attributed to a ServiceAccount; drops non-secret
resources, denied requests, non-SA users, and malformed payloads without panic;
bounds every untrusted field; never logs/stores a secret value.
- AuditSecretReadAdapter: SA→workload attribution is engine-side. Attaches a
SecretRead{Api} signal to EVERY workload that RunsAs the SA (an SA maps to many
pods — the ambiguity is kept, not falsely narrowed), flipping corroboration on
the RBAC-granted credential-access chain exactly as a Falco alert / mounted read
does (the JEF-117 pattern). Shadow-gated: sets corroborated, never actuates.
- wire the ingest behind PROTECTOR_AUDIT_ADDR (unset = off), inbound-only (ADR-0015).
Scope: ingest + parse + corroboration only. The apiserver audit-policy + webhook
config is a deploy-repo follow-up.
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.
Closes JEF-269
What & why
A pod reads a secret two ways: a mounted-file read the eBPF agent sees directly (
Behavior::SecretRead), or an API GET via its ServiceAccount RBAC — a TLS call to the apiserver eBPF can't attribute as a secret read. protector already models the API path as reachability (an RBACCanRead [RBAC-GRANTED]chain) but couldn't observe it live. The apiserver's own audit log records every secret GET/LIST/WATCH, so this ingests it as the missingcorroborated-nowsignal (ADR-0016).Design — native audit-webhook ingest
observe::audit— an authenticated in-cluster HTTP endpoint the apiserver's audit webhook POSTs to, mirroringobserve::runtime: bearer auth (reusesingest_guard), per-peer rate limit, body-size cap, a TTL'd store on the same 300s window as the runtime feed, and wake-the-engine only on a new observation.parse_audit_eventnormalizesverb ∈ {get,list,watch}onsecrets(core group) that was allowed and attributed to a ServiceAccount. Accepts the apiserver'sEventListbatch or a bare event.AuditSecretReadAdapter— the SA→workload attribution is engine-side. For each read it finds theIdentityfor the requesting SA and attaches aSecretRead{Api}signal to everyWorkloadthatRunsAsit. Where an SA backs multiple pods the ambiguity is represented honestly (attach to all), never falsely narrowed to one. That flips corroboration on the RBAC-granted credential-access chain exactly as a Falco alert / mounted read does (the JEF-117 pattern). Shadow-gated: setscorroborated, never actuates.SecretReadsource discriminator (decision)Added a
SecretReadSource {Mounted, Api}enum +sourcefield onBehavior::SecretRead(chose the field over a new variant to keep one secret-read concept). It defaults toMountedand is omitted on the wire, so the eBPF agent's{"kind":"secret_read","secret":"..."}contract is byte-for-byte unchanged and thebehaviorcrate stays cluster-agnostic. Summary + verdict-cache fingerprint distinguish the two; the metric label stays the coarse shared token.Security / zero-egress
PROTECTOR_INGEST_TOKEN[_FILE]), rate-limited, body-capped.MAX_FIELD_LEN/MAX_EVENTS_PER_BODY), malformed/denied/non-SA/non-secret payloads dropped without panic. Audit events carry secret names/refs only, never values — nothing logs or stores a value (only counts are logged).Wiring
Behind
PROTECTOR_AUDIT_ADDR(unset = off);run_loop.rschange is minimal/additive (mirrors the runtime ingest).Follow-up (deploy repo)
The apiserver audit-policy + audit-webhook config (a policy that logs secret get/list/watch at the
Metadata/RequestResponselevel, and the webhook backend pointing atPROTECTOR_AUDIT_ADDRwith the bearer secret) is a deploy-repo change, out of scope here — this PR is the in-cluster ingest + parsing + corroboration.Tests
behavior: source discriminator wire compat (mounted omitted, api explicit, legacy → mounted), summary/fingerprint distinction.observe::audit: allowed get parses; list/watch + cluster-wide covered; denied / non-read verbs / non-secret / non-SA / malformed dropped (no panic); untrusted fields size-bounded; EventList batch; TTL expiry + dedup.AuditSecretReadAdapter: API read attaches to the SA's workload; corroborates the RBAC-granted chain end-to-end; unmodeled SA corroborates nothing; an SA backing many pods corroborates all of them.Checks
cargo fmt·cargo build·cargo clippy --all-targets(engine + behavior, warns=errors) ·cargo test— all green (behavior 9, engine 465 + integration/file-size-guard).🤖 Generated with Claude Code