key_vault/monitor/log_monitor.rs
1//! [`LogMonitor`] — `tracing`-backed `SecurityMonitor`.
2//!
3//! Gated behind the `monitor-tracing` Cargo feature. The monitor emits
4//! `tracing` events at three levels:
5//!
6//! - `on_decryption_failure` → `warn!` with structured fields
7//! - `on_anomalous_access` → `warn!`
8//! - `on_threshold_breach` → `error!`
9//!
10//! All fields are sanitized: only `key_name`, counters, and the
11//! caller-supplied note are emitted. Nothing the monitor receives is
12//! itself a secret (the `SecurityMonitor` trait contract forbids passing
13//! key material in context structs), so the log lines are safe to ship
14//! to any centralized log aggregator.
15
16use super::{AccessContext, FailureContext, SecurityMonitor, ThresholdContext};
17
18/// `SecurityMonitor` implementation that emits `tracing` events.
19///
20/// Construct with [`LogMonitor::new`]; the type holds no state and is
21/// `Copy`. Each event becomes a `tracing` log entry with structured
22/// fields suitable for filtering in `tracing-subscriber`.
23///
24/// # Examples
25///
26/// ```
27/// use key_vault::{KeyVaultBuilder, LogMonitor};
28///
29/// let _vault = KeyVaultBuilder::new()
30/// .with_monitor(LogMonitor::new())
31/// .build();
32/// ```
33#[derive(Debug, Default, Clone, Copy)]
34pub struct LogMonitor;
35
36impl LogMonitor {
37 /// Construct a new log monitor. Stateless; consider sharing one
38 /// instance across all vaults.
39 #[must_use]
40 pub fn new() -> Self {
41 Self
42 }
43}
44
45impl SecurityMonitor for LogMonitor {
46 fn on_decryption_failure(&self, ctx: &FailureContext) {
47 // Saturating cast: u128→u64. Durations exceeding 2^64 ms (~585
48 // million years) would lose precision; we cap rather than fail
49 // because losing a few high bits in a log timestamp is fine.
50 let elapsed_ms = u64::try_from(ctx.window_elapsed.as_millis()).unwrap_or(u64::MAX);
51 tracing::warn!(
52 target: "key_vault::monitor",
53 key_name = %ctx.key_name,
54 consecutive_failures = ctx.consecutive_failures,
55 window_elapsed_ms = elapsed_ms,
56 note = %ctx.note,
57 "key access failure",
58 );
59 }
60
61 fn on_anomalous_access(&self, ctx: &AccessContext) {
62 tracing::warn!(
63 target: "key_vault::monitor",
64 key_name = %ctx.key_name,
65 note = %ctx.note,
66 "anomalous key access",
67 );
68 }
69
70 fn on_threshold_breach(&self, ctx: &ThresholdContext) {
71 let window_ms = u64::try_from(ctx.window.as_millis()).unwrap_or(u64::MAX);
72 tracing::error!(
73 target: "key_vault::monitor",
74 key_name = %ctx.key_name,
75 failures_in_window = ctx.failures_in_window,
76 window_ms = window_ms,
77 lockout_triggered = ctx.lockout_triggered,
78 "threshold breach",
79 );
80 }
81}