1#![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 log::{debug, info};
12use nix::unistd::User;
13use rand::{RngExt, distr::Alphanumeric, rng};
14use signstar_common::{
15 ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
16 system_user::get_home_base_dir_path,
17};
18use signstar_config::config::{
19 AuthorizedKeyEntry,
20 Config,
21 MappingAuthorizedKeyEntry,
22 MappingSystemUserId,
23 SystemUserId,
24 SystemUserMapping,
25};
26#[cfg(feature = "nethsm")]
27use signstar_config::nethsm::NetHsmUserMapping;
28#[cfg(feature = "yubihsm2")]
29use signstar_config::yubihsm2::YubiHsm2UserMapping;
30use sysinfo::{Pid, System};
31
32#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
34mod impl_any {
35 use signstar_config::config::{UserBackendConnection, UserBackendConnectionFilter};
36
37 use super::*;
38
39 pub fn create_system_users(config: &Config) -> Result<(), Error> {
71 for user_backend_connection in config
73 .user_backend_connections(&[UserBackendConnectionFilter::NonAdmin])
74 .iter()
75 {
76 let user = {
77 let user = match user_backend_connection {
78 #[cfg(feature = "nethsm")]
79 UserBackendConnection::NetHsm {
80 admin_secret_handling: _,
81 non_admin_secret_handling: _,
82 connections: _,
83 mapping,
84 } => mapping.system_user_id(),
85 #[cfg(feature = "yubihsm2")]
86 UserBackendConnection::YubiHsm2 {
87 admin_secret_handling: _,
88 non_admin_secret_handling: _,
89 connections: _,
90 mapping,
91 } => mapping.system_user_id(),
92 };
93
94 let Some(user) = user else {
96 continue;
97 };
98 user
99 };
100
101 add_user_and_home(user)?;
102 add_tmpfilesd_integration(user)?;
103
104 let (ssh_force_command, authorized_key_entry) = {
105 match user_backend_connection {
106 #[cfg(feature = "nethsm")]
107 UserBackendConnection::NetHsm { mapping, .. } => (
108 SshForceCommand::try_from(mapping),
109 mapping.authorized_key_entry(),
110 ),
111 #[cfg(feature = "yubihsm2")]
112 UserBackendConnection::YubiHsm2 { mapping, .. } => (
113 SshForceCommand::try_from(mapping),
114 mapping.authorized_key_entry(),
115 ),
116 }
117 };
118
119 if let Ok(force_command) = ssh_force_command
120 && let Some(authorized_key) = authorized_key_entry
121 {
122 add_ssh_integration(user, authorized_key, &force_command)?;
123 }
124 }
125
126 for mapping in config.system().mappings() {
127 let Some(user) = mapping.system_user_id() else {
129 continue;
130 };
131 add_user_and_home(user)?;
132 add_tmpfilesd_integration(user)?;
133
134 let Some(authorized_key) = mapping.authorized_key_entry() else {
135 continue;
136 };
137 let force_command = SshForceCommand::from(mapping);
138 add_ssh_integration(user, authorized_key, &force_command)?;
139 }
140
141 Ok(())
142 }
143}
144
145#[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
147mod impl_none {
148 use super::*;
149
150 pub fn create_system_users(config: &Config) -> Result<(), Error> {
182 for mapping in config.system().mappings() {
183 let Some(user) = mapping.system_user_id() else {
185 continue;
186 };
187 add_user_and_home(user)?;
188 add_tmpfilesd_integration(user)?;
189
190 let Some(authorized_key) = mapping.authorized_key_entry() else {
191 continue;
192 };
193 let force_command = SshForceCommand::from(mapping);
194 add_ssh_integration(user, authorized_key, &force_command)?;
195 }
196
197 Ok(())
198 }
199}
200
201#[cfg(any(feature = "nethsm", feature = "yubihsm2"))]
202pub use impl_any::create_system_users;
203#[cfg(not(any(feature = "nethsm", feature = "yubihsm2")))]
204pub use impl_none::create_system_users;
205use yescrypt::{PasswordHasher, Yescrypt};
206
207pub mod cli;
208
209#[derive(Debug, thiserror::Error)]
211pub enum Error {
212 #[error("Configuration issue: {0}")]
214 Config(#[from] signstar_config::Error),
215
216 #[error(
218 "The command exited with non-zero status code (\"{exit_status}\") and produced the following output on stderr:\n{stderr}"
219 )]
220 CommandNonZero {
221 exit_status: ExitStatus,
223 stderr: String,
225 },
226
227 #[error("Unable to convert u32 to usize on this platform.")]
229 FailedU32ToUsizeConversion,
230
231 #[error(
233 "No SSH ForceCommand defined for user mapping (HSM users: {}{})",
234 backend_users.join(", "),
235 if let Some(system_user) = system_user {
236 format!(", system user: {}", system_user)
237 } else {
238 "".to_string()
239 }
240 )]
241 NoForceCommandForMapping {
242 backend_users: Vec<String>,
244 system_user: Option<String>,
246 },
247
248 #[error("The information on the current process could not be retrieved")]
250 NoProcess,
251
252 #[error("This application must be run as root!")]
254 NotRoot,
255
256 #[error("No user ID could be retrieved for the current process with PID {0}")]
258 NoUidForProcess(usize),
259
260 #[error("Password hash error while {context}: {source}")]
262 PasswordHash {
263 context: String,
267
268 source: yescrypt::password_hash::Error,
270 },
271
272 #[error("The string {0} could not be converted to a \"sysinfo::Uid\"")]
274 SysUidFromStr(String),
275
276 #[error(
278 "The Path value {path} for the tmpfiles.d integration for {user} is not valid:\n{reason}"
279 )]
280 TmpfilesDPath {
281 path: String,
283 user: SystemUserId,
285 reason: &'static str,
292 },
293
294 #[error("Adding user {user} failed:\n{source}")]
296 UserAdd {
297 user: SystemUserId,
299 source: std::io::Error,
301 },
302
303 #[error("Modifying the user {user} failed:\n{source}")]
305 UserMod {
306 user: SystemUserId,
308 source: std::io::Error,
310 },
311
312 #[error("Getting a system user for the username {user} failed:\n{source}")]
314 UserNameConversion {
315 user: SystemUserId,
317 source: nix::Error,
319 },
320
321 #[error("Writing authorized_keys file for {user} failed:\n{source}")]
323 WriteAuthorizedKeys {
324 user: SystemUserId,
326 source: std::io::Error,
328 },
329
330 #[error("Writing sshd_config drop-in for {user} failed:\n{source}")]
332 WriteSshdConfig {
333 user: SystemUserId,
335 source: std::io::Error,
337 },
338
339 #[error("Writing tmpfiles.d integration for {user} failed:\n{source}")]
341 WriteTmpfilesD {
342 user: SystemUserId,
344 source: std::io::Error,
346 },
347}
348
349fn add_user_and_home(user: &SystemUserId) -> Result<(), Error> {
368 if User::from_name(user.as_ref())
370 .map_err(|source| Error::UserNameConversion {
371 user: user.clone(),
372 source,
373 })?
374 .is_none()
375 {
376 let home_base_dir = get_home_base_dir_path();
377
378 info!("Creating user \"{user}\"...");
380 let user_add = Command::new("useradd")
381 .arg("--base-dir")
382 .arg(home_base_dir.as_path())
383 .arg("--user-group")
384 .arg("--groups")
385 .arg("_yubihsm2")
386 .arg("--shell")
387 .arg("/usr/bin/bash")
388 .arg(user.as_ref())
389 .output()
390 .map_err(|error| Error::UserAdd {
391 user: user.clone(),
392 source: error,
393 })?;
394
395 if !user_add.status.success() {
396 return Err(Error::CommandNonZero {
397 exit_status: user_add.status,
398 stderr: String::from_utf8_lossy(&user_add.stderr).into_owned(),
399 });
400 }
401 debug!("{}", String::from_utf8_lossy(&user_add.stdout));
402 } else {
403 debug!("Skipping existing user \"{user}\"...");
404 }
405
406 let random_passphrase: String = rng()
408 .sample_iter(&Alphanumeric)
409 .take(30)
410 .map(char::from)
411 .collect();
412 let yescrypt = Yescrypt::default();
413 let passphrase_hash = yescrypt
414 .hash_password(random_passphrase.as_bytes())
415 .map_err(|source| Error::PasswordHash {
416 context: format!("creating a passphrase hash for user {user}"),
417 source,
418 })?;
419 let mut command = Command::new("usermod");
420 command.arg("--password");
421 command.arg(passphrase_hash.as_str());
422 command.arg(user.as_ref());
423 let command_output = command.output().map_err(|source| Error::UserMod {
424 user: user.clone(),
425 source,
426 })?;
427
428 if !command_output.status.success() {
429 return Err(Error::CommandNonZero {
430 exit_status: command_output.status,
431 stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
432 });
433 }
434 debug!("{}", String::from_utf8_lossy(&command_output.stdout));
435
436 Ok(())
437}
438
439fn add_tmpfilesd_integration(user: &SystemUserId) -> Result<(), Error> {
450 info!("Adding tmpfiles.d integration for user \"{user}\"...");
452
453 let mut buffer = File::create(format!("/usr/lib/tmpfiles.d/signstar-user-{user}.conf"))
454 .map_err(|source| Error::WriteTmpfilesD {
455 user: user.clone(),
456 source,
457 })?;
458 let home_base_dir = get_home_base_dir_path();
459
460 let home_dir = {
464 let home_dir = format!("{}/{user}", home_base_dir.to_string_lossy()).replace(" ", "\\x20");
465 if home_dir.contains("%") {
466 return Err(Error::TmpfilesDPath {
467 path: home_dir.clone(),
468 user: user.clone(),
469 reason: "Specifiers (%) are not supported at this point.",
470 });
471 }
472 home_dir
473 };
474
475 buffer
476 .write_all(format!("d {home_dir} 700 {user} {user}\n",).as_bytes())
477 .map_err(|source| Error::WriteTmpfilesD {
478 user: user.clone(),
479 source,
480 })?;
481
482 Ok(())
483}
484
485fn add_ssh_integration(
498 user: &SystemUserId,
499 authorized_key: &AuthorizedKeyEntry,
500 force_command: &SshForceCommand,
501) -> Result<(), Error> {
502 info!("Adding SSH authorized_keys file for user \"{user}\"...");
503 {
504 let mut buffer = File::create(
505 get_ssh_authorized_key_base_dir().join(format!("signstar-user-{user}.authorized_keys")),
506 )
507 .map_err(|source| Error::WriteAuthorizedKeys {
508 user: user.clone(),
509 source,
510 })?;
511 buffer
512 .write_all(authorized_key.to_string().as_bytes())
513 .map_err(|source| Error::WriteAuthorizedKeys {
514 user: user.clone(),
515 source,
516 })?;
517 }
518
519 info!("Adding sshd_config drop-in configuration for user \"{user}\"...");
521 {
522 let mut buffer = File::create(
523 get_sshd_config_dropin_dir().join(format!("10-signstar-user-{user}.conf")),
524 )
525 .map_err(|source| Error::WriteSshdConfig {
526 user: user.clone(),
527 source,
528 })?;
529 buffer
530 .write_all(
531 format!(
532 r#"Match user {user}
533 AuthorizedKeysFile /etc/ssh/signstar-user-{user}.authorized_keys
534 ForceCommand /usr/bin/{force_command}
535"#
536 )
537 .as_bytes(),
538 )
539 .map_err(|source| Error::WriteSshdConfig {
540 user: user.clone(),
541 source,
542 })?;
543 }
544
545 Ok(())
546}
547
548#[derive(Clone, Debug)]
550pub struct ConfigPath(PathBuf);
551
552impl ConfigPath {
553 pub fn new(path: PathBuf) -> Self {
555 Self(path)
556 }
557}
558
559impl AsRef<Path> for ConfigPath {
560 fn as_ref(&self) -> &Path {
561 self.0.as_path()
562 }
563}
564
565impl Default for ConfigPath {
566 fn default() -> Self {
571 Self(Config::first_existing_system_path().unwrap_or(Config::default_system_path()))
572 }
573}
574
575impl From<PathBuf> for ConfigPath {
576 fn from(value: PathBuf) -> Self {
577 Self(value)
578 }
579}
580
581impl FromStr for ConfigPath {
582 type Err = Error;
583 fn from_str(s: &str) -> Result<Self, Self::Err> {
584 Ok(Self::new(PathBuf::from(s)))
585 }
586}
587
588#[derive(strum::AsRefStr, Debug, strum::Display, strum::EnumString, strum::VariantNames)]
596pub enum SshForceCommand {
597 #[strum(serialize = "signstar-download-backup")]
599 DownloadBackup,
600
601 #[strum(serialize = "signstar-download-key-certificate")]
603 DownloadKeyCertificate,
604
605 #[strum(serialize = "signstar-download-metrics")]
607 DownloadMetrics,
608
609 #[strum(serialize = "signstar-shareholder")]
611 Shareholder,
612
613 #[strum(serialize = "signstar-download-wireguard")]
615 DownloadWireGuard,
616
617 #[strum(serialize = "signstar-sign")]
619 Sign,
620
621 #[strum(serialize = "signstar-upload-backup")]
623 UploadBackup,
624
625 #[strum(serialize = "signstar-upload-update")]
627 UploadUpdate,
628}
629
630impl From<&SystemUserMapping> for SshForceCommand {
631 fn from(value: &SystemUserMapping) -> Self {
632 match value {
633 SystemUserMapping::ShareHolder { .. } => SshForceCommand::Shareholder,
634 SystemUserMapping::WireGuardDownload { .. } => SshForceCommand::DownloadWireGuard,
635 }
636 }
637}
638
639#[cfg(feature = "nethsm")]
640impl TryFrom<&NetHsmUserMapping> for SshForceCommand {
641 type Error = Error;
642
643 fn try_from(value: &NetHsmUserMapping) -> Result<Self, Self::Error> {
644 match value {
645 NetHsmUserMapping::Admin(admin) => Err(Error::NoForceCommandForMapping {
646 backend_users: vec![admin.to_string()],
647 system_user: None,
648 }),
649 NetHsmUserMapping::Backup { .. } => Ok(Self::DownloadBackup),
650 NetHsmUserMapping::HermeticMetrics {
651 backend_users,
652 system_user,
653 } => Err(Error::NoForceCommandForMapping {
654 backend_users: backend_users
655 .get_users()
656 .iter()
657 .map(|user| user.to_string())
658 .collect(),
659 system_user: Some(system_user.to_string()),
660 }),
661 NetHsmUserMapping::Metrics { .. } => Ok(Self::DownloadMetrics),
662 NetHsmUserMapping::Signing { .. } => Ok(SshForceCommand::Sign),
663 }
664 }
665}
666
667#[cfg(feature = "yubihsm2")]
668impl TryFrom<&YubiHsm2UserMapping> for SshForceCommand {
669 type Error = Error;
670
671 fn try_from(value: &YubiHsm2UserMapping) -> Result<Self, Self::Error> {
672 match value {
673 YubiHsm2UserMapping::Admin {
674 authentication_key_id,
675 } => Err(Error::NoForceCommandForMapping {
676 backend_users: vec![authentication_key_id.to_string()],
677 system_user: None,
678 }),
679 YubiHsm2UserMapping::AuditLog { .. } => Ok(SshForceCommand::DownloadMetrics),
680 YubiHsm2UserMapping::Backup { .. } => Ok(SshForceCommand::DownloadBackup),
681 YubiHsm2UserMapping::HermeticAuditLog {
682 authentication_key_id,
683 system_user,
684 } => Err(Error::NoForceCommandForMapping {
685 backend_users: vec![authentication_key_id.to_string()],
686 system_user: Some(system_user.to_string()),
687 }),
688 YubiHsm2UserMapping::Signing { .. } => Ok(SshForceCommand::Sign),
689 }
690 }
691}
692
693pub fn ensure_root() -> Result<(), Error> {
705 let pid: usize = id()
706 .try_into()
707 .map_err(|_| Error::FailedU32ToUsizeConversion)?;
708
709 let system = System::new_all();
710 let Some(process) = system.process(Pid::from(pid)) else {
711 return Err(Error::NoProcess);
712 };
713
714 let Some(uid) = process.effective_user_id() else {
715 return Err(Error::NoUidForProcess(pid));
716 };
717
718 let root_uid_str = "0";
719 let root_uid = sysinfo::Uid::from_str(root_uid_str)
720 .map_err(|_| Error::SysUidFromStr(root_uid_str.to_string()))?;
721
722 if uid.ne(&root_uid) {
723 return Err(Error::NotRoot);
724 }
725
726 Ok(())
727}