shh: rev2 switch to upstreamed patches for strace path fixing, clean up check patch, enable manpages and autocomplete with upstream patches,

update script, make cross-compile possible, make docgen feature and generation optional, add changelog

Signed-off-by: kuflierl <41301536+kuflierl@users.noreply.github.com>
This commit is contained in:
kuflierl 2025-04-29 01:08:31 +02:00
parent e067fb89ac
commit 3dc449dadb
No known key found for this signature in database
GPG key ID: 0B3842DA5392223D
3 changed files with 90 additions and 150 deletions

View file

@ -1,21 +1,9 @@
commit 070bf216bacf6ce1b473f2819a017d1be29716d0
commit 58bdfa7ef92ba07dc41a07aeef6d790ecd8f888c
Author: kuflierl <41301536+kuflierl@users.noreply.github.com>
Date: Sun Apr 13 19:56:58 2025 +0200
Date: Sat May 3 21:02:26 2025 +0200
add support for nix-build-system for tests
fix(tests): add support for nix-build-system for tests
diff --git a/Cargo.toml b/Cargo.toml
index eba0ef8..9153f00 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,6 +58,7 @@ default = []
as-root = [] # for tests only
gen-man-pages = ["dep:clap_mangen"]
nightly = [] # for benchmarks only
+nix-build-env = [] # perform checks in a way compatable with nix build
[lints.rust]
# https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs
index e2abbb7..1151592 100644
--- a/src/systemd/resolver.rs
@ -44,7 +32,7 @@ index e2abbb7..1151592 100644
let actions = vec![ProgramAction::Read("/var/data".into())];
let candidates = resolve(&opts, &actions, &hardening_opts);
diff --git a/tests/options.rs b/tests/options.rs
index 835ee14..cac55e5 100644
index 835ee14..a9c9973 100644
--- a/tests/options.rs
+++ b/tests/options.rs
@@ -24,7 +24,7 @@ fn run_true() {
@ -116,7 +104,7 @@ index 835ee14..cac55e5 100644
BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1))
} else {
BoxPredicate::new(predicate::str::contains("ProtectHome=").not())
@@ -227,11 +227,12 @@ fn run_read_kallsyms() {
@@ -227,7 +227,7 @@ fn run_read_kallsyms() {
.stdout(predicate::str::contains("LockPersonality=true\n").count(1))
.stdout(predicate::str::contains("RestrictRealtime=true\n").count(1))
.stdout(predicate::str::contains("ProtectClock=true\n").count(1))
@ -125,30 +113,7 @@ index 835ee14..cac55e5 100644
.stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1));
}
#[test]
+#[cfg_attr(feature = "nix-build-env", ignore)]
fn run_ls_modules() {
Command::cargo_bin("shh")
.unwrap()
@@ -240,7 +241,7 @@ fn run_ls_modules() {
.assert()
.success()
.stdout(predicate::str::contains("ProtectSystem=strict\n").count(1))
- .stdout(if Uid::effective().is_root() {
+ .stdout(if Uid::effective().is_root() || !env::current_exe().unwrap().starts_with("/home") {
BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1))
} else {
BoxPredicate::new(predicate::str::contains("ProtectHome=").not())
@@ -304,7 +305,7 @@ fn run_dmesg() {
}
#[test]
-#[cfg_attr(feature = "as-root", ignore)]
+#[cfg_attr(any(feature = "nix-build-env", feature = "as-root"), ignore)]
fn run_systemctl() {
assert!(!Uid::effective().is_root());
@@ -344,6 +345,7 @@ fn run_systemctl() {
@@ -344,6 +344,7 @@ fn run_systemctl() {
.stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1));
}
@ -156,7 +121,7 @@ index 835ee14..cac55e5 100644
#[test]
fn run_ss() {
Command::cargo_bin("shh")
@@ -353,7 +355,7 @@ fn run_ss() {
@@ -353,7 +354,7 @@ fn run_ss() {
.assert()
.success()
.stdout(predicate::str::contains("ProtectSystem=strict\n").count(1))
@ -165,7 +130,7 @@ index 835ee14..cac55e5 100644
BoxPredicate::new(predicate::str::contains("ProtectHome=true\n").count(1))
} else {
BoxPredicate::new(predicate::str::contains("ProtectHome=").not())
@@ -369,7 +371,7 @@ fn run_ss() {
@@ -369,7 +370,7 @@ fn run_ss() {
.stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1))
.stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1))
.stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1))
@ -174,7 +139,7 @@ index 835ee14..cac55e5 100644
.stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1))
.stdout(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK AF_UNIX\n").count(1).or(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK\n").count(1)))
.stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1))
@@ -379,7 +381,7 @@ fn run_ss() {
@@ -379,7 +380,7 @@ fn run_ss() {
.stdout(predicate::str::contains("LockPersonality=true\n").count(1))
.stdout(predicate::str::contains("RestrictRealtime=true\n").count(1))
.stdout(predicate::str::contains("ProtectClock=true\n").count(1))
@ -183,19 +148,3 @@ index 835ee14..cac55e5 100644
.stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1));
}
@@ -741,6 +743,7 @@ fn run_mknod() {
}
#[test]
+#[cfg_attr(feature = "nix-build-env", ignore)] // no raw socket cap in nix build
fn run_ping_4() {
Command::cargo_bin("shh")
.unwrap()
@@ -759,6 +762,7 @@ fn run_ping_4() {
}
#[test]
+#[cfg_attr(feature = "nix-build-env", ignore)] // no raw socket cap in nix build
fn run_ping_6() {
Command::cargo_bin("shh")
.unwrap()

View file

@ -2,12 +2,21 @@
lib,
rustPlatform,
fetchFromGitHub,
nix-update-script,
fetchpatch,
installShellFiles,
python3,
strace,
systemd,
iproute2,
stdenv,
enableDocumentationFeature ? true,
enableDocumentationGeneration ? true,
}:
let
isNativeDocgen =
(stdenv.buildPlatform.canExecute stdenv.hostPlatform) && enableDocumentationFeature;
in
rustPlatform.buildRustPackage rec {
pname = "shh";
version = "2025.4.12";
@ -19,36 +28,101 @@ rustPlatform.buildRustPackage rec {
hash = "sha256-+JWz0ya6gi8pPERnpAcQIe7zZUzWGxha+9/gizMVtEw=";
};
cargoHash = "sha256-TdP+1sb1GEFM57z+rc+gqhoWQhPAXzvMt/FCWf3wpr8=";
cargoHash = "sha256-rrOH76LHYSEeuNiMIICpAO7U/sz5V0JRO22mbIICQWw=";
# needs to be done this way to bypass patch conflicts
cargoPatches = [
(fetchpatch {
# to be removed after next release
name = "refactor-man-page-generation-command.patch";
url = "https://github.com/desbma/shh/commit/849b9a6646981c83a72a977b6398371e29d3b928.patch";
hash = "sha256-LZlUFfPtt2ScTxQbQ9j3Kzvp7T4MCFs92cJiI3YbWns=";
})
(fetchpatch {
# to be removed after next release
name = "support-shell-auto-complete.patch";
url = "https://github.com/desbma/shh/commit/74914dc8cfd74dbd7e051a090cc4c1f561b8cdde.patch";
hash = "sha256-WgKRQAEwSpXdQUnrZC1Bp4RfKg2J9kPkT1k6R2wwgT8=";
})
];
patches = [
./fix_run_checks.patch
./pr13-profile-path-fix-strace.patch
(fetchpatch {
# to be removed after next release
name = "feat-static-strace-path-support-at-compile-time.patch";
url = "https://github.com/desbma/shh/commit/da62ceeb227de853be06610721744667c6fe994b.patch";
hash = "sha256-p/W7HRZZ4TpIwrWN8wQB/SH3C8x3ZLXzwGV50oK/znQ=";
})
];
# buildFeatures = [ /*"gen-man-pages"*/ ];
env = {
SHH_STRACE_BIN_PATH = lib.getExe strace;
};
checkFeatures = [ "nix-build-env" ];
buildFeatures = lib.optional enableDocumentationFeature "generate-extra";
checkFlags = [
# no access to system modules in build env
"--skip=run_ls_modules"
# missing systemd daemon in build env
"--skip=run_systemctl"
# no raw socket cap in nix build
"--skip=run_ping_4"
"--skip=run_ping_6"
];
buildInputs = [
strace
systemd
];
nativeCheckInputs = [
strace
nativeBuildInputs = [
installShellFiles
systemd
strace
];
nativeCheckInputs = [
python3
iproute2
];
# todo elvish
postInstall = lib.optionalString enableDocumentationGeneration ''
mkdir -p target/{mangen,shellcomplete}
${
if isNativeDocgen then
''
$out/bin/shh gen-man-pages target/mangen
$out/bin/shh gen-shell-complete target/shellcomplete
''
else
''
unset SHH_STRACE_BIN_PATH
cargo run --features generate-extra -- gen-man-pages target/mangen
cargo run --features generate-extra -- gen-shell-complete target/shellcomplete
''
}
installManPage target/mangen/*
installShellCompletion --cmd ${pname} \
target/shellcomplete/${pname}.{bash,fish} \
--zsh target/shellcomplete/_${pname}
'';
# RUST_BACKTRACE = 1;
passthru.updateScript = nix-update-script { };
meta = {
description = "Automatic systemd service hardening guided by strace profiling";
homepage = "https://github.com/desbma/shh";
license = lib.licenses.gpl3Only;
platforms = lib.platforms.linux;
changelog = "https://github.com/desbma/shh/blob/v${version}/CHANGELOG.md";
mainProgram = "shh";
maintainers = with lib.maintainers; [
erdnaxe

View file

@ -1,83 +0,0 @@
commit 4d2c1556d769695770c95a982e0dcda4d70eee57
Author: kuflierl <41301536+kuflierl@users.noreply.github.com>
Date: Sun Apr 13 19:57:50 2025 +0200
service.rs: profile path fix for strace
Enable path env fixing when path env doesn't have strace to unbreak tool on unique systems and units.
This fixes handling on non FHS operating systems and systemd units that define their own PATH that doesn't include strace.
diff --git a/src/systemd/service.rs b/src/systemd/service.rs
index 908fdf0..e9294cf 100644
--- a/src/systemd/service.rs
+++ b/src/systemd/service.rs
@@ -7,6 +7,7 @@ use std::{
ops::RangeInclusive,
path::{Path, PathBuf},
process::{Command, Stdio},
+ ffi::OsString,
};
use anyhow::Context as _;
@@ -99,6 +100,41 @@ impl Service {
)
}
+ // A function for locating the parent directory i.e. PATH of an executable
+ fn resolve_exec_path<P>(exe_name: &P, path_env: OsString) -> Option<PathBuf>
+ where P: AsRef<Path> + ?Sized,
+ {
+ env::split_paths(&path_env).filter_map(|dir| {
+ let full_path = dir.join(&exe_name);
+ if full_path.is_file() {
+ Some(dir)
+ } else {
+ None
+ }
+ }).next()
+ }
+
+ // determine PATH env used for unit
+ pub(crate) fn get_exec_path(config_paths: &Vec<&Path>) -> anyhow::Result<String> {
+ let old_path_env_option = Self::config_vals("Environment", &config_paths)?
+ .into_iter().filter(|x| x.starts_with("\"PATH=")).last().map(|x| x.trim_matches('\"').get(5..).unwrap().to_owned());
+ Ok(match old_path_env_option {
+ Some(path_env) => {
+ log::info!("Found hard coded PATH environment in unit: {path_env}");
+ path_env
+ },
+ None => {
+ let output = Command::new("systemd-path").arg("search-binaries-default").output()?;
+ if !output.status.success() {
+ anyhow::bail!("systemd-path invocation failed with code {:?}", output.status);
+ }
+ let default_systemd_path = output.stdout.lines().next().ok_or_else(|| anyhow::anyhow!("Unable to get global systemd default PATH"))??;
+ log::info!("Could not find hard coded PATH environment in unit, using systemd default: {default_systemd_path}");
+ default_systemd_path
+ }
+ })
+ }
+
/// Get systemd "exposure level" for the service (0-100).
/// 100 means extremely exposed (no hardening), 0 means so sandboxed it can't do much.
/// Although this is a very crude heuristic, below 40-50 is generally good.
@@ -170,6 +206,20 @@ impl Service {
writeln!(fragment_file, "KillMode=control-group")?;
writeln!(fragment_file, "StandardOutput=journal")?;
+ // Modifying Env Path for strace availability if needed
+ let old_path_env = Self::get_exec_path(&config_paths)?;
+ match Self::resolve_exec_path("strace", (&old_path_env).into()) {
+ Some(_) => log::info!("Found strace in previous path, no correction needed"),
+ None => {
+ let path_with_strace = Self::resolve_exec_path("strace", env::var_os("PATH").unwrap()).unwrap();
+ log::info!("Found strace from local PATH in {}, inserting it into unit config!", path_with_strace.display());
+ let mut paths = env::split_paths(&old_path_env).collect::<Vec<_>>();
+ paths.push(path_with_strace);
+ let new_path = env::join_paths(paths)?;
+ writeln!(fragment_file, "Environment=\"PATH={}\"", new_path.to_str().unwrap())?;
+ },
+ }
+
// Profile data dir
let mut rng = rand::rng();
let profile_data_dir = PathBuf::from(format!(