signstar_configure_build/
lib.rs1use std::{
2 fs::File,
3 io::Write,
4 path::{Path, PathBuf},
5 process::{Command, ExitStatus, id},
6 str::FromStr,
7};
8
9use nix::unistd::User;
10use signstar_common::{
11 config::get_config_file_or_default,
12 ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
13 system_user::get_home_base_dir_path,
14};
15use signstar_config::{SignstarConfig, SystemUserId, UserMapping};
16use sysinfo::{Pid, System};
17
18pub mod cli;
19
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22 #[error("Configuration issue: {0}")]
24 Config(#[from] signstar_config::Error),
25
26 #[error(
28 "The command exited with non-zero status code (\"{exit_status}\") and produced the following output on stderr:\n{stderr}"
29 )]
30 CommandNonZero {
31 exit_status: ExitStatus,
32 stderr: String,
33 },
34
35 #[error("Unable to convert u32 to usize on this platform.")]
37 FailedU32ToUsizeConversion,
38
39 #[error(
41 "No SSH ForceCommand defined for user mapping (NetHSM users: {nethsm_users:?}, system user: {system_user})"
42 )]
43 NoForceCommandForMapping {
44 nethsm_users: Vec<String>,
45 system_user: String,
46 },
47
48 #[error("The information on the current process could not be retrieved")]
50 NoProcess,
51
52 #[error("This application must be run as root!")]
54 NotRoot,
55
56 #[error("No user ID could be retrieved for the current process with PID {0}")]
58 NoUidForProcess(usize),
59
60 #[error("The string {0} could not be converted to a \"sysinfo::Uid\"")]
62 SysUidFromStr(String),
63
64 #[error(
66 "The Path value {path} for the tmpfiles.d integration for {user} is not valid:\n{reason}"
67 )]
68 TmpfilesDPath {
69 path: String,
70 user: SystemUserId,
71 reason: &'static str,
72 },
73
74 #[error("Adding user {user} failed:\n{source}")]
76 UserAdd {
77 user: SystemUserId,
78 source: std::io::Error,
79 },
80
81 #[error("Modifying the user {user} failed:\n{source}")]
83 UserMod {
84 user: SystemUserId,
85 source: std::io::Error,
86 },
87
88 #[error("Getting a system user for the username {user} failed:\n{source}")]
90 UserNameConversion {
91 user: SystemUserId,
92 source: nix::Error,
93 },
94
95 #[error("Writing authorized_keys file for {user} failed:\n{source}")]
97 WriteAuthorizedKeys {
98 user: SystemUserId,
99 source: std::io::Error,
100 },
101
102 #[error("Writing sshd_config drop-in for {user} failed:\n{source}")]
104 WriteSshdConfig {
105 user: SystemUserId,
106 source: std::io::Error,
107 },
108
109 #[error("Writing tmpfiles.d integration for {user} failed:\n{source}")]
111 WriteTmpfilesD {
112 user: SystemUserId,
113 source: std::io::Error,
114 },
115}
116
117#[derive(Clone, Debug)]
121pub struct ConfigPath(PathBuf);
122
123impl ConfigPath {
124 pub fn new(path: PathBuf) -> Self {
125 Self(path)
126 }
127}
128
129impl AsRef<Path> for ConfigPath {
130 fn as_ref(&self) -> &Path {
131 self.0.as_path()
132 }
133}
134
135impl Default for ConfigPath {
136 fn default() -> Self {
141 Self(get_config_file_or_default())
142 }
143}
144
145impl From<PathBuf> for ConfigPath {
146 fn from(value: PathBuf) -> Self {
147 Self(value)
148 }
149}
150
151impl FromStr for ConfigPath {
152 type Err = Error;
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 Ok(Self::new(PathBuf::from(s)))
155 }
156}
157
158#[derive(strum::AsRefStr, Debug, strum::Display, strum::EnumString, strum::VariantNames)]
166pub enum SshForceCommand {
167 #[strum(serialize = "signstar-download-backup")]
169 DownloadBackup,
170
171 #[strum(serialize = "signstar-download-key-certificate")]
173 DownloadKeyCertificate,
174
175 #[strum(serialize = "signstar-download-metrics")]
177 DownloadMetrics,
178
179 #[strum(serialize = "signstar-download-secret-share")]
181 DownloadSecretShare,
182
183 #[strum(serialize = "signstar-download-wireguard")]
185 DownloadWireGuard,
186
187 #[strum(serialize = "signstar-sign")]
189 Sign,
190
191 #[strum(serialize = "signstar-upload-backup")]
193 UploadBackup,
194
195 #[strum(serialize = "signstar-upload-secret-share")]
197 UploadSecretShare,
198
199 #[strum(serialize = "signstar-upload-update")]
201 UploadUpdate,
202}
203
204impl TryFrom<&UserMapping> for SshForceCommand {
205 type Error = Error;
206
207 fn try_from(value: &UserMapping) -> Result<Self, Self::Error> {
208 match value {
209 UserMapping::SystemNetHsmBackup {
210 nethsm_user: _,
211 ssh_authorized_key: _,
212 system_user: _,
213 } => Ok(Self::DownloadBackup),
214 UserMapping::SystemNetHsmMetrics {
215 nethsm_users: _,
216 ssh_authorized_key: _,
217 system_user: _,
218 } => Ok(Self::DownloadMetrics),
219 UserMapping::SystemNetHsmOperatorSigning {
220 nethsm_user: _,
221 key_id: _,
222 nethsm_key_setup: _,
223 ssh_authorized_key: _,
224 system_user: _,
225 tag: _,
226 } => Ok(Self::Sign),
227 #[cfg(feature = "yubihsm2")]
228 UserMapping::SystemYubiHsmOperatorSigning { .. } => Ok(Self::Sign),
229 UserMapping::SystemOnlyShareDownload {
230 system_user: _,
231 ssh_authorized_key: _,
232 } => Ok(SshForceCommand::DownloadSecretShare),
233 UserMapping::SystemOnlyShareUpload {
234 system_user: _,
235 ssh_authorized_key: _,
236 } => Ok(SshForceCommand::UploadSecretShare),
237 UserMapping::SystemOnlyWireGuardDownload {
238 system_user: _,
239 ssh_authorized_key: _,
240 } => Ok(SshForceCommand::DownloadWireGuard),
241 UserMapping::NetHsmOnlyAdmin(_)
242 | UserMapping::HermeticSystemNetHsmMetrics {
243 nethsm_users: _,
244 system_user: _,
245 } => Err(Error::NoForceCommandForMapping {
246 nethsm_users: value
247 .get_nethsm_users()
248 .iter()
249 .map(|user| user.to_string())
250 .collect(),
251 system_user: if let Some(system_user_id) = value.get_system_user() {
252 system_user_id.to_string()
253 } else {
254 "".to_string()
255 },
256 }),
257 }
258 }
259}
260
261pub fn ensure_root() -> Result<(), Error> {
273 let pid: usize = id()
274 .try_into()
275 .map_err(|_| Error::FailedU32ToUsizeConversion)?;
276
277 let system = System::new_all();
278 let Some(process) = system.process(Pid::from(pid)) else {
279 return Err(Error::NoProcess);
280 };
281
282 let Some(uid) = process.effective_user_id() else {
283 return Err(Error::NoUidForProcess(pid));
284 };
285
286 let root_uid_str = "0";
287 let root_uid = sysinfo::Uid::from_str(root_uid_str)
288 .map_err(|_| Error::SysUidFromStr(root_uid_str.to_string()))?;
289
290 if uid.ne(&root_uid) {
291 return Err(Error::NotRoot);
292 }
293
294 Ok(())
295}
296
297pub fn create_system_users(config: &SignstarConfig) -> Result<(), Error> {
329 for mapping in config.iter_user_mappings() {
330 let Some(user) = mapping.get_system_user() else {
332 continue;
333 };
334
335 if User::from_name(user.as_ref())
337 .map_err(|source| Error::UserNameConversion {
338 user: user.clone(),
339 source,
340 })?
341 .is_some()
342 {
343 eprintln!("Skipping existing user \"{user}\"...");
344 continue;
345 }
346
347 let home_base_dir = get_home_base_dir_path();
348
349 print!("Creating user \"{user}\"...");
351 let user_add = Command::new("useradd")
352 .arg("--base-dir")
353 .arg(home_base_dir.as_path())
354 .arg("--user-group")
355 .arg("--shell")
356 .arg("/usr/bin/bash")
357 .arg(user.as_ref())
358 .output()
359 .map_err(|error| Error::UserAdd {
360 user: user.clone(),
361 source: error,
362 })?;
363
364 if !user_add.status.success() {
365 return Err(Error::CommandNonZero {
366 exit_status: user_add.status,
367 stderr: String::from_utf8_lossy(&user_add.stderr).into_owned(),
368 });
369 } else {
370 println!(" Done.");
371 }
372
373 print!("Unlocking user \"{user}\"...");
375 let user_mod = Command::new("usermod")
376 .args(["--unlock", user.as_ref()])
377 .output()
378 .map_err(|source| Error::UserMod {
379 user: user.clone(),
380 source,
381 })?;
382
383 if !user_mod.status.success() {
384 return Err(Error::CommandNonZero {
385 exit_status: user_mod.status,
386 stderr: String::from_utf8_lossy(&user_mod.stderr).into_owned(),
387 });
388 } else {
389 println!(" Done.");
390 }
391
392 print!("Adding tmpfiles.d integration for user \"{user}\"...");
394 {
395 let mut buffer = File::create(format!("/usr/lib/tmpfiles.d/signstar-user-{user}.conf"))
396 .map_err(|source| Error::WriteTmpfilesD {
397 user: user.clone(),
398 source,
399 })?;
400
401 let home_dir = {
405 let home_dir =
406 format!("{}/{user}", home_base_dir.to_string_lossy()).replace(" ", "\\x20");
407 if home_dir.contains("%") {
408 return Err(Error::TmpfilesDPath {
409 path: home_dir.clone(),
410 user: user.clone(),
411 reason: "Specifiers (%) are not supported at this point.",
412 });
413 }
414 home_dir
415 };
416
417 buffer
418 .write_all(format!("d {home_dir} 700 {user} {user}\n",).as_bytes())
419 .map_err(|source| Error::WriteTmpfilesD {
420 user: user.clone(),
421 source,
422 })?;
423 }
424 println!(" Done.");
425
426 if let Ok(force_command) = SshForceCommand::try_from(mapping)
427 && let Some(authorized_key) = mapping.get_ssh_authorized_key()
428 {
429 print!("Adding SSH authorized_keys file for user \"{user}\"...");
431 {
432 let mut buffer = File::create(
433 get_ssh_authorized_key_base_dir()
434 .join(format!("signstar-user-{user}.authorized_keys")),
435 )
436 .map_err(|source| Error::WriteAuthorizedKeys {
437 user: user.clone(),
438 source,
439 })?;
440 buffer
441 .write_all(authorized_key.as_ref().as_bytes())
442 .map_err(|source| Error::WriteAuthorizedKeys {
443 user: user.clone(),
444 source,
445 })?;
446 }
447 println!(" Done.");
448
449 print!("Adding sshd_config drop-in configuration for user \"{user}\"...");
451 {
452 let mut buffer = File::create(
453 get_sshd_config_dropin_dir().join(format!("10-signstar-user-{user}.conf")),
454 )
455 .map_err(|source| Error::WriteSshdConfig {
456 user: user.clone(),
457 source,
458 })?;
459 buffer
460 .write_all(
461 format!(
462 r#"Match user {user}
463 AuthorizedKeysFile /etc/ssh/signstar-user-{user}.authorized_keys
464 ForceCommand /usr/bin/{force_command}
465"#
466 )
467 .as_bytes(),
468 )
469 .map_err(|source| Error::WriteSshdConfig {
470 user: user.clone(),
471 source,
472 })?;
473 }
474 println!(" Done.");
475 };
476 }
477
478 Ok(())
479}