key_vault/audit/mod.rs
1//! Layer 9 — Audit logging.
2//!
3//! Where Layer 8 ([`crate::monitor`]) reports **anomalies and
4//! failures**, Layer 9 records **every operation** the vault performs.
5//! Every successful access — register, read, rotate, unregister,
6//! master-unlock attempt — produces an [`AuditEvent`] that the
7//! configured [`AuditSink`] receives.
8//!
9//! The audit trail is the forensic complement to the monitor:
10//! monitors tell you when something went wrong; audit logs tell you
11//! what happened across the lifetime of the vault, in order.
12//!
13//! # Storage discipline
14//!
15//! Audit events are designed to be safe to ship to remote sinks
16//! (centralized log aggregators, SIEM systems, compliance archives).
17//! Every field is sanitized by trait contract:
18//!
19//! - **No key bytes** — the vault never passes raw key material to a
20//! sink. Only the key's name appears.
21//! - **No caller-supplied secrets** — the `note` field is
22//! `Cow<'static, str>`; caller responsibility to keep it free of
23//! key-equivalent values.
24//!
25//! # Default
26//!
27//! The default sink is [`NoAudit`] — events are constructed and
28//! discarded. Zero allocations on the happy path (the event struct is
29//! built on the stack and dropped immediately).
30//!
31//! Enable a real sink with [`KeyVaultBuilder::with_audit_sink`](crate::KeyVaultBuilder::with_audit_sink).
32
33use alloc::borrow::Cow;
34use alloc::string::String;
35use core::fmt;
36use core::time::Duration;
37use std::thread::ThreadId;
38
39mod no_audit;
40
41pub use self::no_audit::NoAudit;
42
43#[cfg(feature = "monitor-tracing")]
44mod log_audit;
45#[cfg(feature = "monitor-tracing")]
46pub use self::log_audit::LogAudit;
47
48/// Discriminant of the operation an [`AuditEvent`] describes.
49///
50/// `#[non_exhaustive]` — new variants are additive.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52#[non_exhaustive]
53pub enum AccessKind {
54 /// A new named key was registered via
55 /// [`KeyVault::register`](crate::KeyVault::register).
56 Register,
57 /// A registered key was removed via
58 /// [`KeyVault::unregister`](crate::KeyVault::unregister).
59 Unregister,
60 /// The key was accessed in a scoped callback via
61 /// [`KeyVault::with_key`](crate::KeyVault::with_key).
62 Read,
63 /// The key was rotated to fresh material via
64 /// [`KeyVault::rotate`](crate::KeyVault::rotate).
65 Rotate,
66 /// A one-shot [`KeyVault::fragment`](crate::KeyVault::fragment)
67 /// call (no registry entry).
68 OneShotFragment,
69 /// A one-shot [`KeyVault::defragment`](crate::KeyVault::defragment)
70 /// call.
71 OneShotDefragment,
72 /// A master-key emergency-unlock attempt. The boolean reports
73 /// whether the supplied bytes matched the stored digest.
74 MasterUnlockAttempt {
75 /// `true` if the supplied bytes matched the registered master
76 /// digest in constant-time comparison.
77 matched: bool,
78 },
79}
80
81impl fmt::Display for AccessKind {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Register => f.write_str("register"),
85 Self::Unregister => f.write_str("unregister"),
86 Self::Read => f.write_str("read"),
87 Self::Rotate => f.write_str("rotate"),
88 Self::OneShotFragment => f.write_str("one-shot-fragment"),
89 Self::OneShotDefragment => f.write_str("one-shot-defragment"),
90 Self::MasterUnlockAttempt { matched: true } => f.write_str("master-unlock-ok"),
91 Self::MasterUnlockAttempt { matched: false } => f.write_str("master-unlock-fail"),
92 }
93 }
94}
95
96/// Single record in the vault's audit trail.
97///
98/// Constructed by the vault on every operation; passed to the
99/// configured [`AuditSink`]. All fields are non-secret and safe to ship
100/// to log aggregators / SIEM systems.
101///
102/// `#[non_exhaustive]` — additional fields (caller identity, request
103/// id correlation, etc.) may be added in minor releases.
104#[derive(Debug, Clone)]
105#[non_exhaustive]
106pub struct AuditEvent {
107 /// Time the event was emitted, expressed as a `Duration` since the
108 /// Unix epoch. Same encoding used by [`KeyMetadata`](crate::KeyMetadata)
109 /// for portability to future `no_std` builds.
110 pub timestamp: Duration,
111 /// Logical name of the key. For one-shot fragment/defragment
112 /// operations (no registry entry) the value is the empty string.
113 /// For master-unlock attempts the reserved name `"<master>"` is
114 /// used.
115 pub key_name: String,
116 /// Operation discriminant.
117 pub kind: AccessKind,
118 /// Thread that produced the event.
119 pub thread_id: ThreadId,
120 /// Caller-supplied free-text label. Never includes key material.
121 pub note: Cow<'static, str>,
122}
123
124/// Outbound channel for the vault's audit trail.
125///
126/// # Implementor contract
127///
128/// - **Non-blocking.** Sink calls must return promptly. Network / disk
129/// work belongs on a background worker.
130/// - **No panics.** A panicking sink implementation is a bug in the
131/// implementation, not the vault.
132/// - **No back-pressure into the vault.** If the sink is overloaded,
133/// shed events internally — never block the caller.
134/// - **`Send + Sync`.** Sinks are shared across threads.
135pub trait AuditSink: Send + Sync {
136 /// Receive one audit event. The sink may inspect any field but
137 /// must not mutate the event (it is passed by reference).
138 fn on_event(&self, event: &AuditEvent);
139
140 /// Hot-path optimization hook: a sink that returns `true` here
141 /// declares that it will discard every event without inspecting
142 /// any field. The vault uses this to skip [`AuditEvent`]
143 /// construction entirely on the hot path, avoiding one `String`
144 /// allocation per `with_key` / `fragment` / `defragment` call.
145 ///
146 /// Default: `false`. Override only when the sink is provably
147 /// inert ([`NoAudit`] is the canonical example).
148 #[inline]
149 fn is_no_op(&self) -> bool {
150 false
151 }
152}
153
154// Blanket forwarding impl so callers can pass a pre-wrapped
155// `Arc<dyn AuditSink>` to APIs that accept `impl AuditSink`.
156impl AuditSink for alloc::sync::Arc<dyn AuditSink> {
157 fn on_event(&self, event: &AuditEvent) {
158 (**self).on_event(event);
159 }
160
161 #[inline]
162 fn is_no_op(&self) -> bool {
163 (**self).is_no_op()
164 }
165}