// Copyright (C) Parity Technologies (UK) Ltd. // This file is part of Pezkuwi. // Pezkuwi is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Pezkuwi is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Pezkuwi. If not, see . use crate::{Config, SecurityStatus, LOG_TARGET}; use futures::join; use std::{fmt, path::Path}; /// Run checks for supported security features. /// /// # Returns /// /// Returns the set of security features that we were able to enable. If an error occurs while /// enabling a security feature we set the corresponding status to `false`. /// /// # Errors /// /// Returns an error only if we could not fully enforce the security level required by the current /// configuration. pub async fn check_security_status(config: &Config) -> Result { let Config { prepare_worker_program_path, secure_validator_mode, cache_path, .. } = config; let (landlock, seccomp, change_root, secure_clone) = join!( check_landlock(prepare_worker_program_path), check_seccomp(prepare_worker_program_path), check_can_unshare_user_namespace_and_change_root(prepare_worker_program_path, cache_path), check_can_do_secure_clone(prepare_worker_program_path), ); let full_security_status = FullSecurityStatus::new( *secure_validator_mode, landlock, seccomp, change_root, secure_clone, ); let security_status = full_security_status.as_partial(); if full_security_status.err_occurred() { print_secure_mode_error_or_warning(&full_security_status); if !full_security_status.all_errs_allowed() { return Err("could not enable Secure Validator Mode; check logs".into()); } } if security_status.secure_validator_mode { gum::info!( target: LOG_TARGET, "👮‍♀️ Running in Secure Validator Mode. \ It is highly recommended that you operate according to our security guidelines. \ \nMore information: https://wiki.network.pezkuwichain.io/docs/maintain-guides-secure-validator#secure-validator-mode" ); } Ok(security_status) } /// Contains the full security status including error states. struct FullSecurityStatus { partial: SecurityStatus, errs: Vec, } impl FullSecurityStatus { fn new( secure_validator_mode: bool, landlock: SecureModeResult, seccomp: SecureModeResult, change_root: SecureModeResult, secure_clone: SecureModeResult, ) -> Self { Self { partial: SecurityStatus { secure_validator_mode, can_enable_landlock: landlock.is_ok(), can_enable_seccomp: seccomp.is_ok(), can_unshare_user_namespace_and_change_root: change_root.is_ok(), can_do_secure_clone: secure_clone.is_ok(), }, errs: [landlock, seccomp, change_root, secure_clone] .into_iter() .filter_map(|result| result.err()) .collect(), } } fn as_partial(&self) -> SecurityStatus { self.partial.clone() } fn err_occurred(&self) -> bool { !self.errs.is_empty() } fn all_errs_allowed(&self) -> bool { !self.partial.secure_validator_mode || self.errs.iter().all(|err| err.is_allowed_in_secure_mode(&self.partial)) } fn errs_string(&self) -> String { self.errs .iter() .map(|err| { format!( "\n - {}{}", if err.is_allowed_in_secure_mode(&self.partial) { "Optional: " } else { "" }, err ) }) .collect() } } type SecureModeResult = std::result::Result<(), SecureModeError>; /// Errors related to enabling Secure Validator Mode. #[derive(Debug)] enum SecureModeError { CannotEnableLandlock { err: String, abi: u8 }, CannotEnableSeccomp(String), CannotUnshareUserNamespaceAndChangeRoot(String), CannotDoSecureClone(String), } impl SecureModeError { /// Whether this error is allowed with Secure Validator Mode enabled. fn is_allowed_in_secure_mode(&self, security_status: &SecurityStatus) -> bool { use SecureModeError::*; match self { // Landlock is present on relatively recent Linuxes. This is optional if the unshare // capability is present, providing FS sandboxing a different way. CannotEnableLandlock { .. } => { security_status.can_unshare_user_namespace_and_change_root }, // seccomp should be present on all modern Linuxes unless it's been disabled. CannotEnableSeccomp(_) => false, // Should always be present on modern Linuxes. If not, Landlock also provides FS // sandboxing, so don't enforce this. CannotUnshareUserNamespaceAndChangeRoot(_) => security_status.can_enable_landlock, // We have not determined the kernel requirements for this capability, and it's also not // necessary for FS or networking restrictions. CannotDoSecureClone(_) => true, } } } impl fmt::Display for SecureModeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use SecureModeError::*; match self { CannotEnableLandlock{err, abi} => write!(f, "Cannot enable landlock (ABI {abi}), a Linux 5.13+ kernel security feature: {err}"), CannotEnableSeccomp(err) => write!(f, "Cannot enable seccomp, a Linux-specific kernel security feature: {err}"), CannotUnshareUserNamespaceAndChangeRoot(err) => write!(f, "Cannot unshare user namespace and change root, which are Linux-specific kernel security features: {err}"), CannotDoSecureClone(err) => write!(f, "Cannot call clone with all sandboxing flags, a Linux-specific kernel security features: {err}"), } } } /// Print an error if Secure Validator Mode and some mandatory errors occurred, warn otherwise. fn print_secure_mode_error_or_warning(security_status: &FullSecurityStatus) { let all_errs_allowed = security_status.all_errs_allowed(); let errs_string = security_status.errs_string(); if all_errs_allowed { gum::warn!( target: LOG_TARGET, "{}{}", crate::SECURE_MODE_WARNING, errs_string, ); } else { gum::error!( target: LOG_TARGET, "{}{}{}", crate::SECURE_MODE_ERROR, errs_string, crate::IGNORE_SECURE_MODE_TIP ); } } /// Check if we can change root to a new, sandboxed root and return an error if not. /// /// We do this check by spawning a new process and trying to sandbox it. To get as close as possible /// to running the check in a worker, we try it... in a worker. The expected return status is 0 on /// success and -1 on failure. async fn check_can_unshare_user_namespace_and_change_root( prepare_worker_program_path: &Path, cache_path: &Path, ) -> SecureModeResult { let cache_dir_tempdir = tempfile::Builder::new() .prefix("check-can-unshare-") .tempdir_in(cache_path) .map_err(|err| { SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(format!( "could not create a temporary directory in {:?}: {}", cache_path, err )) })?; spawn_process_for_security_check( prepare_worker_program_path, "--check-can-unshare-user-namespace-and-change-root", &[cache_dir_tempdir.path()], ) .await .map_err(|err| SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(err)) } /// Check if landlock is supported and return an error if not. /// /// We do this check by spawning a new process and trying to sandbox it. To get as close as possible /// to running the check in a worker, we try it... in a worker. The expected return status is 0 on /// success and -1 on failure. async fn check_landlock(prepare_worker_program_path: &Path) -> SecureModeResult { let abi = pezkuwi_node_core_pvf_common::worker::security::landlock::LANDLOCK_ABI as u8; spawn_process_for_security_check( prepare_worker_program_path, "--check-can-enable-landlock", std::iter::empty::<&str>(), ) .await .map_err(|err| SecureModeError::CannotEnableLandlock { err, abi }) } /// Check if seccomp is supported and return an error if not. /// /// We do this check by spawning a new process and trying to sandbox it. To get as close as possible /// to running the check in a worker, we try it... in a worker. The expected return status is 0 on /// success and -1 on failure. #[cfg(target_arch = "x86_64")] async fn check_seccomp(prepare_worker_program_path: &Path) -> SecureModeResult { spawn_process_for_security_check( prepare_worker_program_path, "--check-can-enable-seccomp", std::iter::empty::<&str>(), ) .await .map_err(|err| SecureModeError::CannotEnableSeccomp(err)) } #[cfg(not(target_arch = "x86_64"))] async fn check_seccomp(_: &Path) -> SecureModeResult { Err(SecureModeError::CannotEnableSeccomp( "only supported on CPUs from the x86_64 family (usually Intel or AMD)".into(), )) } /// Check if we can call `clone` with all sandboxing flags, and return an error if not. /// /// We do this check by spawning a new process and trying to sandbox it. To get as close as possible /// to running the check in a worker, we try it... in a worker. The expected return status is 0 on /// success and -1 on failure. async fn check_can_do_secure_clone(prepare_worker_program_path: &Path) -> SecureModeResult { spawn_process_for_security_check( prepare_worker_program_path, "--check-can-do-secure-clone", std::iter::empty::<&str>(), ) .await .map_err(|err| SecureModeError::CannotDoSecureClone(err)) } async fn spawn_process_for_security_check( prepare_worker_program_path: &Path, check_arg: &'static str, extra_args: I, ) -> Result<(), String> where I: IntoIterator, S: AsRef, { let mut command = tokio::process::Command::new(prepare_worker_program_path); // Clear env vars. (In theory, running checks with different env vars could result in different // outcomes of the checks.) command.env_clear(); // Add back any env vars we want to keep. if let Ok(value) = std::env::var("RUST_LOG") { command.env("RUST_LOG", value); } match command.arg(check_arg).args(extra_args).output().await { Ok(output) if output.status.success() => Ok(()), Ok(output) => { let stderr = std::str::from_utf8(&output.stderr) .expect("child process writes a UTF-8 string to stderr; qed") .trim(); if stderr.is_empty() { Err("not available".into()) } else { Err(format!("not available: {}", stderr)) } }, Err(err) => Err(format!("could not start child process: {}", err)), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_secure_mode_error_optionality() { let err = SecureModeError::CannotEnableLandlock { err: String::new(), abi: 3 }; assert!(err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: false, can_enable_seccomp: false, can_unshare_user_namespace_and_change_root: true, can_do_secure_clone: true, })); assert!(!err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: false, can_enable_seccomp: true, can_unshare_user_namespace_and_change_root: false, can_do_secure_clone: false, })); let err = SecureModeError::CannotEnableSeccomp(String::new()); assert!(!err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: false, can_enable_seccomp: false, can_unshare_user_namespace_and_change_root: true, can_do_secure_clone: true, })); assert!(!err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: false, can_enable_seccomp: true, can_unshare_user_namespace_and_change_root: false, can_do_secure_clone: false, })); let err = SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(String::new()); assert!(err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: true, can_enable_seccomp: false, can_unshare_user_namespace_and_change_root: false, can_do_secure_clone: false, })); assert!(!err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: false, can_enable_seccomp: true, can_unshare_user_namespace_and_change_root: false, can_do_secure_clone: false, })); let err = SecureModeError::CannotDoSecureClone(String::new()); assert!(err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: true, can_enable_landlock: true, can_enable_seccomp: true, can_unshare_user_namespace_and_change_root: true, can_do_secure_clone: true, })); assert!(err.is_allowed_in_secure_mode(&SecurityStatus { secure_validator_mode: false, can_enable_landlock: false, can_enable_seccomp: false, can_unshare_user_namespace_and_change_root: false, can_do_secure_clone: false, })); } }