mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user