-
Notifications
You must be signed in to change notification settings - Fork 689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PVF host: Make unavailable security features print a warning #2244
Changes from 4 commits
7bb717a
1652d26
4e163b9
d62431f
516dcb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,155 +14,256 @@ | |
// You should have received a copy of the GNU General Public License | ||
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
use crate::LOG_TARGET; | ||
use std::path::Path; | ||
use crate::{Config, SecurityStatus, LOG_TARGET}; | ||
use futures::join; | ||
use std::{fmt, path::Path}; | ||
use tokio::{ | ||
fs::{File, OpenOptions}, | ||
io::{AsyncReadExt, AsyncSeekExt, SeekFrom}, | ||
}; | ||
|
||
/// Check if we can sandbox the root and emit a warning if not. | ||
const SECURE_MODE_ANNOUNCEMENT: &'static str = | ||
"In the next release this will be a hard error by default. | ||
\nMore information: https://wiki.polkadot.network/docs/maintain-guides-secure-validator#secure-validator-mode"; | ||
|
||
/// Run checks for supported security features. | ||
pub async fn check_security_status(config: &Config) -> SecurityStatus { | ||
let Config { prepare_worker_program_path, .. } = config; | ||
|
||
// TODO: add check that syslog is available and that seccomp violations are logged? | ||
let (landlock, seccomp, change_root) = 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) | ||
); | ||
|
||
let security_status = SecurityStatus { | ||
can_enable_landlock: landlock.is_ok(), | ||
can_enable_seccomp: seccomp.is_ok(), | ||
can_unshare_user_namespace_and_change_root: change_root.is_ok(), | ||
}; | ||
|
||
let errs: Vec<SecureModeError> = [landlock, seccomp, change_root] | ||
.into_iter() | ||
.filter_map(|result| result.err()) | ||
.collect(); | ||
let err_occurred = print_secure_mode_message(errs); | ||
if err_occurred { | ||
gum::error!( | ||
target: LOG_TARGET, | ||
"{}", | ||
SECURE_MODE_ANNOUNCEMENT, | ||
); | ||
} | ||
|
||
security_status | ||
} | ||
|
||
type SecureModeResult = std::result::Result<(), SecureModeError>; | ||
|
||
/// Errors related to enabling Secure Validator Mode. | ||
#[derive(Debug)] | ||
enum SecureModeError { | ||
CannotEnableLandlock(String), | ||
CannotEnableSeccomp(String), | ||
CannotUnshareUserNamespaceAndChangeRoot(String), | ||
} | ||
|
||
impl SecureModeError { | ||
/// Whether this error is allowed with Secure Validator Mode enabled. | ||
fn is_allowed_in_secure_mode(&self) -> bool { | ||
use SecureModeError::*; | ||
match self { | ||
CannotEnableLandlock(_) => true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wasn't landlock the feature with the lowest requirements on Kernel versions? Why is this optional? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, it's the only one that requires a relatively recent kernel version. Many validators are not on 5.13 yet so we decided not to burden them with this requirement, and we already have FS sandboxing a different way, with pivot_root. |
||
CannotEnableSeccomp(_) => false, | ||
CannotUnshareUserNamespaceAndChangeRoot(_) => false, | ||
} | ||
} | ||
} | ||
|
||
impl fmt::Display for SecureModeError { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
use SecureModeError::*; | ||
let display = match self { | ||
CannotEnableLandlock(err) => format!("Cannot enable landlock, a Linux 5.13+ kernel security feature: {}", err), | ||
CannotEnableSeccomp(err) => format!("Cannot enable seccomp, a Linux-specific kernel security feature: {}", err), | ||
CannotUnshareUserNamespaceAndChangeRoot(err) => format!("Cannot unshare user namespace and change root, which are Linux-specific kernel security features: {}", err), | ||
}; | ||
|
||
write!(f, "{}", display) | ||
mrcnski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/// Errors if Secure Validator Mode and some mandatory errors occurred, warn otherwise. | ||
/// | ||
/// # Returns | ||
/// | ||
/// `true` if an error was printed, `false` otherwise. | ||
fn print_secure_mode_message(errs: Vec<SecureModeError>) -> bool { | ||
// Trying to run securely and some mandatory errors occurred. | ||
const SECURE_MODE_ERROR: &'static str = "🚨 Your system cannot securely run a validator. \ | ||
\nRunning validation of malicious PVF code has a higher risk of compromising this machine."; | ||
// Some errors occurred when running insecurely, or some optional errors occurred when running | ||
// securely. | ||
const SECURE_MODE_WARNING: &'static str = "🚨 Some security issues have been detected. \ | ||
\nRunning validation of malicious PVF code has a higher risk of compromising this machine."; | ||
|
||
if errs.is_empty() { | ||
return false | ||
} | ||
|
||
let errs_allowed = errs.iter().all(|err| err.is_allowed_in_secure_mode()); | ||
let errs_string: String = errs | ||
.iter() | ||
.map(|err| { | ||
format!( | ||
"\n - {}{}", | ||
if err.is_allowed_in_secure_mode() { "Optional: " } else { "" }, | ||
err | ||
) | ||
}) | ||
.collect(); | ||
|
||
if errs_allowed { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
"{}{}", | ||
SECURE_MODE_WARNING, | ||
errs_string, | ||
); | ||
false | ||
} else { | ||
gum::error!( | ||
target: LOG_TARGET, | ||
"{}{}", | ||
SECURE_MODE_ERROR, | ||
errs_string, | ||
); | ||
true | ||
} | ||
} | ||
|
||
/// 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. | ||
pub async fn check_can_unshare_user_namespace_and_change_root( | ||
async fn check_can_unshare_user_namespace_and_change_root( | ||
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] | ||
prepare_worker_program_path: &Path, | ||
) -> bool { | ||
) -> SecureModeResult { | ||
cfg_if::cfg_if! { | ||
if #[cfg(target_os = "linux")] { | ||
match tokio::process::Command::new(prepare_worker_program_path) | ||
.arg("--check-can-unshare-user-namespace-and-change-root") | ||
.output() | ||
.await | ||
{ | ||
Ok(output) if output.status.success() => true, | ||
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(); | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
?prepare_worker_program_path, | ||
// Docs say to always print status using `Display` implementation. | ||
status = %output.status, | ||
%stderr, | ||
"Cannot unshare user namespace and change root, which are Linux-specific kernel security features. Running validation of malicious PVF code has a higher risk of compromising this machine. Consider running with support for unsharing user namespaces for maximum security." | ||
); | ||
false | ||
}, | ||
Err(err) => { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
?prepare_worker_program_path, | ||
"Could not start child process: {}", | ||
err | ||
); | ||
false | ||
Err(SecureModeError::CannotUnshareUserNamespaceAndChangeRoot( | ||
format!("not available: {}", stderr) | ||
)) | ||
}, | ||
Err(err) => | ||
Err(SecureModeError::CannotUnshareUserNamespaceAndChangeRoot( | ||
format!("could not start child process: {}", err) | ||
)), | ||
} | ||
} else { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
"Cannot unshare user namespace and change root, which are Linux-specific kernel security features. Running validation of malicious PVF code has a higher risk of compromising this machine. Consider running on Linux with support for unsharing user namespaces for maximum security." | ||
); | ||
false | ||
Err(SecureModeError::CannotUnshareUserNamespaceAndChangeRoot( | ||
"only available on Linux".into() | ||
)) | ||
} | ||
} | ||
} | ||
|
||
/// Check if landlock is supported and emit a warning if not. | ||
/// 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. | ||
pub async fn check_landlock( | ||
async fn check_landlock( | ||
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] | ||
prepare_worker_program_path: &Path, | ||
) -> bool { | ||
) -> SecureModeResult { | ||
cfg_if::cfg_if! { | ||
if #[cfg(target_os = "linux")] { | ||
match tokio::process::Command::new(prepare_worker_program_path) | ||
.arg("--check-can-enable-landlock") | ||
.status() | ||
.await | ||
{ | ||
Ok(status) if status.success() => true, | ||
Ok(status) => { | ||
Ok(status) if status.success() => Ok(()), | ||
Ok(_status) => { | ||
let abi = | ||
polkadot_node_core_pvf_common::worker::security::landlock::LANDLOCK_ABI as u8; | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
?prepare_worker_program_path, | ||
?status, | ||
%abi, | ||
"Cannot fully enable landlock, a Linux-specific kernel security feature. Running validation of malicious PVF code has a higher risk of compromising this machine. Consider upgrading the kernel version for maximum security." | ||
); | ||
false | ||
}, | ||
Err(err) => { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
?prepare_worker_program_path, | ||
"Could not start child process: {}", | ||
err | ||
); | ||
false | ||
Err(SecureModeError::CannotEnableLandlock( | ||
format!("landlock ABI {} not available", abi) | ||
)) | ||
}, | ||
Err(err) => | ||
Err(SecureModeError::CannotEnableLandlock( | ||
format!("could not start child process: {}", err) | ||
)), | ||
} | ||
} else { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
"Cannot enable landlock, a Linux-specific kernel security feature. Running validation of malicious PVF code has a higher risk of compromising this machine. Consider running on Linux with landlock support for maximum security." | ||
); | ||
false | ||
Err(SecureModeError::CannotEnableLandlock( | ||
"only available on Linux".into() | ||
)) | ||
} | ||
} | ||
} | ||
|
||
/// Check if seccomp is supported and emit a warning if not. | ||
/// 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. | ||
pub async fn check_seccomp( | ||
async fn check_seccomp( | ||
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] | ||
prepare_worker_program_path: &Path, | ||
) -> bool { | ||
) -> SecureModeResult { | ||
cfg_if::cfg_if! { | ||
if #[cfg(target_os = "linux")] { | ||
match tokio::process::Command::new(prepare_worker_program_path) | ||
.arg("--check-can-enable-seccomp") | ||
.status() | ||
.await | ||
{ | ||
Ok(status) if status.success() => true, | ||
Ok(status) => { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
?prepare_worker_program_path, | ||
?status, | ||
"Cannot fully enable seccomp, a Linux-specific kernel security feature. Running validation of malicious PVF code has a higher risk of compromising this machine. Consider upgrading the kernel version for maximum security." | ||
); | ||
false | ||
}, | ||
Err(err) => { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
?prepare_worker_program_path, | ||
"Could not start child process: {}", | ||
err | ||
); | ||
false | ||
}, | ||
cfg_if::cfg_if! { | ||
if #[cfg(target_arch = "x86_64")] { | ||
match tokio::process::Command::new(prepare_worker_program_path) | ||
.arg("--check-can-enable-seccomp") | ||
.status() | ||
.await | ||
{ | ||
Ok(status) if status.success() => Ok(()), | ||
Ok(_status) => | ||
Err(SecureModeError::CannotEnableSeccomp( | ||
"not available".into() | ||
)), | ||
Err(err) => | ||
Err(SecureModeError::CannotEnableSeccomp( | ||
format!("could not start child process: {}", err) | ||
)), | ||
} | ||
} else { | ||
Err(SecureModeError::CannotEnableSeccomp( | ||
"only supported on CPUs from the x86_64 family (usually Intel or AMD)".into() | ||
)) | ||
} | ||
} | ||
} else { | ||
gum::warn!( | ||
target: LOG_TARGET, | ||
"Cannot enable seccomp, a Linux-specific kernel security feature. Running validation of malicious PVF code has a higher risk of compromising this machine. Consider running on Linux with seccomp support for maximum security." | ||
); | ||
false | ||
cfg_if::cfg_if! { | ||
if #[cfg(target_arch = "x86_64")] { | ||
Err(SecureModeError::CannotEnableSeccomp( | ||
"only supported on Linux".into() | ||
)) | ||
} else { | ||
Err(SecureModeError::CannotEnableSeccomp( | ||
"only supported on Linux and on CPUs from the x86_64 family (usually Intel or AMD).".into() | ||
)) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also add a note about the new flag that will enable validators to run "unsecurely". They should know that this is will be an option
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wiki link does mention it. I would rather validators read the full information, instead of being given a flag here and applying it without thinking. When we have the error it will spell out the exact flag here, but maybe it shouldn't...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error should spell out the entire flag. However, it is fine to wait until this is a hard error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright cool. Thanks for the review!