Add SELinux support (#465)

* Add SELinux support

* Nits

* Fix spellcheck

* Don't store mod, use locked shell

* Unwhoops a stale comment

* Speeling: Myy aarch neemesis

* Fix lost code:

* Add method note
This commit is contained in:
Ana Hobden 2023-05-17 07:27:14 -07:00 committed by GitHub
parent 10732cef68
commit e3a5ffc8f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 255 additions and 46 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
.ci-store .ci-store
.direnv .direnv
result* result*
src/action/linux/selinux/nix.mod

View file

@ -21,6 +21,22 @@
"type": "github" "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": { "lowdown-src": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -129,6 +145,7 @@
"root": { "root": {
"inputs": { "inputs": {
"fenix": "fenix", "fenix": "fenix",
"flake-compat": "flake-compat",
"naersk": "naersk", "naersk": "naersk",
"nix": "nix", "nix": "nix",
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"

View file

@ -19,6 +19,7 @@
# Omitting `inputs.nixpkgs.follows = "nixpkgs";` on purpose # Omitting `inputs.nixpkgs.follows = "nixpkgs";` on purpose
}; };
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
}; };
outputs = outputs =
@ -139,6 +140,8 @@
cacert cacert
cargo-audit cargo-audit
nixpkgs-fmt nixpkgs-fmt
semodule-utils
checkpolicy
check.check-rustfmt check.check-rustfmt
check.check-spelling check.check-spelling
check.check-nixpkgs-fmt check.check-nixpkgs-fmt

View file

@ -18,8 +18,8 @@ in
runtimeInputs = with pkgs; [ git codespell ]; runtimeInputs = with pkgs; [ git codespell ];
text = '' text = ''
codespell \ codespell \
--ignore-words-list ba,sur,crate,pullrequest,pullrequests,ser,distroname \ --ignore-words-list="ba,sur,crate,pullrequest,pullrequests,ser,distroname" \
--skip target,.git \ --skip="./target,.git,./src/action/linux/selinux" \
. .
''; '';
}); });

View file

