Skip to main content

varta_watch/config/
loaders.rs

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