diff --git a/src/cl.rs b/src/cl.rs index 742a8f0..864f51d 100644 --- a/src/cl.rs +++ b/src/cl.rs @@ -24,6 +24,43 @@ pub(crate) enum HardeningMode { Aggressive, } +#[derive(Debug, clap::Parser)] +pub(crate) struct HardeningOptions { + /// How hard we should harden + #[arg(short, long, default_value_t, value_enum)] + pub mode: HardeningMode, + /// Enable advanced network firewalling + #[arg(short = 'f', long, default_value_t)] + pub network_firewalling: bool, +} + +impl HardeningOptions { + /// Build the most safe options + #[cfg_attr(not(test), expect(dead_code))] + pub(crate) fn safe() -> Self { + Self { + mode: HardeningMode::Safe, + network_firewalling: false, + } + } + + /// Build the most strict options + pub(crate) fn strict() -> Self { + Self { + mode: HardeningMode::Aggressive, + network_firewalling: true, + } + } + + pub(crate) fn to_cmdline(&self) -> String { + format!( + "-m {}{}", + self.mode, + if self.network_firewalling { " -n" } else { "" } + ) + } +} + #[derive(Debug, clap::Subcommand)] pub(crate) enum Action { /// Run a program to profile its behavior @@ -31,9 +68,8 @@ pub(crate) enum Action { /// The command line to run #[arg(num_args = 1.., required = true)] command: Vec, - /// How hard we should harden - #[arg(short, long, default_value_t, value_enum)] - mode: HardeningMode, + #[command(flatten)] + hardening_opts: HardeningOptions, /// Generate profile data file to be merged with others instead of generating systemd options directly #[arg(short, long, default_value = None)] profile_data_path: Option, @@ -44,9 +80,8 @@ pub(crate) enum Action { }, /// Merge profile data from previous runs to generate systemd options MergeProfileData { - /// How hard we should harden - #[arg(short, long, default_value_t, value_enum)] - mode: HardeningMode, + #[command(flatten)] + hardening_opts: HardeningOptions, /// Profile data paths #[arg(num_args = 1.., required = true)] paths: Vec, @@ -64,9 +99,8 @@ pub(crate) enum ServiceAction { StartProfile { /// Service unit name service: String, - /// How hard we should harden - #[arg(short, long, default_value_t, value_enum)] - mode: HardeningMode, + #[command(flatten)] + hardening_opts: HardeningOptions, /// Disable immediate service restart #[arg(short, long, default_value_t = false)] no_restart: bool, diff --git a/src/main.rs b/src/main.rs index 8c248ac..4184ef7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,9 @@ mod systemd; fn sd_options( sd_version: &systemd::SystemdVersion, kernel_version: &systemd::KernelVersion, - mode: &cl::HardeningMode, + hardening_opts: &cl::HardeningOptions, ) -> Vec { - let sd_opts = systemd::build_options(sd_version, kernel_version, mode); + let sd_opts = systemd::build_options(sd_version, kernel_version, hardening_opts); log::info!( "Enabled support for systemd options: {}", sd_opts @@ -60,12 +60,12 @@ fn main() -> anyhow::Result<()> { match args.action { cl::Action::Run { command, - mode, + hardening_opts, profile_data_path, strace_log_path, } => { // Build supported systemd options - let sd_opts = sd_options(&sd_version, &kernel_version, &mode); + let sd_opts = sd_options(&sd_version, &kernel_version, &hardening_opts); // Run strace let cmd = command.iter().map(|a| &**a).collect::>(); @@ -102,9 +102,12 @@ fn main() -> anyhow::Result<()> { systemd::report_options(resolved_opts); } } - cl::Action::MergeProfileData { mode, paths } => { + cl::Action::MergeProfileData { + hardening_opts, + paths, + } => { // Build supported systemd options - let sd_opts = sd_options(&sd_version, &kernel_version, &mode); + let sd_opts = sd_options(&sd_version, &kernel_version, &hardening_opts); // Load and merge profile data let mut actions: Vec = Vec::new(); @@ -129,11 +132,11 @@ fn main() -> anyhow::Result<()> { } cl::Action::Service(cl::ServiceAction::StartProfile { service, - mode, + hardening_opts, no_restart, }) => { let service = systemd::Service::new(&service); - service.add_profile_fragment(&mode)?; + service.add_profile_fragment(&hardening_opts)?; if no_restart { log::warn!("Profiling config will only be applied when systemd config is reloaded, and service restarted"); } else { @@ -175,8 +178,11 @@ fn main() -> anyhow::Result<()> { } cl::Action::ListSystemdOptions => { println!("# Supported systemd options"); - let mut sd_opts = - sd_options(&sd_version, &kernel_version, &cl::HardeningMode::Aggressive); + let mut sd_opts = sd_options( + &sd_version, + &kernel_version, + &cl::HardeningOptions::strict(), + ); sd_opts.sort_unstable_by_key(|o| o.name); for sd_opt in sd_opts { println!("- [`{sd_opt}`](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#{sd_opt}=)"); diff --git a/src/systemd/options.rs b/src/systemd/options.rs index fbc1398..8c0a141 100644 --- a/src/systemd/options.rs +++ b/src/systemd/options.rs @@ -13,7 +13,7 @@ use itertools::Itertools; use strum::IntoEnumIterator; use crate::{ - cl::HardeningMode, + cl::{HardeningMode, HardeningOptions}, summarize::{ CountableSetSpecifier, NetworkActivity, NetworkActivityKind, ProgramAction, SetSpecifier, }, @@ -818,7 +818,7 @@ static SYSCALL_CLASSES: LazyLock>> = pub(crate) fn build_options( systemd_version: &SystemdVersion, kernel_version: &KernelVersion, - mode: &HardeningMode, + hardening_opts: &HardeningOptions, ) -> Vec { let mut options = Vec::new(); @@ -1196,7 +1196,7 @@ pub(crate) fn build_options( updater: None, }); - if let HardeningMode::Aggressive = mode { + if let HardeningMode::Aggressive = hardening_opts.mode { // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateNetwork= // // For now we enable this option if no sockets are used at all, in theory this could break if @@ -1256,7 +1256,7 @@ pub(crate) fn build_options( .collect(), ), }], - updater: Some(OptionUpdater { + updater: hardening_opts.network_firewalling.then_some(OptionUpdater { effect: |e, a| { let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(effect_na)) = e else { @@ -1546,7 +1546,7 @@ pub(crate) fn build_options( updater: None, }); - if let HardeningMode::Aggressive = mode { + if let HardeningMode::Aggressive = hardening_opts.mode { // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallArchitectures= // // This is actually very safe to enable, but since we don't currently support checking for its diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs index 7ff53be..48be46e 100644 --- a/src/systemd/resolver.rs +++ b/src/systemd/resolver.rs @@ -278,14 +278,14 @@ mod tests { use super::*; use crate::{ - cl::HardeningMode, + cl::HardeningOptions, systemd::{build_options, KernelVersion, SystemdVersion}, }; fn test_options(names: &[&str]) -> Vec { let sd_version = SystemdVersion::new(254, 0); let kernel_version = KernelVersion::new(6, 4, 0); - build_options(&sd_version, &kernel_version, &HardeningMode::Safe) + build_options(&sd_version, &kernel_version, &HardeningOptions::safe()) .into_iter() .filter(|o| names.contains(&o.name)) .collect() diff --git a/src/systemd/service.rs b/src/systemd/service.rs index 55e4311..8a9e584 100644 --- a/src/systemd/service.rs +++ b/src/systemd/service.rs @@ -12,7 +12,7 @@ use itertools::Itertools; use rand::Rng; use crate::{ - cl::HardeningMode, + cl::HardeningOptions, systemd::{options::OptionWithValue, END_OPTION_OUTPUT_SNIPPET, START_OPTION_OUTPUT_SNIPPET}, }; @@ -54,7 +54,10 @@ impl Service { ) } - pub(crate) fn add_profile_fragment(&self, mode: &HardeningMode) -> anyhow::Result<()> { + pub(crate) fn add_profile_fragment( + &self, + hardening_opts: &HardeningOptions, + ) -> anyhow::Result<()> { // Check first if our fragment does not yet exist let fragment_path = self.fragment_path(PROFILING_FRAGMENT_NAME, false); anyhow::ensure!( @@ -135,10 +138,10 @@ impl Service { #[expect(clippy::unwrap_used)] writeln!( fragment_file, - "{}={} run -m {} -p {} -- {}", + "{}={} run {} -p {} -- {}", exec_start_opt, shh_bin, - mode, + hardening_opts.to_cmdline(), profile_data_path.to_str().unwrap(), cmd )?; @@ -151,9 +154,9 @@ impl Service { #[expect(clippy::unwrap_used)] writeln!( fragment_file, - "ExecStopPost={} merge-profile-data -m {} {}", + "ExecStopPost={} merge-profile-data {} {}", shh_bin, - mode, + hardening_opts.to_cmdline(), profile_data_paths .iter() .map(|p| p.to_str().unwrap())