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}