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
.direnv
result*
src/action/linux/selinux/nix.mod

View file

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

View file

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

View file

@ -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" \
.
'';
});

View file

@ -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" = { }; });
}

View file

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

View file

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

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::{
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<HashMap<String, serde_json::Value>, 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),
_ => (),
async fn detect_selinux() -> Result<bool, PlannerError> {
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)
}
},
// 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`")
},
} else {
Ok(false)
}
Ok(())
}
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)")]
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::<linux::LinuxErrorKind>() {