key_vault/monitor/mod.rs
1//! Layer 8 — Security monitor.
2//!
3//! A [`SecurityMonitor`] is the vault's outbound channel for anomaly events:
4//! repeated decryption failures, unusual access patterns, and threshold
5//! breaches. Monitor calls happen on the failure path only; the success path
6//! costs nothing.
7//!
8//! Built-in monitors (`NoMonitor`, `LogMonitor`, `MetricsMonitor`,
9//! `WebhookMonitor`, `CompositeMonitor`) arrive in Phase 0.8. This module
10//! currently defines the trait surface and the three event-context structs.
11
12use alloc::borrow::Cow;
13use alloc::string::String;
14use core::time::Duration;
15
16mod composite;
17#[cfg(feature = "monitor-tracing")]
18mod log_monitor;
19mod no_monitor;
20
21pub use self::composite::CompositeMonitor;
22#[cfg(feature = "monitor-tracing")]
23pub use self::log_monitor::LogMonitor;
24pub use self::no_monitor::NoMonitor;
25
26/// Context passed when a decryption attempt fails — wrong key, tampered
27/// ciphertext, etc.
28#[non_exhaustive]
29#[derive(Debug, Clone)]
30pub struct FailureContext {
31 /// Logical name of the key whose use produced the failure.
32 pub key_name: String,
33 /// Number of consecutive failures observed for this key, including this
34 /// one.
35 pub consecutive_failures: u32,
36 /// Time elapsed since the first failure in the current window.
37 pub window_elapsed: Duration,
38 /// Caller-supplied free-form note. Sanitized — never includes key bytes
39 /// or ciphertext.
40 pub note: Cow<'static, str>,
41}
42
43/// Context for a successful access that the monitor flagged as anomalous —
44/// unusual caller, unusual frequency, off-hours activity.
45#[non_exhaustive]
46#[derive(Debug, Clone)]
47pub struct AccessContext {
48 /// Logical name of the key that was accessed.
49 pub key_name: String,
50 /// Caller-supplied free-form note. Sanitized.
51 pub note: Cow<'static, str>,
52}
53
54/// Context for a configured threshold being crossed (e.g. N failures in M
55/// seconds).
56#[non_exhaustive]
57#[derive(Debug, Clone)]
58pub struct ThresholdContext {
59 /// Logical name of the key.
60 pub key_name: String,
61 /// Number of failures observed within the configured window.
62 pub failures_in_window: u32,
63 /// Width of the configured window.
64 pub window: Duration,
65 /// `true` if this breach has put the vault into lock-out state.
66 pub lockout_triggered: bool,
67}
68
69/// Outbound channel for anomaly events.
70///
71/// Implementations should treat monitor calls as advisory and fire-and-forget:
72/// the vault must not block on a monitor, must not crash if a monitor panics
73/// or returns an error, and must not retry. Implementations that need
74/// retry, queuing, or batching are expected to handle that internally
75/// (typically on a background thread).
76///
77/// # Implementor contract
78///
79/// - **Non-blocking.** Calls must return promptly. Network or disk work should
80/// be deferred to a background worker.
81/// - **No panics.** A panicking monitor implementation is a bug in the
82/// implementation, not the vault. Wrap fallible operations and absorb
83/// their errors.
84/// - **No key material in calls.** None of the context structs carry raw key
85/// bytes; do not introduce custom side-channels that do.
86/// - **`Send + Sync`.** Monitors are shared across threads.
87pub trait SecurityMonitor: Send + Sync {
88 /// Called when a decryption attempt fails.
89 fn on_decryption_failure(&self, ctx: &FailureContext);
90
91 /// Called when an access pattern looks anomalous to the configured
92 /// detector.
93 fn on_anomalous_access(&self, ctx: &AccessContext);
94
95 /// Called when a configured failure threshold is crossed.
96 fn on_threshold_breach(&self, ctx: &ThresholdContext);
97}
98
99// Blanket forwarding impl so callers can pass a pre-wrapped
100// `Arc<dyn SecurityMonitor>` to APIs that accept `impl SecurityMonitor`.
101// Useful when the same monitor is referenced from multiple places and
102// the caller already holds it as a trait object.
103impl SecurityMonitor for alloc::sync::Arc<dyn SecurityMonitor> {
104 fn on_decryption_failure(&self, ctx: &FailureContext) {
105 (**self).on_decryption_failure(ctx);
106 }
107 fn on_anomalous_access(&self, ctx: &AccessContext) {
108 (**self).on_anomalous_access(ctx);
109 }
110 fn on_threshold_breach(&self, ctx: &ThresholdContext) {
111 (**self).on_threshold_breach(ctx);
112 }
113}