Skip to content

feat(observe): ingest k8s audit-log secret GETs as a corroborating runtime signal (JEF-269)#141

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-269-ingest-k8s-audit-log-secret-gets-as-a-corroborating-runtime
Jul 1, 2026
Merged

feat(observe): ingest k8s audit-log secret GETs as a corroborating runtime signal (JEF-269)#141
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-269-ingest-k8s-audit-log-secret-gets-as-a-corroborating-runtime

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

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 RBAC CanRead [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 missing corroborated-now signal (ADR-0016).

Design — native audit-webhook ingest

  • observe::audit — an authenticated in-cluster HTTP endpoint the apiserver's audit webhook POSTs to, mirroring observe::runtime: bearer auth (reuses ingest_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_event normalizes verb ∈ {get,list,watch} on secrets (core group) that was allowed and attributed to a ServiceAccount. Accepts the apiserver's EventList batch or a bare event.
  • AuditSecretReadAdapter — the SA→workload attribution is engine-side. For each read it finds the Identity for the requesting SA and attaches a SecretRead{Api} signal to every Workload that RunsAs it. 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: sets corroborated, never actuates.

SecretRead source discriminator (decision)

Added a SecretReadSource {Mounted, Api} enum + source field on Behavior::SecretRead (chose the field over a new variant to keep one secret-read concept). It defaults to Mounted and is omitted on the wire, so the eBPF agent's {"kind":"secret_read","secret":"..."} contract is byte-for-byte unchanged and the behavior crate stays cluster-agnostic. Summary + verdict-cache fingerprint distinguish the two; the metric label stays the coarse shared token.

Security / zero-egress

  • Inbound only (apiserver → protector, in-cluster) — no outbound call (ADR-0015).
  • Endpoint authenticated (shared-secret bearer, PROTECTOR_INGEST_TOKEN[_FILE]), rate-limited, body-capped.
  • All event fields untrusted: size-bounded (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.rs change 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/RequestResponse level, and the webhook backend pointing at PROTECTOR_AUDIT_ADDR with 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

…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
@thejefflarson thejefflarson merged commit 61f946d into main Jul 1, 2026
6 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-269-ingest-k8s-audit-log-secret-gets-as-a-corroborating-runtime 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