signstar_configure_build/
lib.rs1#![doc = include_str!("../README.md")]
2
3use std::{
4 fs::File,
5 io::Write,
6 path::{Path, PathBuf},
7 process::{Command, ExitStatus, id},
8 str::FromStr,
9};
10
11use nix::unistd::User;
12use signstar_common::{
13 config::get_config_file_or_default,
14 ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
15 system_user::get_home_base_dir_path,
16};
17use signstar_config::{SignstarConfig, SystemUserId, UserMapping};
18use sysinfo::{Pid, System};
19
20pub mod cli;
21
22#[derive(Debug, thiserror::Error)]
24pub enum Error {
25 #[error("Configuration issue: {0}")]
27 Config(#[from] signstar_config::Error),
28
29 #[error(
31 "The command exited with non-zero status code (\"{exit_status}\") and produced the following output on stderr:\n{stderr}"
32 )]
33 CommandNonZero {
34 exit_status: ExitStatus,
36 stderr: String,
38 },
39
40 #[error("Unable to convert u32 to usize on this platform.")]
42 FailedU32ToUsizeConversion,
43
44 #[error(
46 "No SSH ForceCommand defined for user mapping (HSM users: {}{})",
47 backend_users.join(", "),
48 if let Some(system_user) = system_user {
49 format!(", system user: {}", system_user)
50 } else {
51 "".to_string()
52 }
53 )]
54 NoForceCommandForMapping {
55 backend_users: Vec<String>,
57 system_user: Option<String>,
59 },
60
61 #[error("The information on the current process could not be retrieved")]
63 NoProcess,
64
65 #[error("This application must be run as root!")]
67 NotRoot,
68
69 #[error("No user ID could be retrieved for the current process with PID {0}")]
71 NoUidForProcess(usize),
72
73 #[error("The string {0} could not be converted to a \"sysinfo::Uid\"")]
75 SysUidFromStr(String),
76
77 #[error(
79 "The Path value {path} for the tmpfiles.d integration for {user} is not valid:\n{reason}"
80 )]
81 TmpfilesDPath {
82 path: String,
84 user: SystemUserId,
86 reason: &'static str,
93 },
94
95 #[error("Adding user {user} failed:\n{source}")]
97 UserAdd {
98 user: SystemUserId,
100 source: std::io::Error,
102 },
103
104 #[error("Modifying the user {user} failed:\n{source}")]
106 UserMod {
107 user: SystemUserId,
109 source: std::io::Error,
111 },
112
113 #[error("Getting a system user for the username {user} failed:\n{source}")]
115 UserNameConversion {
116 user: SystemUserId,
118 source: nix::Error,
120 },
121
122 #[error("Writing authorized_keys file for {user} failed:\n{source}")]
124 WriteAuthorizedKeys {
125 user: SystemUserId,
127 source: std::io::Error,
129 },
130
131 #[error("Writing sshd_config drop-in for {user} failed:\n{source}")]
133 WriteSshdConfig {
134 user: SystemUserId,
136 source: std::io::Error,
138 },
139
140 #[error("Writing tmpfiles.d integration for {user} failed:\n{source}")]
142 WriteTmpfilesD {
143 user: SystemUserId,
145 source: std::io::Error,
147 },
148}
149
150#[derive(Clone, Debug)]
154pub struct ConfigPath(PathBuf);
155
156impl ConfigPath {
157 pub fn new(path: PathBuf) -> Self {
159 Self(path)
160 }
161}
162
163impl AsRef<Path> for ConfigPath {
164 fn as_ref(&self) -> &Path {
165 self.0.as_path()
166 }
167}
168
169impl Default for ConfigPath {
170 fn default() -> Self {
175 Self(get_config_file_or_default())
176 }
177}
178
179impl From<PathBuf> for ConfigPath {
180 fn from(value: PathBuf) -> Self {
181 Self(value)
182 }
183}
184
185impl FromStr for ConfigPath {
186 type Err = Error;
187 fn from_str(s: &str) -> Result<Self, Self::Err> {
188 Ok(Self::new(PathBuf::from(s)))
189 }
190}
191
192#[derive(strum::AsRefStr, Debug, strum::Display, strum::EnumString, strum::VariantNames)]
200pub enum SshForceCommand {
201 #[strum(serialize = "signstar-download-backup")]
203 DownloadBackup,
204
205 #[strum(serialize = "signstar-download-key-certificate")]
207 DownloadKeyCertificate,
208
209 #[strum(serialize = "signstar-download-metrics")]
211 DownloadMetrics,
212
213 #[strum(serialize = "signstar-download-secret-share")]
215 DownloadSecretShare,
216
217 #[strum(serialize = "signstar-download-wireguard")]
219 DownloadWireGuard,
220
221 #[strum(serialize = "signstar-sign")]
223 Sign,
224
225 #[strum(serialize = "signstar-upload-backup")]
227 UploadBackup,
228
229 #[strum(serialize = "signstar-upload-secret-share")]
231 UploadSecretShare,
232
233 #[strum(serialize = "signstar-upload-update")]
235 UploadUpdate,
236}
237
238impl TryFrom<&UserMapping> for SshForceCommand {
239 type Error = Error;
240
241 fn try_from(value: &UserMapping) -> Result<Self, Self::Error> {
242 match value {
243 UserMapping::SystemNetHsmBackup {
244 nethsm_user: _,
245 ssh_authorized_key: _,
246 system_user: _,
247 } => Ok(Self::DownloadBackup),
248 UserMapping::SystemNetHsmMetrics {
249 nethsm_users: _,
250 ssh_authorized_key: _,
251 system_user: _,
252 } => Ok(Self::DownloadMetrics),
253 UserMapping::SystemNetHsmOperatorSigning {
254 nethsm_user: _,
255 key_id: _,
256 nethsm_key_setup: _,
257 ssh_authorized_key: _,
258 system_user: _,
259 tag: _,
260 } => Ok(Self::Sign),
261 #[cfg(feature = "yubihsm2")]
262 UserMapping::SystemYubiHsm2Backup { .. } => Ok(Self::DownloadBackup),
263 #[cfg(feature = "yubihsm2")]
264 UserMapping::SystemYubiHsm2Metrics { .. } => Ok(Self::DownloadMetrics),
265 #[cfg(feature = "yubihsm2")]
266 UserMapping::HermeticSystemYubiHsm2Metrics {
267 authentication_key_id,
268 system_user,
269 } => Err(Error::NoForceCommandForMapping {
270 backend_users: vec![authentication_key_id.to_string()],
271 system_user: Some(system_user.to_string()),
272 }),
273 #[cfg(feature = "yubihsm2")]
274 UserMapping::SystemYubiHsmOperatorSigning { .. } => Ok(Self::Sign),
275 UserMapping::SystemOnlyShareDownload {
276 system_user: _,
277 ssh_authorized_key: _,
278 } => Ok(SshForceCommand::DownloadSecretShare),
279 UserMapping::SystemOnlyShareUpload {
280 system_user: _,
281 ssh_authorized_key: _,
282 } => Ok(SshForceCommand::UploadSecretShare),
283 UserMapping::SystemOnlyWireGuardDownload {
284 system_user: _,
285 ssh_authorized_key: _,
286 } => Ok(SshForceCommand::DownloadWireGuard),
287 #[cfg(feature = "yubihsm2")]
288 UserMapping::YubiHsmOnlyAdmin(admin) => Err(Error::NoForceCommandForMapping {
289 backend_users: vec![admin.to_string()],
290 system_user: None,
291 }),
292 UserMapping::NetHsmOnlyAdmin(_)
293 | UserMapping::HermeticSystemNetHsmMetrics {
294 nethsm_users: _,
295 system_user: _,
296 } => Err(Error::NoForceCommandForMapping {
297 backend_users: value
298 .get_nethsm_users()
299 .iter()
300 .map(|user| user.to_string())
301 .collect(),
302 system_user: value.get_system_user().map(|user| user.to_string()),
303 }),
304 }
305 }
306}
307
308pub fn ensure_root() -> Result<(), Error> {
320 let pid: usize = id()
321 .try_into()
322 .map_err(|_| Error::FailedU32ToUsizeConversion)?;
323
324 let system = System::new_all();
325 let Some(process) = system.process(Pid::from(pid)) else {
326 return Err(Error::NoProcess);
327 };
328
329 let Some(uid) = process.effective_user_id() else {
330 return Err(Error::NoUidForProcess(pid));
331 };
332
333 let root_uid_str = "0";
334 let root_uid = sysinfo::Uid::from_str(root_uid_str)
335 .map_err(|_| Error::SysUidFromStr(root_uid_str.to_string()))?;
336
337 if uid.ne(&root_uid) {
338 return Err(Error::NotRoot);
339 }
340
341 Ok(())
342}
343
344pub fn create_system_users(config: &SignstarConfig) -> Result<(), Error> {
376 for mapping in config.iter_user_mappings() {
377 let Some(user) = mapping.get_system_user() else {
379 continue;
380 };
381
382 if User::from_name(user.as_ref())
384 .map_err(|source| Error::UserNameConversion {
385 user: user.clone(),
386 source,
387 })?
388 .is_some()
389 {
390 eprintln!("Skipping existing user \"{user}\"...");
391 continue;
392 }
393
394 let home_base_dir = get_home_base_dir_path();
395
396 print!("Creating user \"{user}\"...");
398 let user_add = Command::new("useradd")
399 .arg("--base-dir")
400 .arg(home_base_dir.as_path())
401 .arg("--user-group")
402 .arg("--shell")
403 .arg("/usr/bin/bash")
404 .arg(user.as_ref())
405 .output()
406 .map_err(|error| Error::UserAdd {
407 user: user.clone(),
408 source: error,
409 })?;
410
411 if !user_add.status.success() {
412 return Err(Error::CommandNonZero {
413 exit_status: user_add.status,
414 stderr: String::from_utf8_lossy(&user_add.stderr).into_owned(),
415 });
416 } else {
417 println!(" Done.");
418 }
419
420 print!("Unlocking user \"{user}\"...");
422 let user_mod = Command::new("usermod")
423 .args(["--unlock", user.as_ref()])
424 .output()
425 .map_err(|source| Error::UserMod {
426 user: user.clone(),
427 source,
428 })?;
429
430 if !user_mod.status.success() {
431 return Err(Error::CommandNonZero {
432 exit_status: user_mod.status,
433 stderr: String::from_utf8_lossy(&user_mod.stderr).into_owned(),
434 });
435 } else {
436 println!(" Done.");
437 }
438
439 print!("Adding tmpfiles.d integration for user \"{user}\"...");
441 {
442 let mut buffer = File::create(format!("/usr/lib/tmpfiles.d/signstar-user-{user}.conf"))
443 .map_err(|source| Error::WriteTmpfilesD {
444 user: user.clone(),
445 source,
446 })?;
447
448 let home_dir = {
452 let home_dir =
453 format!("{}/{user}", home_base_dir.to_string_lossy()).replace(" ", "\\x20");
454 if home_dir.contains("%") {
455 return Err(Error::TmpfilesDPath {
456 path: home_dir.clone(),
457 user: user.clone(),
458 reason: "Specifiers (%) are not supported at this point.",
459 });
460 }
461 home_dir
462 };
463
464 buffer
465 .write_all(format!("d {home_dir} 700 {user} {user}\n",).as_bytes())
466 .map_err(|source| Error::WriteTmpfilesD {
467 user: user.clone(),
468 source,
469 })?;
470 }
471 println!(" Done.");
472
473 if let Ok(force_command) = SshForceCommand::try_from(mapping)
474 && let Some(authorized_key) = mapping.get_ssh_authorized_key()
475 {
476 print!("Adding SSH authorized_keys file for user \"{user}\"...");
478 {
479 let mut buffer = File::create(
480 get_ssh_authorized_key_base_dir()
481 .join(format!("signstar-user-{user}.authorized_keys")),
482 )
483 .map_err(|source| Error::WriteAuthorizedKeys {
484 user: user.clone(),
485 source,
486 })?;
487 buffer
488 .write_all(authorized_key.as_ref().as_bytes())
489 .map_err(|source| Error::WriteAuthorizedKeys {
490 user: user.clone(),
491 source,
492 })?;
493 }
494 println!(" Done.");
495
496 print!("Adding sshd_config drop-in configuration for user \"{user}\"...");
498 {
499 let mut buffer = File::create(
500 get_sshd_config_dropin_dir().join(format!("10-signstar-user-{user}.conf")),
501 )
502 .map_err(|source| Error::WriteSshdConfig {
503 user: user.clone(),
504 source,
505 })?;
506 buffer
507 .write_all(
508 format!(
509 r#"Match user {user}
510 AuthorizedKeysFile /etc/ssh/signstar-user-{user}.authorized_keys
511 ForceCommand /usr/bin/{force_command}
512"#
513 )
514 .as_bytes(),
515 )
516 .map_err(|source| Error::WriteSshdConfig {
517 user: user.clone(),
518 source,
519 })?;
520 }
521 println!(" Done.");
522 };
523 }
524
525 Ok(())
526}