Hedronite · Dev Lesson · Polyglot-Dev / Rust · Fri 2026-10-09

Rust serde over Kubernetes audit events JSON lines, a stage-aware fold, and the Secret-read flag

One request can write three records. Count the one that finished.

Lesson Class: Dev (T1 · Rust touching K8s audit.k8s.io)
Language Idiom: serde enums · Option · matches! · fold · BTreeMap
Verified: cargo test 6 passed · synthetic fixture
Lag rule: TRPL whole canon shipped at Duha
Paired Ops: EKS control-plane logging census
Paired Cert: CKS audit Policy
Closed Vocabulary
Level and Stage are enums.
Open Line
A bad line is a counter, not a panic.
Count the Finish
ResponseComplete only.
Strings drift. Enums refuse.

One request can write three records. Count the one that finished.

§I. Frame

The paired Ops lesson ships EKS audit logs to CloudWatch. The paired Cert lesson writes the Policy that shapes them. Both end in the same artifact: a file of JSON lines, one audit.k8s.io/v1 Event per line. Today we read it in Rust.

The problem for today: parse each line into typed Rust, then fold the file into a per-user list of Secret reads by non-system users, denied attempts included. Crate: crate/ (package audit_fold).

§II. Closed Vocabulary, Open Line

Closed Vocabulary, Open Line (named technique). Fields with a fixed set of values become enums. Fields that may be absent become Option. A line that will not parse is counted, never fatal.

level has four values and stage has four, so both are enums. serde matches the variant name to the JSON string:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum Level { None, Metadata, Request, RequestResponse }

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum Stage { RequestReceived, ResponseStarted, ResponseComplete, Panic }

A misspelled level now fails at parse time, not in a string compare three functions later.

objectRef is absent for non-resource requests like /healthz, and responseStatus is absent at RequestReceived. Both are Option. The Event uses rename_all = "camelCase", plus one explicit rename = "auditID" because camelCase would produce auditId.

§III. Count the Finish

Count the Finish (named technique). The API server can emit a record at RequestReceived, ResponseStarted (watches and other long-running calls), and ResponseComplete for the same auditID. Counting every line counts one read up to three times. The predicate keeps only ResponseComplete:

pub fn is_secret_read(&self) -> bool {
    self.stage == Stage::ResponseComplete
        && matches!(self.verb.as_str(), "get" | "list" | "watch")
        && self.object_ref.as_ref().is_some_and(|o|
            o.resource.as_deref() == Some("secrets") && o.api_group.is_none())
}

api_group.is_none() pins the core group, so a CRD that happens to be named secrets in another group does not match.

"System" means the username starts with system:: service accounts, nodes, and control-plane components. Groups are deliberately ignored. Every authenticated human is in system:authenticated.

§IV. The fold

pub fn fold<'a>(lines: impl IntoIterator<Item = &'a str>) -> Census {
    lines.into_iter().filter(|l| !l.trim().is_empty()).fold(Census::default(), |mut c, line| {
        c.lines += 1;
        match serde_json::from_str::<Event>(line) {
            Err(_) => c.bad_lines += 1,
            Ok(e) if e.is_secret_read() && !e.is_system_user() => { /* push "ns/name verb code" */ }
            Ok(_) => {}
        }
        c
    })
}

The accumulator is a BTreeMap<String, Vec<String>>, so output is sorted by user without a separate sort. A 403 is kept and tagged denied: a human probing Secrets they cannot read is a signal, not noise.

§V. Verified

fixture/audit.jsonl is synthetic, nine lines in the documented shape: Alice's get at two stages, a controller's list, Bob's denied list, a ConfigMap read, a node's read, Carol's watch still at ResponseStarted, a create, and one truncated line. Real output (cargo test 6 passed, 0 warnings):

[email protected]	SECRET_READ	payments/db-creds get 200
[email protected]	SECRET_READ	payments/* list 403 denied
summary lines=9 bad_lines=1 users_flagged=2

Alice appears once, not twice. Carol's watch has not finished, so it does not count yet. The truncated line is counted and skipped.

cd crate && cargo test && cargo run -- ../fixture/audit.jsonl   # exit 2 when anyone is flagged

§VI. Close

Closed Vocabulary, Open Line: enums for level and stage, Option for what may be missing, and a bad line is a counter. Count the Finish: filter to ResponseComplete before you count. The Cert lesson explains why a Secret read at Metadata level tells you who and what, but never the value.

Related