passwd: implement Landlock filesystem sandboxing (#41)

Restricts passwd's filesystem access to only what it needs:
- /etc/ (read+write for passwd/shadow files)
- /dev/ (read+write for tty prompts)
- /usr/sbin/ (execute for nscd/sss_cache)

Uses the landlock crate (MIT/Apache-2.0) with best-effort mode:
silently does nothing on kernels < 5.13. The apply_landlock()
helper is in shadow-core::hardening so other tools can reuse it.

Skipped during #[cfg(test)] since Landlock is irreversible per-process.

Fixes #41
This commit is contained in:
Pierre Warnier
2026-04-03 12:18:35 +02:00
parent 829b37f7a8
commit 7fbea1f102
4 changed files with 59 additions and 15 deletions
+2
View File
@@ -15,6 +15,7 @@ libc = { workspace = true }
nix = { workspace = true }
thiserror = { workspace = true }
zeroize = { workspace = true }
landlock = { workspace = true, optional = true }
[dev-dependencies]
proptest = { workspace = true }
@@ -30,6 +31,7 @@ group = []
gshadow = []
login-defs = []
subid = []
landlock = ["dep:landlock"]
[lints]
workspace = true
+43
View File
@@ -57,6 +57,49 @@ pub fn sanitized_env() -> Vec<(String, String)> {
env
}
/// Restrict filesystem access via Landlock (Linux 5.13+).
///
/// Best-effort: silently does nothing on kernels without Landlock support.
/// `rw_paths` get read+write access, `ro_paths` get read-only access,
/// `exec_paths` get execute access. Everything else is denied.
#[cfg(feature = "landlock")]
pub fn apply_landlock(
writable: &[&std::path::Path],
readable: &[&std::path::Path],
exec_paths: &[&std::path::Path],
) {
use landlock::{
ABI, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, path_beneath_rules,
};
let abi = ABI::V5;
let all_access = AccessFs::from_all(abi);
let read_access = AccessFs::from_read(abi);
let exec_access = AccessFs::Execute | AccessFs::ReadFile | AccessFs::ReadDir;
let result = Ruleset::default()
.handle_access(all_access)
.and_then(Ruleset::create)
.and_then(|rs| rs.add_rules(path_beneath_rules(writable, all_access)))
.and_then(|rs| rs.add_rules(path_beneath_rules(readable, read_access)))
.and_then(|rs| rs.add_rules(path_beneath_rules(exec_paths, exec_access)))
.and_then(landlock::RulesetCreated::restrict_self);
// Best-effort: log but don't fail
if let Err(e) = result {
eprintln!("landlock: {e}");
}
}
/// No-op on non-Linux or when the `landlock` feature is disabled.
#[cfg(not(feature = "landlock"))]
pub fn apply_landlock(
_writable: &[&std::path::Path],
_readable: &[&std::path::Path],
_exec_paths: &[&std::path::Path],
) {
}
/// Run all standard hardening steps for a setuid-root tool.
///
/// Call at the top of `uumain` before any argument parsing.
+1 -1
View File
@@ -18,7 +18,7 @@ path = "src/main.rs"
clap = { workspace = true }
libc = { workspace = true }
nix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow", "login-defs"] }
shadow-core = { workspace = true, features = ["shadow", "login-defs", "landlock"] }
uucore = { workspace = true }
[features]
+13 -14
View File
@@ -117,23 +117,22 @@ impl UError for PasswdError {
/// Restrict filesystem access using landlock (Linux 5.13+).
///
/// Best-effort: silently does nothing on kernels that don't support landlock.
/// Best-effort Landlock sandboxing for passwd.
///
/// Restricts filesystem access to only what passwd needs:
/// read+write `/etc/` (passwd/shadow files) and `/dev/` (tty for prompts),
/// execute `/usr/sbin/` (`nscd`/`sss_cache` invalidation).
#[allow(unused_variables)]
fn apply_landlock(root: &SysRoot) {
#[cfg(target_os = "linux")]
apply_landlock_inner(root);
}
// Landlock is irreversible per-process, skip during tests
#[cfg(not(test))]
{
let etc = root.resolve("/etc");
let dev = Path::new("/dev");
let usr_sbin = Path::new("/usr/sbin");
#[cfg(target_os = "linux")]
fn apply_landlock_inner(_root: &SysRoot) {
// Landlock requires the landlock crate or raw syscalls.
// TODO(#41): Add landlock crate dependency and implement restriction.
//
// The restriction would be:
// - /etc/ (read + write for passwd/shadow files)
// - /dev/tty (read + write for password prompts)
// - /usr/sbin/ (execute for nscd/sss_cache)
// - deny everything else
shadow_core::hardening::apply_landlock(&[etc.as_path(), dev], &[], &[usr_sbin]);
}
}
// ---------------------------------------------------------------------------