diff --git a/Cargo.lock b/Cargo.lock index 10c5766..c54dc8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,26 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -934,6 +954,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "landlock" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088" +dependencies = [ + "enumflags2", + "libc", + "thiserror 2.0.17", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1054,6 +1085,7 @@ dependencies = [ "futures-util", "indoc", "itertools", + "landlock", "nix", "num_cpus", "prettytable", @@ -1093,7 +1125,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9df0482..26e91f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,9 @@ tracing-journald = "0.3.2" tracing-subscriber = "0.3.22" uuid = { version = "1.18.1", features = ["v4"] } +[target.'cfg(target_os = "linux")'.dependencies] +landlock = "0.4.4" + [features] default = ["mysql-admutils-compatibility"] mysql-admutils-compatibility = [] diff --git a/assets/systemd/muscl.service b/assets/systemd/muscl.service index 6764bc9..007bba3 100644 --- a/assets/systemd/muscl.service +++ b/assets/systemd/muscl.service @@ -51,6 +51,6 @@ RestrictRealtime=true RestrictSUIDSGID=true SocketBindDeny=any SystemCallArchitectures=native -SystemCallFilter=@system-service +SystemCallFilter=@system-service @sandbox SystemCallFilter=~@privileged @resources UMask=0777 diff --git a/src/core/bootstrap.rs b/src/core/bootstrap.rs index d591835..37251f0 100644 --- a/src/core/bootstrap.rs +++ b/src/core/bootstrap.rs @@ -14,6 +14,7 @@ use crate::{ }, server::{ config::{MysqlConfig, ServerConfig}, + landlock::landlock_restrict_server, session_handler, }, }; @@ -223,6 +224,9 @@ fn invoke_server_with_config(config_path: PathBuf) -> anyhow::Result { tracing::debug!("Running server in child process"); + landlock_restrict_server(Some(config_path.as_path())) + .context("Failed to apply Landlock restrictions to the server process")?; + match run_forked_server(config_path, server_socket, unix_user) { Err(e) => Err(e), Ok(_) => unreachable!(), diff --git a/src/main.rs b/src/main.rs index f4b9369..190e6eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use crate::{ common::executable_is_suid_or_sgid, protocol::{Response, create_client_to_server_message_stream}, }, - server::command::ServerArgs, + server::{command::ServerArgs, landlock::landlock_restrict_server}, }; #[cfg(feature = "mysql-admutils-compatibility")] @@ -146,6 +146,10 @@ fn handle_server_command(args: &Args) -> anyhow::Result> { !executable_is_suid_or_sgid()?, "The executable should not be SUID or SGID when running the server manually" ); + + landlock_restrict_server(args.config.as_deref()) + .context("Failed to apply Landlock restrictions to the server process")?; + tokio_start_server( args.config.to_owned(), args.verbose.to_owned(), diff --git a/src/server.rs b/src/server.rs index 7470b45..831dea7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,7 +3,7 @@ pub mod command; mod common; pub mod config; pub mod input_sanitization; -// pub mod server_loop; +pub mod landlock; pub mod session_handler; pub mod sql; pub mod supervisor; diff --git a/src/server/landlock.rs b/src/server/landlock.rs new file mode 100644 index 0000000..2800d02 --- /dev/null +++ b/src/server/landlock.rs @@ -0,0 +1,89 @@ +#[cfg(target_os = "linux")] +use std::path::Path; + +#[cfg(target_os = "linux")] +pub fn landlock_restrict_server(config_path: Option<&Path>) -> anyhow::Result<()> { + use crate::{core::common::DEFAULT_CONFIG_PATH, server::config::ServerConfig}; + use anyhow::Context; + use landlock::{ + ABI, Access, AccessFs, AccessNet, NetPort, Ruleset, RulesetAttr, RulesetCreatedAttr, + path_beneath_rules, + }; + + let config_path = config_path.unwrap_or(Path::new(DEFAULT_CONFIG_PATH)); + + let config = ServerConfig::read_config_from_path(config_path)?; + + let abi = ABI::V4; + let mut ruleset = Ruleset::default() + .handle_access(AccessFs::from_all(abi))? + .handle_access(AccessNet::from_all(abi))? + .create() + .context("Failed to create Landlock ruleset")? + .add_rules(path_beneath_rules( + &["/run/muscl"], + AccessFs::from_read(abi), + )) + .context("Failed to add Landlock rules for /run/muscl")? + // Needs read access to /etc to access unix user/group info + .add_rules(path_beneath_rules(&["/etc"], AccessFs::from_read(abi))) + .context("Failed to add Landlock rules for /etc")? + .add_rules(path_beneath_rules(&[config_path], AccessFs::from_read(abi))) + .context(format!( + "Failed to add Landlock rules for server config path at {}", + config_path.display() + ))?; + + if let Some(socket_path) = &config.socket_path { + ruleset = ruleset + .add_rules(path_beneath_rules(&[socket_path], AccessFs::from_all(abi))) + .context(format!( + "Failed to add Landlock rules for server socket path at {}", + socket_path.display() + ))?; + } + + if let Some(mysql_socket_path) = &config.mysql.socket_path { + ruleset = ruleset + .add_rules(path_beneath_rules( + &[mysql_socket_path], + AccessFs::from_all(abi), + )) + .context(format!( + "Failed to add Landlock rules for MySQL socket path at {}", + mysql_socket_path.display() + ))?; + } + + if let Some(mysql_host) = &config.mysql.host { + ruleset = ruleset + .add_rule(NetPort::new(config.mysql.port, AccessNet::ConnectTcp)) + .context(format!( + "Failed to add Landlock rules for MySQL host at {}:{}", + mysql_host, config.mysql.port + ))?; + } + + if let Some(mysql_passwd_file) = &config.mysql.password_file { + ruleset = ruleset + .add_rules(path_beneath_rules( + &[mysql_passwd_file], + AccessFs::from_read(abi), + )) + .context(format!( + "Failed to add Landlock rules for MySQL password file at {}", + mysql_passwd_file.display() + ))?; + } + + ruleset + .restrict_self() + .context("Failed to apply Landlock restrictions to the server process")?; + + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +pub fn landlock_restrict_server() -> anyhow::Result<()> { + Ok(()) +}