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:
parent
10732cef68
commit
e3a5ffc8f7
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
.ci-store
|
||||
.direnv
|
||||
result*
|
||||
src/action/linux/selinux/nix.mod
|
17
flake.lock
17
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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" \
|
||||
.
|
||||
'';
|
||||
});
|
||||
|
|
|
@ -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" = { }; });
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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};
|
||||
|
|
125
src/action/linux/provision_selinux.rs
Normal file
125
src/action/linux/provision_selinux.rs
Normal 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(())
|
||||
}
|
9
src/action/linux/selinux/README.md
Normal file
9
src/action/linux/selinux/README.md
Normal 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.
|
5
src/action/linux/selinux/build.sh
Executable file
5
src/action/linux/selinux/build.sh
Executable 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
|
8
src/action/linux/selinux/nix.fc
Normal file
8
src/action/linux/selinux/nix.fc
Normal 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
|
BIN
src/action/linux/selinux/nix.pp
Normal file
BIN
src/action/linux/selinux/nix.pp
Normal file
Binary file not shown.
11
src/action/linux/selinux/nix.te
Normal file
11
src/action/linux/selinux/nix.te
Normal 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;
|
||||
}
|
|
@ -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),
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
// 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<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)
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_nix_not_already_installed() -> Result<(), PlannerError> {
|
||||
|
|
|
@ -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>() {
|
||||
|
|
Loading…
Reference in a new issue