vault_audit_tools/commands/
k8s_auth.rs

1//! Kubernetes authentication analysis command.
2//!
3//! Analyzes Kubernetes auth method usage to understand service account
4//! access patterns and identify high-volume K8s clients.
5//! Supports multi-file analysis for tracking over time.
6//!
7//! # Usage
8//!
9//! ```bash
10//! # Single file analysis
11//! vault-audit k8s-auth audit.log
12//!
13//! # Multi-day analysis with CSV export
14//! vault-audit k8s-auth logs/*.log --output k8s-usage.csv
15//! ```
16//!
17//! # Output
18//!
19//! Displays or exports Kubernetes authentication statistics:
20//! - Service account name
21//! - Namespace
22//! - Pod name (if available)
23//! - Authentication count
24//! - Associated entity ID
25//!
26//! Helps identify:
27//! - Most active K8s service accounts
28//! - Service accounts with excessive auth requests
29//! - K8s authentication patterns by namespace
30//! - Pods making frequent Vault requests
31
32use crate::audit::types::AuditEntry;
33use crate::utils::format::format_number;
34use crate::utils::processor::{ProcessingMode, ProcessorBuilder};
35use anyhow::Result;
36use std::collections::HashMap;
37
38#[derive(Debug, Clone)]
39struct K8sAuthState {
40    k8s_logins: usize,
41    entities_seen: HashMap<String, usize>,
42}
43
44impl K8sAuthState {
45    fn new() -> Self {
46        Self {
47            k8s_logins: 0,
48            entities_seen: HashMap::with_capacity(1000),
49        }
50    }
51
52    fn merge(mut self, other: Self) -> Self {
53        self.k8s_logins += other.k8s_logins;
54        for (entity, count) in other.entities_seen {
55            *self.entities_seen.entry(entity).or_insert(0) += count;
56        }
57        self
58    }
59}
60
61pub fn run(log_files: &[String], output: Option<&str>) -> Result<()> {
62    let processor = ProcessorBuilder::new()
63        .mode(ProcessingMode::Auto)
64        .progress_label("Processing".to_string())
65        .build();
66
67    let (result, stats) = processor.process_files_streaming(
68        log_files,
69        |entry: &AuditEntry, state: &mut K8sAuthState| {
70            // Filter for successful Kubernetes auth operations (response type, no error)
71            if entry.entry_type != "response" || entry.error.is_some() {
72                return;
73            }
74
75            let Some(request) = &entry.request else {
76                return;
77            };
78
79            let path = match &request.path {
80                Some(p) => p.as_str(),
81                None => return,
82            };
83
84            if !path.ends_with("/login") {
85                return;
86            }
87
88            // Check if it's a K8s/OpenShift login by path OR mount_type
89            let is_k8s_by_path = path.contains("kubernetes") || path.contains("openshift");
90            let is_k8s_by_mount = request
91                .mount_type
92                .as_deref()
93                .is_some_and(|mt| mt == "kubernetes" || mt == "openshift");
94
95            if is_k8s_by_path || is_k8s_by_mount {
96                state.k8s_logins += 1;
97
98                if let Some(entity_id) = entry.auth.as_ref().and_then(|a| a.entity_id.as_deref()) {
99                    *state
100                        .entities_seen
101                        .entry(entity_id.to_string())
102                        .or_insert(0) += 1;
103                }
104            }
105        },
106        K8sAuthState::merge,
107        K8sAuthState::new(),
108    )?;
109
110    let total_lines = stats.total_lines;
111    let k8s_logins = result.k8s_logins;
112    let entities_seen = result.entities_seen;
113
114    eprintln!(
115        "\nTotal: Processed {} lines, found {} K8s logins",
116        format_number(total_lines),
117        format_number(k8s_logins)
118    );
119
120    println!("\n{}", "=".repeat(80));
121    println!("KUBERNETES/OPENSHIFT AUTHENTICATION ANALYSIS");
122    println!("{}", "=".repeat(80));
123
124    println!("\nSummary:");
125    println!("  Total lines processed: {}", format_number(total_lines));
126    println!(
127        "  Total K8s/OpenShift logins: {}",
128        format_number(k8s_logins)
129    );
130    println!("  Unique entities: {}", format_number(entities_seen.len()));
131
132    if k8s_logins > 0 {
133        let ratio = k8s_logins as f64 / entities_seen.len() as f64;
134        println!("  Login-to-Entity ratio: {:.2}", ratio);
135
136        println!("\nTop 20 Entities by Login Count:");
137        println!("{}", "-".repeat(80));
138
139        let mut sorted: Vec<_> = entities_seen.iter().collect();
140        sorted.sort_by(|a, b| b.1.cmp(a.1));
141
142        for (i, (entity, count)) in sorted.iter().take(20).enumerate() {
143            println!("{}. {} - {} logins", i + 1, entity, format_number(**count));
144        }
145    }
146
147    if let Some(output_file) = output {
148        use std::fs::File;
149        use std::io::Write;
150        let mut file = File::create(output_file)?;
151        writeln!(file, "entity_id,login_count")?;
152        for (entity, count) in &entities_seen {
153            writeln!(file, "{},{}", entity, count)?;
154        }
155        println!("\nOutput written to: {}", output_file);
156    }
157
158    println!("\n{}", "=".repeat(80));
159
160    Ok(())
161}