@ -174,7 +174,7 @@ let
uninstallCheck = installCases.install-default.uninstallCheck; uninstallCheck = installCases.install-default.uninstallCheck;
}; };
}; };
cureCases = { cureSelfCases = {
cure-self-linux-working = { cure-self-linux-working = {
preinstall = '' preinstall = ''
${nix-installer-install-quiet} ${nix-installer-install-quiet}
@ -253,6 +253,8 @@ let
uninstall = installCases.install-default.uninstall; uninstall = installCases.install-default.uninstall;
uninstallCheck = installCases.install-default.uninstallCheck; uninstallCheck = installCases.install-default.uninstallCheck;
}; };
};
cureScriptCases = {
cure-script-multi-self-broken-no-nix-path = { cure-script-multi-self-broken-no-nix-path = {
preinstall = '' preinstall = ''
${cure-script-multi-user} ${cure-script-multi-user}
@ -413,7 +415,7 @@ let
}; };
rootDisk = "box.img"; rootDisk = "box.img";
system = "x86_64-linux"; system = "x86_64-linux";
postBoot = disableSELinux; upstreamScriptsWork = false; # SELinux!
}; };
"fedora-v37" = { "fedora-v37" = {
@ -423,7 +425,7 @@ let
}; };
rootDisk = "box.img"; rootDisk = "box.img";
system = "x86_64-linux"; system = "x86_64-linux";
postBoot = disableSELinux; upstreamScriptsWork = false; # SELinux!
}; };
# Currently fails with 'error while loading shared libraries: # Currently fails with 'error while loading shared libraries:
@ -435,7 +437,7 @@ let
hash = "sha256-QwzbvRoRRGqUCQptM7X/InRWFSP2sqwRt2HaaO6zBGM="; hash = "sha256-QwzbvRoRRGqUCQptM7X/InRWFSP2sqwRt2HaaO6zBGM=";
}; };
rootDisk = "box.img"; rootDisk = "box.img";
postBoot = disableSELinux; upstreamScriptsWork = false; # SELinux!
system = "x86_64-linux"; system = "x86_64-linux";
}; };
*/ */
@ -446,7 +448,7 @@ let
hash = "sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U="; hash = "sha256-b4afnqKCO9oWXgYHb9DeQ2berSwOjS27rSd9TxXDc/U=";
}; };
rootDisk = "box.img"; rootDisk = "box.img";
postBoot = disableSELinux; upstreamScriptsWork = false; # SELinux!
system = "x86_64-linux"; system = "x86_64-linux";
}; };
@ -457,7 +459,7 @@ let
}; };
rootDisk = "box.img"; rootDisk = "box.img";
system = "x86_64-linux"; system = "x86_64-linux";
postBoot = disableSELinux; upstreamScriptsWork = false; # SELinux!
}; };
"rhel-v9" = { "rhel-v9" = {
@ -467,7 +469,7 @@ let
}; };
rootDisk = "box.img"; rootDisk = "box.img";
system = "x86_64-linux"; system = "x86_64-linux";
postBoot = disableSELinux; upstreamScriptsWork = false; # SELinux!
extraQemuOpts = "-cpu Westmere-v2"; extraQemuOpts = "-cpu Westmere-v2";
}; };
@ -596,11 +598,13 @@ let
) )
images; 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; 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; uninstall-tests = makeTests "uninstall" uninstallCases;
@ -610,14 +614,14 @@ let
name = "all"; name = "all";
constituents = [ constituents = [
install-tests."${imageName}"."x86_64-linux".install 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 uninstall-tests."${imageName}"."x86_64-linux".uninstall
]; ] ++ (lib.optional (image.upstreamScriptsWork or false) cure-script-tests."${imageName}"."x86_64-linux".cure-script);
}); });
}) })
images; 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 in
lib.recursiveUpdate joined-tests { lib.recursiveUpdate joined-tests {
@ -626,5 +630,5 @@ lib.recursiveUpdate joined-tests {
name = caseName; name = caseName;
constituents = pkgs.lib.mapAttrsToList (name: value: value."x86_64-linux"."${caseName}") joined-tests; 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" = { }; });
} }

View file

@ -300,6 +300,12 @@ impl Action for ConfigureInitService {
Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST) Self::check_if_systemd_unit_exists(SERVICE_SRC, SERVICE_DEST)
.await .await
.map_err(Self::error)?; .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) tokio::fs::symlink(SERVICE_SRC, SERVICE_DEST)
.await .await
.map_err(|e| { .map_err(|e| {
@ -310,10 +316,15 @@ impl Action for ConfigureInitService {
) )
}) })
.map_err(Self::error)?; .map_err(Self::error)?;
Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST) Self::check_if_systemd_unit_exists(SOCKET_SRC, SOCKET_DEST)
.await .await
.map_err(Self::error)?; .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) tokio::fs::symlink(SOCKET_SRC, SOCKET_DEST)
.await .await
.map_err(|e| { .map_err(|e| {

View file

@ -1,3 +1,5 @@
pub(crate) mod provision_selinux;
pub(crate) mod start_systemd_unit; pub(crate) mod start_systemd_unit;
pub use provision_selinux::ProvisionSelinux;
pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError}; pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError};

View file

@ -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<StatefulAction<Self>, 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<ActionDescription> {
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<ActionDescription> {
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(())
}

View file

@ -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.

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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;
}

View file

@ -2,6 +2,7 @@ use crate::{
action::{ action::{
base::{CreateDirectory, RemoveDirectory}, base::{CreateDirectory, RemoveDirectory},
common::{ConfigureInitService, ConfigureNix, ProvisionNix}, common::{ConfigureInitService, ConfigureNix, ProvisionNix},
linux::ProvisionSelinux,
StatefulAction, StatefulAction,
}, },
error::HasExpectedErrors, error::HasExpectedErrors,
@ -12,6 +13,7 @@ use crate::{
}; };
use std::{collections::HashMap, path::Path}; use std::{collections::HashMap, path::Path};
use tokio::process::Command; use tokio::process::Command;
use which::which;
use super::ShellProfileLocations; use super::ShellProfileLocations;
@ -42,25 +44,44 @@ impl Planner for Linux {
check_not_wsl1()?; check_not_wsl1()?;
check_not_selinux().await?; let has_selinux = detect_selinux().await?;
if self.init.init == InitSystem::Systemd && self.init.start_daemon { if self.init.init == InitSystem::Systemd && self.init.start_daemon {
check_systemd_active()?; check_systemd_active()?;
} }
Ok(vec![ let mut plan = vec![];
plan.push(
CreateDirectory::plan("/nix", None, None, 0o0755, true) CreateDirectory::plan("/nix", None, None, 0o0755, true)
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .boxed(),
);
plan.push(
ProvisionNix::plan(&self.settings.clone()) ProvisionNix::plan(&self.settings.clone())
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .boxed(),
);
plan.push(
ConfigureNix::plan(ShellProfileLocations::default(), &self.settings) ConfigureNix::plan(ShellProfileLocations::default(), &self.settings)
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .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( ConfigureInitService::plan(
self.init.init, self.init.init,
self.init.start_daemon, self.init.start_daemon,
@ -69,11 +90,15 @@ impl Planner for Linux {
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .boxed(),
);
plan.push(
RemoveDirectory::plan(crate::settings::SCRATCH_DIR) RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .boxed(),
]) );
Ok(plan)
} }
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> { fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
@ -139,26 +164,19 @@ fn check_not_wsl1() -> Result<(), PlannerError> {
Ok(()) Ok(())
} }
async fn check_not_selinux() -> Result<(), PlannerError> { async fn detect_selinux() -> Result<bool, PlannerError> {
// We currently do not support SELinux if Path::new("/sys/fs/selinux").exists() {
match Command::new("getenforce").output().await { // We expect systems with SELinux to have the normal SELinux tools.
Ok(output) => { let has_semodule = which("semodule").is_ok();
let stdout_string = String::from_utf8(output.stdout).map_err(PlannerError::Utf8)?; let has_restorecon = which("restorecon").is_ok();
tracing::trace!(getenforce_stdout = stdout_string, "SELinux detected"); if !(has_semodule && has_restorecon) {
match stdout_string.trim() { Err(PlannerError::SelinuxRequirements)
"Enforcing" => return Err(PlannerError::SelinuxEnforcing), } else {
_ => (), Ok(true)
} }
}, } else {
// The device doesn't have SELinux set up Ok(false)
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`")
},
} }
Ok(())
} }
async fn check_nix_not_already_installed() -> Result<(), PlannerError> { async fn check_nix_not_already_installed() -> Result<(), PlannerError> {

View file

@ -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)")] #[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, RosettaDetected,
/// A Linux SELinux related error /// A Linux SELinux related error
#[error("\ #[error("Unable to install on an SELinux system without common SELinux tooling, the binaries `restorecon`, and `semodule` are required")]
This installer doesn't yet support SELinux in `Enforcing` mode.\n SelinuxRequirements,
\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,
/// A UTF-8 related error /// A UTF-8 related error
#[error("UTF-8 error")] #[error("UTF-8 error")]
Utf8(#[from] FromUtf8Error), Utf8(#[from] FromUtf8Error),
@ -401,7 +396,7 @@ impl HasExpectedErrors for PlannerError {
PlannerError::Sysctl(_) => None, PlannerError::Sysctl(_) => None,
this @ PlannerError::RosettaDetected => Some(Box::new(this)), this @ PlannerError::RosettaDetected => Some(Box::new(this)),
PlannerError::Utf8(_) => None, PlannerError::Utf8(_) => None,
PlannerError::SelinuxEnforcing => Some(Box::new(self)), PlannerError::SelinuxRequirements => Some(Box::new(self)),
PlannerError::Custom(e) => { PlannerError::Custom(e) => {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Some(err) = e.downcast_ref::<linux::LinuxErrorKind>() { if let Some(err) = e.downcast_ref::<linux::LinuxErrorKind>() {