diff --git a/.gitignore b/.gitignore index d71551e..bbcb978 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .ci-store .direnv result* +src/action/linux/selinux/nix.mod \ No newline at end of file diff --git a/flake.lock b/flake.lock index a52e4d0..7930ae9 100644 --- a/flake.lock +++ b/flake.lock @@ -21,6 +21,22 @@ "type": "github" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "lowdown-src": { "flake": false, "locked": { @@ -129,6 +145,7 @@ "root": { "inputs": { "fenix": "fenix", + "flake-compat": "flake-compat", "naersk": "naersk", "nix": "nix", "nixpkgs": "nixpkgs_2" diff --git a/flake.nix b/flake.nix index 437f292..16914a5 100644 --- a/flake.nix +++ b/flake.nix @@ -19,6 +19,7 @@ # Omitting `inputs.nixpkgs.follows = "nixpkgs";` on purpose }; + flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; }; outputs = @@ -139,6 +140,8 @@ cacert cargo-audit nixpkgs-fmt + semodule-utils + checkpolicy check.check-rustfmt check.check-spelling check.check-nixpkgs-fmt diff --git a/nix/check.nix b/nix/check.nix index 31badf4..f271038 100644 --- a/nix/check.nix +++ b/nix/check.nix @@ -18,8 +18,8 @@ in runtimeInputs = with pkgs; [ git codespell ]; text = '' codespell \ - --ignore-words-list ba,sur,crate,pullrequest,pullrequests,ser,distroname \ - --skip target,.git \ + --ignore-words-list="ba,sur,crate,pullrequest,pullrequests,ser,distroname" \ + --skip="./target,.git,./src/action/linux/selinux" \ . ''; }); diff --git a/nix/tests/vm-test/default.nix b/nix/tests/vm-test/default.nix index 9e57285..47c1199 100644 --- a/nix/tests/vm-test/default.nix +++ b/nix/tests/vm-test/default.nix @@ -174,7 +174,7 @@ let uninstallCheck = installCases.install-default.uninstallCheck; }; }; - cureCases = { + cureSelfCases = { cure-self-linux-working = { preinstall = '' ${nix-installer-install-quiet} @@ -253,6 +253,8 @@ let uninstall = installCases.install-default.uninstall; uninstallCheck = installCases.install-default.uninstallCheck; }; + }; + cureScriptCases = { cure-script-multi-self-broken-no-nix-path = { preinstall = '' ${cure-script-multi-user} @@ -413,7 +415,7 @@ let }; rootDisk = "box.img"; system = "x86_64-linux"; - postBoot = disableSELinux; + upstreamScriptsWork = false; # SELinux! }; "fedora-v37" = { @@ -423,7 +425,7 @@ let }; rootDisk = "box.img"; system = "x86_64-linux"; - postBoot = disableSELinux; + upstreamScriptsWork = false; # SELinux! }; # Currently fails with 'error while loading shared libraries: @@ -435,7 +437,7 @@ let hash = "sha256-QwzbvRoRRGqUCQptM7X/InRWFSP2sqwRt2HaaO6zBGM="; }; rootDisk = "box.img"; - postBoot = disableSELinux; + upstreamScriptsWork = false; # SELinux! system = "x86_64-linux"; }; */ @@ -446,7 +448,7 @@ let hash = "sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="; }; rootDisk = "box.img"; - postBoot = disableSELinux; + upstreamScriptsWork = false; # SELinux! system = "x86_64-linux"; }; @@ -457,7 +459,7 @@ let }; rootDisk = "box.img"; system = "x86_64-linux"; - postBoot = disableSELinux; + upstreamScriptsWork = false; # SELinux! }; "rhel-v9" = { @@ -467,7 +469,7 @@ let }; rootDisk = "box.img"; system = "x86_64-linux"; - postBoot = disableSELinux; + upstreamScriptsWork = false; # SELinux! extraQemuOpts = "-cpu Westmere-v2"; }; @@ -596,11 +598,13 @@ let ) images; - allCases = lib.recursiveUpdate (lib.recursiveUpdate installCases cureCases) uninstallCases; + allCases = lib.recursiveUpdate (lib.recursiveUpdate installCases (lib.recursiveUpdate cureSelfCases cureScriptCases)) uninstallCases; install-tests = makeTests "install" installCases; - cure-tests = makeTests "cure" cureCases; + cure-self-tests = makeTests "cure-self" cureSelfCases; + + cure-script-tests = makeTests "cure-script" cureScriptCases; uninstall-tests = makeTests "uninstall" uninstallCases; @@ -610,14 +614,14 @@ let name = "all"; constituents = [ install-tests."${imageName}"."x86_64-linux".install - cure-tests."${imageName}"."x86_64-linux".cure + cure-self-tests."${imageName}"."x86_64-linux".cure-self uninstall-tests."${imageName}"."x86_64-linux".uninstall - ]; + ] ++ (lib.optional (image.upstreamScriptsWork or false) cure-script-tests."${imageName}"."x86_64-linux".cure-script); }); }) images; - joined-tests = lib.recursiveUpdate (lib.recursiveUpdate (lib.recursiveUpdate cure-tests install-tests) uninstall-tests) all-tests; + joined-tests = lib.recursiveUpdate (lib.recursiveUpdate (lib.recursiveUpdate install-tests (lib.recursiveUpdate cure-self-tests cure-script-tests)) uninstall-tests) all-tests; in lib.recursiveUpdate joined-tests { @@ -626,5 +630,5 @@ lib.recursiveUpdate joined-tests { name = caseName; constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux"."${caseName}") joined-tests; } - )) (allCases // { "cure" = { }; "install" = { }; "uninstall" = { }; "all" = { }; }); + )) (allCases // { "cure-self" = { }; "cure-script" = { }; "install" = { }; "uninstall" = { }; "all" = { }; }); } diff --git a/src/action/common/configure_init_service.rs b/src/action/common/configure_init_service.rs index d5f3b6b..fec8d1e 100644 --- a/src/action/common/configure_init_service.rs +++ b/src/action/common/configure_init_service.rs @@ -300,6 +300,12 @@ impl Action for ConfigureInitService { Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST) .await .map_err(Self::error)?; + if Path::new(SERVICE_DEST).exists() { + tokio::fs::remove_file(SERVICE_DEST) + .await + .map_err(|e| ActionErrorKind::Remove(SERVICE_DEST.into(), e)) + .map_err(Self::error)?; + } tokio::fs::symlink(SERVICE_SRC, SERVICE_DEST) .await .map_err(|e| { @@ -310,10 +316,15 @@ impl Action for ConfigureInitService { ) }) .map_err(Self::error)?; - Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST) .await .map_err(Self::error)?; + if Path::new(SOCKET_DEST).exists() { + tokio::fs::remove_file(SOCKET_DEST) + .await + .map_err(|e| ActionErrorKind::Remove(SOCKET_DEST.into(), e)) + .map_err(Self::error)?; + } tokio::fs::symlink(SOCKET_SRC, SOCKET_DEST) .await .map_err(|e| { diff --git a/src/action/linux/mod.rs b/src/action/linux/mod.rs index cdd4b80..046133e 100644 --- a/src/action/linux/mod.rs +++ b/src/action/linux/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod provision_selinux; pub(crate) mod start_systemd_unit; +pub use provision_selinux::ProvisionSelinux; pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError}; diff --git a/src/action/linux/provision_selinux.rs b/src/action/linux/provision_selinux.rs new file mode 100644 index 0000000..b7ea34a --- /dev/null +++ b/src/action/linux/provision_selinux.rs @@ -0,0 +1,125 @@ +use std::path::{Path, PathBuf}; + +use tokio::fs::{create_dir_all, remove_file}; +use tokio::process::Command; +use tracing::{span, Span}; + +use crate::action::{ActionError, ActionErrorKind, ActionTag}; +use crate::execute_command; + +use crate::action::{Action, ActionDescription, StatefulAction}; + +const SE_LINUX_POLICY_PP_CONTENT: &[u8] = include_bytes!("selinux/nix.pp"); + +/** +Provision the selinux/nix.pp for SELinux compatibility +*/ +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ProvisionSelinux { + policy_path: PathBuf, +} + +impl ProvisionSelinux { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan(policy_path: PathBuf) -> Result, ActionError> { + let this = Self { policy_path }; + + // Note: `restorecon` requires us to not just skip this, even if everything is in place. + + Ok(StatefulAction::uncompleted(this)) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "provision_selinux")] +impl Action for ProvisionSelinux { + fn action_tag() -> ActionTag { + ActionTag("provision_selinux") + } + fn tracing_synopsis(&self) -> String { + format!("Install an SELinux Policy for Nix") + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "provision_selinux", + policy_path = %self.policy_path.display() + ) + } + + fn execute_description(&self) -> Vec { + vec![ActionDescription::new( + self.tracing_synopsis(), + vec![format!( + "On SELinux systems (such as Fedora) a policy for Nix needs to be configured for correct operation." + )], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + if self.policy_path.exists() { + // Rebuild it. + remove_existing_policy(&self.policy_path) + .await + .map_err(Self::error)?; + } + + if let Some(parent) = self.policy_path.parent() { + create_dir_all(&parent) + .await + .map_err(|e| ActionErrorKind::CreateDirectory(parent.into(), e)) + .map_err(Self::error)?; + } + + tokio::fs::write(&self.policy_path, SE_LINUX_POLICY_PP_CONTENT) + .await + .map_err(|e| ActionErrorKind::Write(self.policy_path.clone(), e)) + .map_err(Self::error)?; + + execute_command( + Command::new("semodule") + .arg("--install") + .arg(&self.policy_path), + ) + .await + .map_err(Self::error)?; + + execute_command(Command::new("restorecon").args(["-FR", "/nix"])) + .await + .map_err(Self::error)?; + + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new( + "Remove the SELinux policy for Nix".into(), + vec![], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + if self.policy_path.exists() { + remove_existing_policy(&self.policy_path) + .await + .map_err(Self::error)?; + } + + Ok(()) + } +} + +async fn remove_existing_policy(policy_path: &Path) -> Result<(), ActionErrorKind> { + execute_command(Command::new("semodule").arg("--remove").arg("nix")).await?; + + remove_file(&policy_path) + .await + .map_err(|e| ActionErrorKind::Remove(policy_path.into(), e))?; + + execute_command(Command::new("restorecon").args(["-FR", "/nix"])).await?; + + Ok(()) +} diff --git a/src/action/linux/selinux/README.md b/src/action/linux/selinux/README.md new file mode 100644 index 0000000..9da2f85 --- /dev/null +++ b/src/action/linux/selinux/README.md @@ -0,0 +1,9 @@ +To refresh the output `pp` file: + +```bash +./build.sh +``` + +## Method + +We use the same method and definitions as https://github.com/nix-community/nix-installers/tree/master/selinux. \ No newline at end of file diff --git a/src/action/linux/selinux/build.sh b/src/action/linux/selinux/build.sh new file mode 100755 index 0000000..b44b15f --- /dev/null +++ b/src/action/linux/selinux/build.sh @@ -0,0 +1,5 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash ../../../../shell.nix + +checkmodule -M -m -c 5 -o nix.mod nix.te +semodule_package -o nix.pp -m nix.mod -f nix.fc \ No newline at end of file diff --git a/src/action/linux/selinux/nix.fc b/src/action/linux/selinux/nix.fc new file mode 100644 index 0000000..32cc479 --- /dev/null +++ b/src/action/linux/selinux/nix.fc @@ -0,0 +1,8 @@ +/nix/store/[^/]+/s?bin(/.*)? system_u:object_r:bin_t:s0 +/nix/store/[^/]+/lib/systemd/system(/.*)? system_u:object_r:systemd_unit_file_t:s0 +/nix/store/[^/]+/lib(/.*)? system_u:object_r:lib_t:s0 +/nix/store/[^/]+/man(/.*)? system_u:object_r:man_t:s0 +/nix/store/[^/]+/etc(/.*)? system_u:object_r:etc_t:s0 +/nix/store/[^/]+/share(/.*)? system_u:object_r:usr_t:s0 +/nix/var/nix/daemon-socket(/.*)? system_u:object_r:var_run_t:s0 +/nix/var/nix/profiles(/per-user/[^/]+)?/[^/]+ system_u:object_r:usr_t:s0 \ No newline at end of file diff --git a/src/action/linux/selinux/nix.pp b/src/action/linux/selinux/nix.pp new file mode 100644 index 0000000..de72136 Binary files /dev/null and b/src/action/linux/selinux/nix.pp differ diff --git a/src/action/linux/selinux/nix.te b/src/action/linux/selinux/nix.te new file mode 100644 index 0000000..f8ea7b1 --- /dev/null +++ b/src/action/linux/selinux/nix.te @@ -0,0 +1,11 @@ +module nix 1.0; + +require { + type bin_t; + type lib_t; + type man_t; + type usr_t; + type etc_t; + type var_run_t; + type systemd_unit_file_t; +} \ No newline at end of file diff --git a/src/planner/linux.rs b/src/planner/linux.rs index a230b9c..822b3d8 100644 --- a/src/planner/linux.rs +++ b/src/planner/linux.rs @@ -2,6 +2,7 @@ use crate::{ action::{ base::{CreateDirectory, RemoveDirectory}, common::{ConfigureInitService, ConfigureNix, ProvisionNix}, + linux::ProvisionSelinux, StatefulAction, }, error::HasExpectedErrors, @@ -12,6 +13,7 @@ use crate::{ }; use std::{collections::HashMap, path::Path}; use tokio::process::Command; +use which::which; use super::ShellProfileLocations; @@ -42,25 +44,44 @@ impl Planner for Linux { check_not_wsl1()?; - check_not_selinux().await?; + let has_selinux = detect_selinux().await?; if self.init.init == InitSystem::Systemd && self.init.start_daemon { check_systemd_active()?; } - Ok(vec![ + let mut plan = vec![]; + + plan.push( CreateDirectory::plan("/nix", None, None, 0o0755, true) .await .map_err(PlannerError::Action)? .boxed(), + ); + + plan.push( ProvisionNix::plan(&self.settings.clone()) .await .map_err(PlannerError::Action)? .boxed(), + ); + plan.push( ConfigureNix::plan(ShellProfileLocations::default(), &self.settings) .await .map_err(PlannerError::Action)? .boxed(), + ); + + if has_selinux { + plan.push( + ProvisionSelinux::plan("/usr/share/selinux/packages/nix.pp".into()) + .await + .map_err(PlannerError::Action)? + .boxed(), + ); + } + + plan.push( ConfigureInitService::plan( self.init.init, self.init.start_daemon, @@ -69,11 +90,15 @@ impl Planner for Linux { .await .map_err(PlannerError::Action)? .boxed(), + ); + plan.push( RemoveDirectory::plan(crate::settings::SCRATCH_DIR) .await .map_err(PlannerError::Action)? .boxed(), - ]) + ); + + Ok(plan) } fn settings(&self) -> Result, InstallSettingsError> { @@ -139,26 +164,19 @@ fn check_not_wsl1() -> Result<(), PlannerError> { Ok(()) } -async fn check_not_selinux() -> Result<(), PlannerError> { - // We currently do not support SELinux - match Command::new("getenforce").output().await { - Ok(output) => { - let stdout_string = String::from_utf8(output.stdout).map_err(PlannerError::Utf8)?; - tracing::trace!(getenforce_stdout = stdout_string, "SELinux detected"); - match stdout_string.trim() { - "Enforcing" => return Err(PlannerError::SelinuxEnforcing), - _ => (), - } - }, - // The device doesn't have SELinux set up - Err(e) if e.kind() == std::io::ErrorKind::NotFound => (), - // Some unknown error - Err(e) => { - tracing::warn!(error = ?e, "Got an error checking for SELinux setting, this install may fail if SELinux is set to `Enforcing`") - }, +async fn detect_selinux() -> Result { + if Path::new("/sys/fs/selinux").exists() { + // We expect systems with SELinux to have the normal SELinux tools. + let has_semodule = which("semodule").is_ok(); + let has_restorecon = which("restorecon").is_ok(); + if !(has_semodule && has_restorecon) { + Err(PlannerError::SelinuxRequirements) + } else { + Ok(true) + } + } else { + Ok(false) } - - Ok(()) } async fn check_nix_not_already_installed() -> Result<(), PlannerError> { diff --git a/src/planner/mod.rs b/src/planner/mod.rs index 3a1553a..3d2d9b0 100644 --- a/src/planner/mod.rs +++ b/src/planner/mod.rs @@ -367,13 +367,8 @@ pub enum PlannerError { #[error("Detected that this process is running under Rosetta, using Nix in Rosetta is not supported (Please open an issue with your use case)")] RosettaDetected, /// A Linux SELinux related error - #[error("\ - This installer doesn't yet support SELinux in `Enforcing` mode.\n - \n\ - If desirable, consider setting SELinux to `Permissive` mode with `setenforce Permissive`.\n\ - \n\ - If SELinux is important to you, please see https://github.com/DeterminateSystems/nix-installer/issues/124.")] - SelinuxEnforcing, + #[error("Unable to install on an SELinux system without common SELinux tooling, the binaries `restorecon`, and `semodule` are required")] + SelinuxRequirements, /// A UTF-8 related error #[error("UTF-8 error")] Utf8(#[from] FromUtf8Error), @@ -401,7 +396,7 @@ impl HasExpectedErrors for PlannerError { PlannerError::Sysctl(_) => None, this @ PlannerError::RosettaDetected => Some(Box::new(this)), PlannerError::Utf8(_) => None, - PlannerError::SelinuxEnforcing => Some(Box::new(self)), + PlannerError::SelinuxRequirements => Some(Box::new(self)), PlannerError::Custom(e) => { #[cfg(target_os = "linux")] if let Some(err) = e.downcast_ref::() {