Skip to main content

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}