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 }
|
nix = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
zeroize = { workspace = true }
|
zeroize = { workspace = true }
|
||||||
|
landlock = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = { workspace = true }
|
proptest = { workspace = true }
|
||||||
@@ -30,6 +31,7 @@ group = []
|
|||||||
gshadow = []
|
gshadow = []
|
||||||
login-defs = []
|
login-defs = []
|
||||||
subid = []
|
subid = []
|
||||||
|
landlock = ["dep:landlock"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -57,6 +57,49 @@ pub fn sanitized_env() -> Vec<(String, String)> {
|
|||||||
env
|
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.
|
/// Run all standard hardening steps for a setuid-root tool.
|
||||||
///
|
///
|
||||||
/// Call at the top of `uumain` before any argument parsing.
|
/// Call at the top of `uumain` before any argument parsing.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ path = "src/main.rs"
|
|||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
nix = { 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 }
|
uucore = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
+13
-14
@@ -117,23 +117,22 @@ impl UError for PasswdError {
|
|||||||
|
|
||||||
/// Restrict filesystem access using landlock (Linux 5.13+).
|
/// 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)]
|
#[allow(unused_variables)]
|
||||||
fn apply_landlock(root: &SysRoot) {
|
fn apply_landlock(root: &SysRoot) {
|
||||||
#[cfg(target_os = "linux")]
|
// Landlock is irreversible per-process, skip during tests
|
||||||
apply_landlock_inner(root);
|
#[cfg(not(test))]
|
||||||
}
|
{
|
||||||
|
let etc = root.resolve("/etc");
|
||||||
|
let dev = Path::new("/dev");
|
||||||
|
let usr_sbin = Path::new("/usr/sbin");
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
shadow_core::hardening::apply_landlock(&[etc.as_path(), dev], &[], &[usr_sbin]);
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user