Skip to main content

kipuka/config/
audit.rs

1//! Audit configuration.
2//!
3//! The `[audit]` section controls the audit trail that satisfies NIAP CA PP
4//! FAU family requirements:
5//!
6//! - **FAU_GEN.1** — the server generates audit records for all security-relevant
7//!   events (enrollment, authentication, key operations, admin actions).
8//! - **FAU_STG.1** — the audit trail is append-only at the application level.
9//! - **FAU_STG.4** — when `overflow_policy = "halt"`, EST operations are rejected
10//!   if the audit trail storage is exhausted.
11//! - **FAU_ARP.1** — when the alarm threshold is reached, the configured alarm
12//!   action is triggered.
13
14use serde::Deserialize;
15
16/// Audit log rotation policy.
17#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "lowercase")]
19#[derive(Default)]
20pub enum RotationPolicy {
21    /// Rotate based on file size.
22    Size,
23    /// Rotate daily.
24    #[default]
25    Daily,
26    /// Rotate weekly.
27    Weekly,
28    /// Never rotate (rely on external log management).
29    Never,
30}
31
32/// What to do when audit storage is exhausted (FAU_STG.4).
33#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35#[derive(Default)]
36pub enum OverflowPolicy {
37    /// Drop the oldest audit records to make room.
38    #[default]
39    DropOldest,
40    /// Halt EST operations until audit storage is cleared.
41    ///
42    /// NIAP CA PP FAU_STG.4: "The TSF shall prevent audited events,
43    /// except those taken by the authorised administrator, if the
44    /// audit trail is full."
45    Halt,
46}
47
48/// `[audit]` section — audit trail configuration.
49///
50/// ```toml
51/// [audit]
52/// enabled = true
53/// log_path = "/var/log/kipuka/audit.log"
54/// signed = true
55/// rotation_policy = "daily"
56/// overflow_policy = "halt"
57/// max_rows = 1000000
58/// ```
59#[derive(Debug, Clone, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct AuditConfig {
62    /// Enable audit logging.  Default: `true`.
63    #[serde(default = "bool_true")]
64    pub enabled: bool,
65
66    /// Path to the audit log file.
67    ///
68    /// When using database-backed audit (`log_to_db = true`), this path
69    /// is used for the file-based backup copy.
70    #[serde(default = "default_log_path")]
71    pub log_path: String,
72
73    /// Enable cryptographic signing of audit log entries.
74    ///
75    /// When `true`, each audit entry includes an RFC 3161-style timestamp
76    /// signature chain for tamper detection.
77    #[serde(default)]
78    pub signed: bool,
79
80    /// Log rotation policy.
81    #[serde(default)]
82    pub rotation_policy: RotationPolicy,
83
84    /// Maximum rotation file size in bytes (when `rotation_policy = "size"`).
85    /// Default: 100 MiB.
86    #[serde(default = "default_max_file_size")]
87    pub max_file_size: u64,
88
89    /// Number of rotated log files to retain.
90    /// Default: 10.
91    #[serde(default = "default_retention_count")]
92    pub retention_count: u32,
93
94    /// Store audit events in the database in addition to the log file.
95    #[serde(default = "bool_true")]
96    pub log_to_db: bool,
97
98    /// Overflow policy when audit storage is full (FAU_STG.4).
99    #[serde(default)]
100    pub overflow_policy: OverflowPolicy,
101
102    /// Maximum number of audit rows in the database.
103    ///
104    /// When this limit is reached, the `overflow_policy` determines
105    /// whether old rows are dropped or EST operations are halted.
106    /// `None` means no limit (rely on disk space monitoring).
107    pub max_rows: Option<u64>,
108
109    /// Number of consecutive security violations before the alarm
110    /// action fires (FAU_ARP.1).
111    ///
112    /// Default: 10.
113    #[serde(default = "default_alarm_threshold")]
114    pub alarm_threshold: u32,
115
116    /// Action taken when the alarm threshold is reached.
117    ///
118    /// - `"syslog"` — emit a syslog alert.
119    /// - `"halt"` — halt EST operations.
120    ///
121    /// Default: `"syslog"`.
122    #[serde(default = "default_alarm_action")]
123    pub alarm_action: String,
124
125    /// NIAP CA PP FAU_GEN.1: list of auditable event types.
126    ///
127    /// When non-empty, only these event types are recorded.
128    /// When empty (default), all events are audited.
129    #[serde(default)]
130    pub auditable_events: Vec<String>,
131}
132
133fn bool_true() -> bool {
134    true
135}
136
137fn default_log_path() -> String {
138    "/var/log/kipuka/audit.log".to_string()
139}
140
141fn default_max_file_size() -> u64 {
142    100 * 1024 * 1024 // 100 MiB
143}
144
145fn default_retention_count() -> u32 {
146    10
147}
148
149fn default_alarm_threshold() -> u32 {
150    10
151}
152
153fn default_alarm_action() -> String {
154    "syslog".to_string()
155}
156
157impl Default for AuditConfig {
158    fn default() -> Self {
159        Self {
160            enabled: true,
161            log_path: default_log_path(),
162            signed: false,
163            rotation_policy: RotationPolicy::default(),
164            max_file_size: default_max_file_size(),
165            retention_count: default_retention_count(),
166            log_to_db: true,
167            overflow_policy: OverflowPolicy::default(),
168            max_rows: None,
169            alarm_threshold: default_alarm_threshold(),
170            alarm_action: default_alarm_action(),
171            auditable_events: Vec::new(),
172        }
173    }
174}
175
176impl AuditConfig {
177    /// Validate audit configuration constraints.
178    pub fn validate(&self) -> std::result::Result<(), String> {
179        if !self.enabled {
180            return Ok(());
181        }
182
183        match self.alarm_action.as_str() {
184            "syslog" | "halt" => {}
185            other => {
186                return Err(format!(
187                    "[audit].alarm_action must be \"syslog\" or \"halt\", got {other:?}"
188                ));
189            }
190        }
191
192        if self.alarm_threshold == 0 {
193            return Err("[audit].alarm_threshold must be at least 1".into());
194        }
195
196        if self.rotation_policy == RotationPolicy::Size && self.max_file_size == 0 {
197            return Err(
198                "[audit].max_file_size must be at least 1 when rotation_policy = \"size\"".into(),
199            );
200        }
201
202        Ok(())
203    }
204}