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}