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 nethsm_config::{HermeticParallelConfig, SystemUserId, UserMapping};
10use nix::unistd::User;
11use signstar_common::{
12 config::get_config_file_or_default,
13 ssh::{get_ssh_authorized_key_base_dir, get_sshd_config_dropin_dir},
14 system_user::get_home_base_dir_path,
15};
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] nethsm_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(Debug, strum::AsRefStr, 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-signature")]
185 DownloadSignature,
186
187 #[strum(serialize = "signstar-download-wireguard")]
189 DownloadWireGuard,
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 nethsm_key_setup: _,
222 ssh_authorized_key: _,
223 system_user: _,
224 tag: _,
225 } => Ok(Self::DownloadSignature),
226 UserMapping::SystemOnlyShareDownload {
227 system_user: _,
228 ssh_authorized_keys: _,
229 } => Ok(SshForceCommand::DownloadSecretShare),
230 UserMapping::SystemOnlyShareUpload {
231 system_user: _,
232 ssh_authorized_keys: _,
233 } => Ok(SshForceCommand::UploadSecretShare),
234 UserMapping::SystemOnlyWireGuardDownload {
235 system_user: _,
236 ssh_authorized_keys: _,
237 } => Ok(SshForceCommand::DownloadWireGuard),
238 UserMapping::NetHsmOnlyAdmin(_)
239 | UserMapping::HermeticSystemNetHsmMetrics {
240 nethsm_users: _,
241 system_user: _,
242 } => Err(Error::NoForceCommandForMapping {
243 nethsm_users: value
244 .get_nethsm_users()
245 .iter()
246 .map(|user| user.to_string())
247 .collect(),
248 system_user: if let Some(system_user_id) = value.get_system_user() {
249 system_user_id.to_string()
250 } else {
251 "".to_string()
252 },
253 }),
254 }
255 }
256}
257
258pub fn ensure_root() -> Result<(), Error> {
270 let pid: usize = id()
271 .try_into()
272 .map_err(|_| Error::FailedU32ToUsizeConversion)?;
273
274 let system = System::new_all();
275 let Some(process) = system.process(Pid::from(pid)) else {
276 return Err(Error::NoProcess);
277 };
278
279 let Some(uid) = process.effective_user_id() else {
280 return Err(Error::NoUidForProcess(pid));
281 };
282
283 let root_uid_str = "0";
284 let root_uid = sysinfo::Uid::from_str(root_uid_str)
285 .map_err(|_| Error::SysUidFromStr(root_uid_str.to_string()))?;
286
287 if uid.ne(&root_uid) {
288 return Err(Error::NotRoot);
289 }
290
291 Ok(())
292}
293
294pub fn create_system_users(config: &HermeticParallelConfig) -> Result<(), Error> {
326 for mapping in config.iter_user_mappings() {
327 let Some(user) = mapping.get_system_user() else {
329 continue;
330 };
331
332 if User::from_name(user.as_ref())
334 .map_err(|source| Error::UserNameConversion {
335 user: user.clone(),
336 source,
337 })?
338 .is_some()
339 {
340 eprintln!("Skipping existing user \"{user}\"...");
341 continue;
342 }
343
344 let home_base_dir = get_home_base_dir_path();
345
346 print!("Creating user \"{user}\"...");
348 let user_add = Command::new("useradd")
349 .arg("--base-dir")
350 .arg(home_base_dir.as_path())
351 .arg("--user-group")
352 .arg("--shell")
353 .arg("/usr/bin/bash")
354 .arg(user.as_ref())
355 .output()
356 .map_err(|error| Error::UserAdd {
357 user: user.clone(),
358 source: error,
359 })?;
360
361 if !user_add.status.success() {
362 return Err(Error::CommandNonZero {
363 exit_status: user_add.status,
364 stderr: String::from_utf8_lossy(&user_add.stderr).into_owned(),
365 });
366 } else {
367 println!(" Done.");
368 }
369
370 print!("Unlocking user \"{user}\"...");
372 let user_mod = Command::new("usermod")
373 .args(["--unlock", user.as_ref()])
374 .output()
375 .map_err(|source| Error::UserMod {
376 user: user.clone(),
377 source,
378 })?;
379
380 if !user_mod.status.success() {
381 return Err(Error::CommandNonZero {
382 exit_status: user_mod.status,
383 stderr: String::from_utf8_lossy(&user_mod.stderr).into_owned(),
384 });
385 } else {
386 println!(" Done.");
387 }
388
389 print!("Adding tmpfiles.d integration for user \"{user}\"...");
391 {
392 let mut buffer = File::create(format!("/usr/lib/tmpfiles.d/signstar-user-{user}.conf"))
393 .map_err(|source| Error::WriteTmpfilesD {
394 user: user.clone(),
395 source,
396 })?;
397
398 let home_dir = {
402 let home_dir =
403 format!("{}/{user}", home_base_dir.to_string_lossy()).replace(" ", "\\x20");
404 if home_dir.contains("%") {
405 return Err(Error::TmpfilesDPath {
406 path: home_dir.clone(),
407 user: user.clone(),
408 reason: "Specifiers (%) are not supported at this point.",
409 });
410 }
411 home_dir
412 };
413
414 buffer
415 .write_all(format!("d {home_dir} 700 {user} {user}\n",).as_bytes())
416 .map_err(|source| Error::WriteTmpfilesD {
417 user: user.clone(),
418 source,
419 })?;
420 }
421 println!(" Done.");
422
423 if let Ok(force_command) = SshForceCommand::try_from(mapping) {
424 let authorized_keys = mapping.get_ssh_authorized_keys();
425 if !authorized_keys.is_empty() {
426 print!("Adding SSH authorized_keys file for user \"{user}\"...");
428 {
429 let mut buffer = File::create(
430 get_ssh_authorized_key_base_dir()
431 .join(format!("signstar-user-{user}.authorized_keys")),
432 )
433 .map_err(|source| Error::WriteAuthorizedKeys {
434 user: user.clone(),
435 source,
436 })?;
437 buffer
438 .write_all(
439 (authorized_keys
440 .iter()
441 .map(|authorized_key| authorized_key.as_ref())
442 .collect::<Vec<&str>>()
443 .join("\n")
444 + "\n")
445 .as_bytes(),
446 )
447 .map_err(|source| Error::WriteAuthorizedKeys {
448 user: user.clone(),
449 source,
450 })?;
451 }
452 println!(" Done.");
453
454 print!("Adding sshd_config drop-in configuration for user \"{user}\"...");
456 {
457 let mut buffer = File::create(
458 get_sshd_config_dropin_dir().join(format!("10-signstar-user-{user}.conf")),
459 )
460 .map_err(|source| Error::WriteSshdConfig {
461 user: user.clone(),
462 source,
463 })?;
464 buffer
465 .write_all(
466 format!(
467 r#"Match user {user}
468 AuthorizedKeysFile /etc/ssh/signstar-user-{user}.authorized_keys
469 ForceCommand /usr/bin/{force_command}
470"#
471 )
472 .as_bytes(),
473 )
474 .map_err(|source| Error::WriteSshdConfig {
475 user: user.clone(),
476 source,
477 })?;
478 }
479 println!(" Done.");
480 }
481 };
482 }
483
484 Ok(())
485}