Skip to main content

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