1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
use super::types::Config;
#[cfg(feature = "secure-udp")]
use super::validate::read_secret_file;
use super::validate::validate_recovery_file;
#[cfg(feature = "prometheus-exporter")]
use super::validate::validate_secret_file;
impl Config {
/// Resolve recovery mode from CLI flags, enforcing mutual exclusion
/// and loading/validating any file-based command sources.
///
/// Returns `Ok(None)` when no recovery is configured. Returns
/// `Ok(Some(RecoveryMode::Exec{..}))` when `--recovery-exec` or
/// `--recovery-exec-file` is set.
///
/// # Errors
///
/// Returns an `io::Error` if a file cannot be read, its permissions are
/// too open, or mutually exclusive flags are specified.
pub fn resolve_recovery_mode(&self) -> std::io::Result<Option<crate::recovery::RecoveryMode>> {
use crate::recovery::RecoveryMode;
// Collect which sources are configured
let has_exec = self.recovery_exec_cmd.is_some();
let has_exec_file = self.recovery_exec_file.is_some();
if has_exec && has_exec_file {
#[cfg(not(feature = "compile-time-config"))]
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"--recovery-exec and --recovery-exec-file are mutually exclusive",
));
#[cfg(feature = "compile-time-config")]
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"inline exec-recovery command and exec-recovery file are mutually exclusive",
));
}
// Exec mode
if let Some(ref cmd) = self.recovery_exec_cmd {
let mut parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() {
#[cfg(not(feature = "compile-time-config"))]
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"--recovery-exec: command must not be empty",
));
#[cfg(feature = "compile-time-config")]
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"exec-recovery command must not be empty",
));
}
let program = parts.remove(0).to_string();
let args: Vec<String> = parts.into_iter().map(|s| s.to_string()).collect();
return Ok(Some(RecoveryMode::Exec { program, args }));
}
if let Some(ref path) = self.recovery_exec_file {
let cmd = validate_recovery_file(path)?;
let mut parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{}: file is empty", path.display()),
));
}
let program = parts.remove(0).to_string();
let args: Vec<String> = parts.into_iter().map(|s| s.to_string()).collect();
return Ok(Some(RecoveryMode::Exec { program, args }));
}
Ok(None)
}
/// Load the primary and accepted secure keys for AEAD transport.
///
/// `--key-file` is the sole source for secure-UDP keys: it is the only
/// path that goes through [`validate_secret_file`], guaranteeing mode
/// 0600 ownership and an `O_NOFOLLOW` open. Environment-variable keys
/// were removed (see `ConfigError::RemovedFlag`) because they leak
/// through `/proc/<pid>/environ` and `docker inspect`.
///
/// Returns `Ok(None)` when `--key-file` is not set (UDP without AEAD).
///
/// # Errors
///
/// Returns an `io::Error` if the file cannot be read, the key(s) cannot
/// be parsed as 64-character hex strings, or the primary key file contains
/// more than one key.
#[cfg(feature = "secure-udp")]
pub fn load_secure_keys(
&self,
) -> std::io::Result<Option<(varta_vlp::crypto::Key, Vec<varta_vlp::crypto::Key>)>> {
use std::io;
use varta_vlp::crypto::Key;
let Some(ref path) = self.secure_key_file else {
return Ok(None);
};
let content = read_secret_file(path)?;
let mut primary: Option<Key> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if primary.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{}: multiple primary keys found (expected exactly one)",
path.display()
),
));
}
primary = Some(Key::from_hex(line).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{}: {e}", path.display()),
)
})?);
}
let primary = match primary {
Some(k) => k,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("{}: no key found in file", path.display()),
))
}
};
// Load accepted (rotation) keys
let mut accepted = Vec::new();
if let Some(ref path) = self.accepted_key_file {
let content = read_secret_file(path)?;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let key = Key::from_hex(line).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{}: {e}", path.display()),
)
})?;
accepted.push(key);
}
}
Ok(Some((primary, accepted)))
}
/// Load the master key for per-agent key derivation.
///
/// Returns `Ok(None)` when `--master-key-file` is not set.
///
/// # Errors
///
/// Returns an `io::Error` if the file cannot be read, the file does not
/// meet [`validate_secret_file`]'s hardened requirements, or the key
/// cannot be parsed as a 64-character hex string.
#[cfg(feature = "secure-udp")]
pub fn load_master_key(&self) -> std::io::Result<Option<varta_vlp::crypto::Key>> {
use varta_vlp::crypto::Key;
let Some(ref path) = self.master_key_file else {
return Ok(None);
};
let hex = read_secret_file(path)?;
Key::from_hex(hex.trim()).map(Some).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{}: {e}", path.display()),
)
})
}
#[cfg(feature = "prometheus-exporter")]
/// Load the Prometheus `/metrics` bearer token from
/// [`Self::prom_token_file`].
///
/// Returns `Ok(None)` when `--prom-token-file` is not set. The file is
/// validated through [`validate_secret_file`] (regular file, owned by
/// the observer UID, mode `0o600` or stricter, `O_NOFOLLOW` open) and
/// the contents must be exactly 64 hex characters (the same encoding
/// used by [`varta_vlp::crypto::Key`], so operators can reuse
/// `openssl rand -hex 32`).
///
/// # Errors
///
/// Returns an `io::Error` if the file fails validation or the contents
/// cannot be decoded as 64 hex characters.
pub fn load_prom_token(&self) -> std::io::Result<Option<varta_vlp::crypto::BearerToken>> {
use std::io;
let Some(ref path) = self.prom_token_file else {
return Ok(None);
};
let raw = validate_secret_file(path)?;
let trimmed = raw.trim();
let bytes = varta_vlp::decode_hex_32(trimmed.as_bytes()).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{}: {e}", path.display()),
)
})?;
Ok(Some(varta_vlp::crypto::BearerToken::from_bytes(bytes)))
}
}