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.
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
- Tome: TRPL ch6.1 enums and Option (grounded-in) · ch13.2 iterators (grounded-in) · ch10.2 traits (referenced)
- Prior K8s Dev: Quantity newtype 10-06 · async kube-rs selector 09-30
- Web: Kubernetes Auditing · audit.k8s.io/v1 reference · serde attributes