signstar_crypto/secret_file/non_admin.rs
1//! Reading and writing of non-administrative secrets.
2
3use std::{
4 fs::{File, Permissions, create_dir_all, read_to_string, set_permissions},
5 io::Write,
6 os::unix::fs::{PermissionsExt, chown},
7 path::PathBuf,
8 process::{Command, Stdio},
9};
10
11use change_user_run::get_command;
12use log::info;
13use nix::unistd::{User, geteuid};
14use serde::{Deserialize, Serialize};
15use signstar_common::{
16 common::SECRET_FILE_MODE,
17 system_user::{
18 get_home_base_dir_path,
19 get_plaintext_secret_file,
20 get_systemd_creds_secret_file,
21 get_user_secrets_dir,
22 },
23};
24
25use crate::{
26 passphrase::Passphrase,
27 secret_file::{Error, common::check_secrets_file},
28};
29
30/// The handling of non-administrative secrets.
31///
32/// Non-administrative secrets represent passphrases for (non-administrator) HSM users and may be
33/// handled in different ways (e.g. encrypted or not encrypted).
34#[derive(
35 Clone,
36 Copy,
37 Debug,
38 Default,
39 Deserialize,
40 strum::Display,
41 strum::EnumString,
42 Eq,
43 Ord,
44 PartialEq,
45 PartialOrd,
46 Serialize,
47)]
48#[serde(rename_all = "kebab-case")]
49#[strum(serialize_all = "kebab-case")]
50pub enum NonAdministrativeSecretHandling {
51 /// Each non-administrative secret is handled in a plaintext file in a non-volatile
52 /// directory.
53 ///
54 /// ## Warning
55 ///
56 /// This variant should only be used in non-production test setups, as it implies the
57 /// persistence of unencrypted non-administrative secrets on a file system.
58 Plaintext,
59
60 /// Each non-administrative secret is encrypted for a specific system user using
61 /// [systemd-creds(1)] and the resulting files are stored in a non-volatile directory.
62 ///
63 /// ## Note
64 ///
65 /// Although secrets are stored as encrypted strings in dedicated files, they may be extracted
66 /// under certain circumstances:
67 ///
68 /// - the root account is compromised
69 /// - decrypts and exfiltrates _all_ secrets
70 /// - the secret is not encrypted using a [TPM] and the file
71 /// `/var/lib/systemd/credential.secret` as well as _any_ encrypted secret is exfiltrated
72 /// - a specific user is compromised, decrypts and exfiltrates its own secret
73 ///
74 /// It is therefore crucial to follow common best-practices:
75 ///
76 /// - rely on a [TPM] for encrypting secrets, so that files become host-specific
77 /// - heavily guard access to all users, especially root
78 ///
79 /// [systemd-creds(1)]: https://man.archlinux.org/man/systemd-creds.1
80 /// [TPM]: https://en.wikipedia.org/wiki/Trusted_Platform_Module
81 #[default]
82 SystemdCreds,
83}
84
85/// Writes a [`Passphrase`] to a secret file location of a system user.
86///
87/// The secret file location is established based on the chosen `secret_handling`, `system_user` and
88/// `backend_user`.
89///
90/// # Note
91///
92/// This function must be run as root, as the secrets file is created for a specific `system_user`
93/// and the ownership of the resulting secrets file is adjusted in such a way that the
94/// `system_user` has access.
95///
96/// # Errors
97///
98/// Returns an error if
99///
100/// - the effective user ID of the calling user is not that of root
101/// - the secret is a plaintext file, but reading it as a string fails
102/// - the secret needs to be encrypted using [systemd-creds(1)], but
103/// - [systemd-creds(1)] cannot be found or the [systemd-creds(1)] command
104/// - cannot be spawned in the background
105/// - cannot be attached to on stdin in the background
106/// - cannot be written to on its stdin
107/// - fails to execute
108/// - returned with a non-zero exit code
109/// - the file at `path` cannot be created
110/// - the file at `path` cannot be written to
111/// - the ownership of file at `path` cannot be changed to that of [systemd-creds(1)]
112/// - the file permissions of the file at `path` cannot be adjusted
113///
114/// [systemd-creds(1)]: https://man.archlinux.org/man/systemd-creds.1
115pub fn write_passphrase_to_secrets_file(
116 secret_handling: NonAdministrativeSecretHandling,
117 system_user: &User,
118 backend_user: &str,
119 passphrase: &Passphrase,
120) -> Result<(), crate::Error> {
121 let path = match secret_handling {
122 NonAdministrativeSecretHandling::Plaintext => {
123 get_plaintext_secret_file(&system_user.name, backend_user)
124 }
125 NonAdministrativeSecretHandling::SystemdCreds => {
126 get_systemd_creds_secret_file(&system_user.name, backend_user)
127 }
128 };
129
130 if !geteuid().is_root() {
131 return Err(Error::NotRunningAsRoot {
132 context: format!(
133 "writing a passphrase to secrets file at path {path:?} for system user {} and backend user {backend_user}",
134 system_user.name
135 ),
136 }
137 .into());
138 }
139
140 info!(
141 "Write passphrase to secrets file {path:?} of system user {} and backend user {backend_user}",
142 system_user.name
143 );
144
145 create_secrets_dir(system_user)?;
146
147 let secret = {
148 // Create credentials files depending on secret handling
149 match secret_handling {
150 NonAdministrativeSecretHandling::Plaintext => {
151 passphrase.expose_borrowed().as_bytes().to_vec()
152 }
153 NonAdministrativeSecretHandling::SystemdCreds => {
154 // Create systemd-creds encrypted secret.
155 let creds_command = get_command("systemd-creds")?;
156 let mut command = Command::new(creds_command);
157 let command = command
158 .arg("--user")
159 .arg("--name=")
160 .arg("--uid")
161 .arg(system_user.name.as_str())
162 .arg("encrypt")
163 .arg("-")
164 .arg("-")
165 .stdin(Stdio::piped())
166 .stdout(Stdio::piped())
167 .stderr(Stdio::piped());
168 let mut command_child =
169 command.spawn().map_err(|source| Error::CommandBackground {
170 command: format!("{command:?}"),
171 source,
172 })?;
173
174 // write to stdin
175 command_child
176 .stdin
177 .take()
178 .ok_or(Error::CommandAttachToStdin {
179 command: format!("{command:?}"),
180 })?
181 .write_all(passphrase.expose_borrowed().as_bytes())
182 .map_err(|source| Error::CommandWriteToStdin {
183 command: format!("{command:?}"),
184 source,
185 })?;
186
187 let command_output =
188 command_child
189 .wait_with_output()
190 .map_err(|source| Error::CommandExec {
191 command: format!("{command:?}"),
192 source,
193 })?;
194
195 if !command_output.status.success() {
196 return Err(Error::CommandNonZero {
197 command: format!("{command:?}"),
198 exit_status: command_output.status,
199 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
200 }
201 .into());
202 }
203 command_output.stdout
204 }
205 }
206 };
207
208 // Write secret to file and adjust permission and ownership of file.
209 let mut file = File::create(&path).map_err(|source| Error::SecretsFileCreate {
210 path: path.clone(),
211 system_user: system_user.name.clone(),
212 source,
213 })?;
214 file.write_all(&secret)
215 .map_err(|source| Error::SecretsFileWrite {
216 path: path.to_path_buf(),
217 system_user: system_user.name.clone(),
218 source,
219 })?;
220 chown(
221 &path,
222 Some(system_user.uid.as_raw()),
223 Some(system_user.gid.as_raw()),
224 )
225 .map_err(|source| Error::Chown {
226 path: path.clone(),
227 user: system_user.name.clone(),
228 source,
229 })?;
230 set_permissions(&path, Permissions::from_mode(SECRET_FILE_MODE)).map_err(|source| {
231 Error::ApplyPermissions {
232 path: path.clone(),
233 mode: SECRET_FILE_MODE,
234 source,
235 }
236 })?;
237
238 Ok(())
239}
240
241/// Reads a secret from a secret file location of a user and returns it as a [`Passphrase`].
242///
243/// The secret file location is established based on the chosen `secret_handling`, `system_user` and
244/// `backend_user`.
245///
246/// # Notes
247///
248/// This function must be called using an unprivileged user, as the `path` is assumed to be in that
249/// user's home directory.
250/// If [systemd-creds(1)] based encryption is used, then the same user used to encrypt the secret
251/// must be used to decrypt the secret.
252///
253/// # Errors
254///
255/// Returns an error if
256///
257/// - the effective user ID of the calling user is that of root,
258/// - the secret is a plaintext file, but reading it as a string fails,
259/// - the secret is encrypted using [systemd-creds(1)], but
260/// - [systemd-creds(1)] cannot be found,
261/// - or the [systemd-creds(1)] command fails to execute,
262/// - or the [systemd-creds(1)] command returned with a non-zero exit code,
263/// - or the returned output cannot be converted into valid UTF-8 string
264///
265/// [systemd-creds(1)]: https://man.archlinux.org/man/systemd-creds.1
266pub fn load_passphrase_from_secrets_file(
267 secret_handling: NonAdministrativeSecretHandling,
268 system_user: &User,
269 backend_user: &str,
270) -> Result<Passphrase, crate::Error> {
271 if geteuid().is_root() {
272 return Err(Error::RunningAsRoot {
273 target_user: system_user.name.clone(),
274 context: format!("loading a passphrase from secrets file for system user {} and backend user {backend_user}",
275 system_user.name
276 ),
277 }
278 .into());
279 }
280
281 let path = match secret_handling {
282 NonAdministrativeSecretHandling::Plaintext => {
283 get_plaintext_secret_file(&system_user.name, backend_user)
284 }
285 NonAdministrativeSecretHandling::SystemdCreds => {
286 get_systemd_creds_secret_file(&system_user.name, backend_user)
287 }
288 };
289
290 info!(
291 "Load passphrase from secrets file {path:?} for system user {} and backend user {backend_user}",
292 system_user.name
293 );
294
295 check_secrets_file(&path)?;
296
297 match secret_handling {
298 // Read from plaintext secrets file.
299 NonAdministrativeSecretHandling::Plaintext => Ok(Passphrase::new(
300 read_to_string(&path).map_err(|source| Error::IoPath {
301 path: path.clone(),
302 context: "reading the secrets file as a string",
303 source,
304 })?,
305 )),
306 // Read from systemd-creds encrypted secrets file.
307 NonAdministrativeSecretHandling::SystemdCreds => {
308 let creds_command = get_command("systemd-creds")?;
309 let mut command = Command::new(creds_command);
310 let command = command.arg("--user").arg("decrypt").arg(&path).arg("-");
311 let command_output = command.output().map_err(|source| Error::CommandExec {
312 command: format!("{command:?}"),
313 source,
314 })?;
315
316 if !command_output.status.success() {
317 return Err(Error::CommandNonZero {
318 command: format!("{command:?}"),
319 exit_status: command_output.status,
320 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
321 }
322 .into());
323 }
324
325 Ok(Passphrase::new(
326 String::from_utf8(command_output.stdout).map_err(|source| Error::Utf8String {
327 path: path.clone(),
328 context: format!("converting stdout of {command:?} to string"),
329 source,
330 })?,
331 ))
332 }
333 }
334}
335
336/// Creates the secrets directory for a [`User`].
337///
338/// Creates the secrets directory for the user and ensures correct ownership of it and all
339/// parent directories up until the user's home directory.
340///
341/// # Note
342///
343/// Relies on [`get_user_secrets_dir`] to retrieve the secrets dir for the `system_user`.
344///
345/// # Errors
346///
347/// Returns an error if
348///
349/// - the effective user ID of the calling process is not that of root,
350/// - the directory or one of its parents could not be created,
351/// - the ownership of any directory between the user's home and the secrets directory can not be
352/// changed
353pub(crate) fn create_secrets_dir(system_user: &User) -> Result<(), crate::Error> {
354 if !geteuid().is_root() {
355 return Err(Error::NotRunningAsRoot {
356 context: format!("creating secrets dir for user {}", system_user.name),
357 }
358 .into());
359 }
360
361 // get and create the user's passphrase directory
362 let path = get_user_secrets_dir(&system_user.name);
363 create_dir_all(&path).map_err(|source| crate::secret_file::Error::SecretsDirCreate {
364 path: path.clone(),
365 system_user: system_user.name.clone(),
366 source,
367 })?;
368
369 // Recursively chown all directories to the user and group, until `HOME_BASE_DIR` is
370 // reached.
371 let home_dir = get_home_base_dir_path().join(PathBuf::from(&system_user.name));
372 let mut chown_dir = path.clone();
373 while chown_dir != home_dir {
374 chown(
375 &chown_dir,
376 Some(system_user.uid.as_raw()),
377 Some(system_user.gid.as_raw()),
378 )
379 .map_err(|source| Error::Chown {
380 path: chown_dir.to_path_buf(),
381 user: system_user.name.clone(),
382 source,
383 })?;
384 if let Some(parent) = &chown_dir.parent() {
385 chown_dir = parent.to_path_buf()
386 } else {
387 break;
388 }
389 }
390
391 Ok(())
392}