diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2ebd6c4fa..411af3c00 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -27,6 +27,7 @@ - Pi-Cla - Sebastiano Tocci(Seba-T) - Minh Phan (MinhPhan8803) +- Kenton Groombridge (0xC0ncord) ## Acknowledgements diff --git a/Cargo.lock b/Cargo.lock index edb5bfb3a..1c2f851e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,7 +426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d06c690e5e2800f70c0cf8773a9fe7680d66e719dae9b4cabedd13ef4885d056" dependencies = [ "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "core-foundation", "devd-rs", @@ -503,6 +503,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease 0.2.6", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.16", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -524,6 +547,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" + [[package]] name = "blake3" version = "0.3.8" @@ -644,6 +673,15 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -714,6 +752,17 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "3.2.25" @@ -721,7 +770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_derive", "clap_lex", "indexmap", @@ -1308,6 +1357,12 @@ dependencies = [ "syn 2.0.17", ] +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "dyn-clone" version = "1.0.11" @@ -1673,7 +1728,7 @@ version = "0.13.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libgit2-sys", "log", @@ -1682,6 +1737,12 @@ dependencies = [ "url", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gloo" version = "0.8.0" @@ -2366,6 +2427,7 @@ dependencies = [ "reqwest", "rpassword 7.2.0", "rusqlite", + "selinux", "serde", "serde_json", "sketching", @@ -2550,6 +2612,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lber" version = "0.4.0" @@ -2615,6 +2683,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + [[package]] name = "libnss" version = "0.4.0" @@ -3017,7 +3095,7 @@ version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "foreign-types", "libc", @@ -3158,6 +3236,12 @@ dependencies = [ "proc-macro-hack", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "peg" version = "0.8.1" @@ -3281,7 +3365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "concurrent-queue", "libc", @@ -3317,6 +3401,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +dependencies = [ + "proc-macro2", + "syn 2.0.16", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3553,7 +3647,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -3562,7 +3656,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -3576,6 +3670,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "reference-counted-singleton" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bfbf25d7eb88ddcbb1ec3d755d0634da8f7657b2cb8b74089121409ab8228f" + [[package]] name = "regex" version = "1.8.3" @@ -3704,7 +3804,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3712,6 +3812,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -3736,7 +3842,7 @@ version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -3818,7 +3924,7 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -3835,6 +3941,32 @@ dependencies = [ "libc", ] +[[package]] +name = "selinux" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5ff8f655381fc80f470384da0a461b19b7bf18d685639ccf6df0abd3d2363d" +dependencies = [ + "bitflags 2.3.1", + "libc", + "once_cell", + "reference-counted-singleton", + "selinux-sys", + "thiserror", +] + +[[package]] +name = "selinux-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f37ccfd0557caec11f117e6d6103aaa217d1aad80cfb6ee3d0e1af5b568a1d9" +dependencies = [ + "bindgen", + "cc", + "dunce", + "walkdir", +] + [[package]] name = "semver" version = "0.9.0" @@ -4037,6 +4169,12 @@ dependencies = [ "dirs", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" version = "0.3.15" @@ -5042,6 +5180,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "whoami" version = "1.4.0" @@ -5365,7 +5514,7 @@ checksum = "b64c253c1d401f1ea868ca9988db63958cfa15a69f739101f338d6f05eea8301" dependencies = [ "boolinator", "once_cell", - "prettyplease", + "prettyplease 0.1.25", "proc-macro-error", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a565df4e1..5913f8de8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ scim_proto = "^0.2.0" # scim_proto = { path = "../scim/proto", version = "^0.2.0" } # scim_proto = { git = "https://github.com/kanidm/scim.git", version = "0.1.1" } +selinux = "^0.4.1" serde = "^1.0.163" serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" } serde_json = "^1.0.96" diff --git a/book/src/integrations/pam_and_nsswitch.md b/book/src/integrations/pam_and_nsswitch.md index 409191f37..4aee89760 100644 --- a/book/src/integrations/pam_and_nsswitch.md +++ b/book/src/integrations/pam_and_nsswitch.md @@ -55,6 +55,7 @@ home_alias = "spn" use_etc_skel = false uid_attr_map = "spn" gid_attr_map = "spn" +selinux = true ``` `pam_allowed_login_groups` defines a set of POSIX groups where membership of any of these groups @@ -89,6 +90,12 @@ when first created. Defaults to false. `gid_attr_map` chooses which attribute is used for domain local groups in presentation. Defaults to `spn`. Groups from a trust will always use spn. +`selinux` controls whether the `kanidm_unixd_tasks` daemon should detect and enable SELinux runtime +compatibility features to ensure that newly created home directories are labeled correctly. This +setting as no bearing on systems without SELinux, as these features will automatically be disabled +if SELinux is not detected when the daemon starts. Note that `kanidm_unixd_tasks` must also be built +with the SELinux feature flag for this functionality. Defaults to true. + You can then check the communication status of the daemon: ```bash diff --git a/examples/unixd b/examples/unixd index f60b7e82d..31cd66e2d 100644 --- a/examples/unixd +++ b/examples/unixd @@ -7,4 +7,4 @@ # home_alias = "spn" # use_etc_skel = false # uid_attr_map = "spn" -# gid_attr_map = "spn" \ No newline at end of file +# gid_attr_map = "spn" diff --git a/unix_integration/Cargo.toml b/unix_integration/Cargo.toml index 0ea623a1e..3a5e7e283 100644 --- a/unix_integration/Cargo.toml +++ b/unix_integration/Cargo.toml @@ -14,6 +14,7 @@ repository.workspace = true [features] default = ["unix"] unix = [] +selinux = ["dep:selinux"] [[bin]] name = "kanidm_unixd" @@ -55,6 +56,7 @@ r2d2.workspace = true r2d2_sqlite.workspace = true rpassword.workspace = true rusqlite.workspace = true +selinux = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sketching.workspace = true diff --git a/unix_integration/src/constants.rs b/unix_integration/src/constants.rs index a888f646d..ab6b8e644 100644 --- a/unix_integration/src/constants.rs +++ b/unix_integration/src/constants.rs @@ -13,3 +13,4 @@ pub const DEFAULT_HOME_ALIAS: Option = Some(HomeAttr::Spn); pub const DEFAULT_USE_ETC_SKEL: bool = false; pub const DEFAULT_UID_ATTR_MAP: UidAttr = UidAttr::Spn; pub const DEFAULT_GID_ATTR_MAP: UidAttr = UidAttr::Spn; +pub const DEFAULT_SELINUX: bool = true; diff --git a/unix_integration/src/lib.rs b/unix_integration/src/lib.rs index e1a34bd5c..bbefec968 100644 --- a/unix_integration/src/lib.rs +++ b/unix_integration/src/lib.rs @@ -27,6 +27,8 @@ pub mod client_sync; pub mod constants; #[cfg(target_family = "unix")] pub(crate) mod db; +#[cfg(all(target_family = "unix", feature = "selinux"))] +pub mod selinux_util; #[cfg(target_family = "unix")] pub mod unix_config; #[cfg(target_family = "unix")] diff --git a/unix_integration/src/selinux_util.rs b/unix_integration/src/selinux_util.rs new file mode 100644 index 000000000..04b415af4 --- /dev/null +++ b/unix_integration/src/selinux_util.rs @@ -0,0 +1,32 @@ +use std::ffi::CString; + +use selinux::{kernel_support, label::back_end::File, label::Labeler, KernelSupport}; + +pub fn supported() -> bool { + return !matches!(kernel_support(), KernelSupport::Unsupported); +} + +pub fn get_labeler() -> Result, String> { + if let Ok(v) = Labeler::new(&[], true) { + Ok(v) + } else { + Err("Failed getting handle for SELinux labeling".to_string()) + } +} + +pub fn do_setfscreatecon_for_path( + path_raw: &String, + labeler: &Labeler, +) -> Result<(), String> { + match labeler.look_up(&CString::new(path_raw.to_owned()).unwrap(), 0) { + Ok(context) => { + if let Err(_) = context.set_for_new_file_system_objects(true) { + return Err("Failed setting creation context home directory path".to_string()); + } + Ok(()) + } + Err(_) => { + return Err("Failed looking up default context for home directory path".to_string()); + } + } +} diff --git a/unix_integration/src/tasks_daemon.rs b/unix_integration/src/tasks_daemon.rs index 08485bca8..5bc2338d7 100644 --- a/unix_integration/src/tasks_daemon.rs +++ b/unix_integration/src/tasks_daemon.rs @@ -34,6 +34,15 @@ use tokio_util::codec::{Decoder, Encoder, Framed}; use users::{get_effective_gid, get_effective_uid}; use walkdir::WalkDir; +#[cfg(all(target_family = "unix", feature = "selinux"))] +use kanidm_unix_common::selinux_util; +#[cfg(all(target_family = "unix", feature = "selinux"))] +use selinux::SecurityContext; +#[cfg(all(target_family = "unix", feature = "selinux"))] +use std::process::Command; +#[cfg(all(target_family = "unix", feature = "selinux"))] +use users::get_user_by_uid; + struct TaskCodec; impl Decoder for TaskCodec { @@ -111,10 +120,35 @@ fn create_home_directory( return Err("Invalid/Corrupt home directory path - no prefix found".to_string()); } + // Get a handle to the SELinux labeling interface + #[cfg(all(target_family = "unix", feature = "selinux"))] + let labeler = selinux_util::get_labeler()?; + + // Construct a path for SELinux context lookups. + // We do this because the policy only associates a home directory to its owning + // user by the name of the directory. Since the real user's home directory is (by + // default) their uuid or spn, its context will always be the policy default + // (usually user_u or unconfined_u). This lookup path is used to ask the policy + // what the context SHOULD be, and we will create policy equivalence rules below + // so that relabels in the future do not break it. + #[cfg(all(target_family = "unix", feature = "selinux"))] + // Yes, gid, because we use the GID number for both the user's UID and primary GID + let sel_lookup_path_raw = match get_user_by_uid(info.gid) { + Some(v) => format!("{}{}", home_prefix, v.name().to_str().unwrap()), + None => { + return Err("Failed looking up username by uid for SELinux relabeling".to_string()); + } + }; + // Does the home directory exist? if !hd_path.exists() { // Set a umask let before = unsafe { umask(0o0027) }; + + // Set the SELinux security context for file creation + #[cfg(all(target_family = "unix", feature = "selinux"))] + selinux_util::do_setfscreatecon_for_path(&sel_lookup_path_raw, &labeler)?; + // Create the dir if let Err(e) = fs::create_dir_all(hd_path) { let _ = unsafe { umask(before) }; @@ -135,16 +169,48 @@ fn create_home_directory( .strip_prefix(skel_dir) .map_err(|e| e.to_string())?, ); + + #[cfg(all(target_family = "unix", feature = "selinux"))] + { + // Look up the correct SELinux context of this object + let sel_lookup_path = Path::new(&sel_lookup_path_raw).join( + entry + .path() + .strip_prefix(skel_dir) + .map_err(|e| e.to_string())?, + ); + selinux_util::do_setfscreatecon_for_path( + &sel_lookup_path.to_str().unwrap().to_string(), + &labeler, + )?; + } + if entry.path().is_dir() { fs::create_dir_all(dest).map_err(|e| e.to_string())?; } else { fs::copy(entry.path(), dest).map_err(|e| e.to_string())?; } chown(dest, info.gid)?; + + // Create equivalence rule in the SELinux policy + #[cfg(all(target_family = "unix", feature = "selinux"))] + if Command::new("semanage") + .args(["fcontext", "-ae", &sel_lookup_path_raw, &hd_path_raw]) + .spawn() + .is_err() + { + return Err("Failed creating SELinux policy equivalence rule".to_string()); + } } } } + // Reset object creation SELinux context to default + #[cfg(all(target_family = "unix", feature = "selinux"))] + if SecurityContext::set_default_context_for_new_file_system_objects().is_err() { + return Err("Failed resetting SELinux file creation contexts".to_string()); + } + let name_rel_path = Path::new(&name); // Does the aliases exist for alias in info.aliases.iter() { diff --git a/unix_integration/src/unix_config.rs b/unix_integration/src/unix_config.rs index fed00c64e..3fff33366 100644 --- a/unix_integration/src/unix_config.rs +++ b/unix_integration/src/unix_config.rs @@ -4,12 +4,15 @@ use std::fs::File; use std::io::{ErrorKind, Read}; use std::path::Path; +#[cfg(all(target_family = "unix", feature = "selinux"))] +use crate::selinux_util; + use serde::Deserialize; use crate::constants::{ DEFAULT_CACHE_TIMEOUT, DEFAULT_CONN_TIMEOUT, DEFAULT_DB_PATH, DEFAULT_GID_ATTR_MAP, - DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL, DEFAULT_SOCK_PATH, - DEFAULT_TASK_SOCK_PATH, DEFAULT_UID_ATTR_MAP, DEFAULT_USE_ETC_SKEL, + DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SELINUX, DEFAULT_SHELL, + DEFAULT_SOCK_PATH, DEFAULT_TASK_SOCK_PATH, DEFAULT_UID_ATTR_MAP, DEFAULT_USE_ETC_SKEL, }; #[derive(Debug, Deserialize)] @@ -27,6 +30,7 @@ struct ConfigInt { use_etc_skel: Option, uid_attr_map: Option, gid_attr_map: Option, + selinux: Option, } #[derive(Debug, Copy, Clone)] @@ -85,6 +89,7 @@ pub struct KanidmUnixdConfig { pub use_etc_skel: bool, pub uid_attr_map: UidAttr, pub gid_attr_map: UidAttr, + pub selinux: bool, } impl Default for KanidmUnixdConfig { @@ -115,7 +120,9 @@ impl Display for KanidmUnixdConfig { } writeln!(f, "uid_attr_map: {}", self.uid_attr_map)?; - writeln!(f, "gid_attr_map: {}", self.gid_attr_map) + writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?; + + writeln!(f, "selinux: {}", self.selinux) } } @@ -140,6 +147,7 @@ impl KanidmUnixdConfig { use_etc_skel: DEFAULT_USE_ETC_SKEL, uid_attr_map: DEFAULT_UID_ATTR_MAP, gid_attr_map: DEFAULT_GID_ATTR_MAP, + selinux: DEFAULT_SELINUX, } } @@ -246,6 +254,11 @@ impl KanidmUnixdConfig { } }) .unwrap_or(self.gid_attr_map), + selinux: match config.selinux.unwrap_or(self.selinux) { + #[cfg(all(target_family = "unix", feature = "selinux"))] + true => selinux_util::supported(), + _ => false, + }, }) } }