Skip to content

Commit

Permalink
Merge pull request #397 from cgwalters/more-labeling
Browse files Browse the repository at this point in the history
Rework SELinux labeling more
  • Loading branch information
cgwalters authored Mar 18, 2024
2 parents 67a597b + 28e5bed commit d039f26
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 114 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,10 @@ jobs:
- name: Integration tests
run: |
set -xeuo pipefail
image=quay.io/centos-bootc/centos-bootc-dev:stream9
echo 'ssh-ed25519 ABC0123 [email protected]' > test_authorized_keys
sudo podman run --rm -ti --privileged -v ./test_authorized_keys:/test_authorized_keys --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
quay.io/centos-bootc/centos-bootc-dev:stream9 bootc install to-filesystem \
${image} bootc install to-filesystem \
--karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target
ls -al /boot/loader/
sudo grep foo=bar /boot/loader/entries/*.conf
Expand All @@ -143,5 +144,5 @@ jobs:
sudo chattr -i /ostree/deploy/default/deploy/*
sudo rm /ostree/deploy/default -rf
sudo podman run --rm -ti --privileged --env BOOTC_SKIP_SELINUX_HOST_CHECK=1 --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
quay.io/centos-bootc/centos-bootc-dev:stream9 bootc install to-existing-root
sudo ls -ldZ / /ostree/deploy/default/deploy/* /ostree/deploy/default/deploy/*/etc
${image} bootc install to-existing-root
sudo podman run --rm -ti --privileged -v /:/target -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable ${image} bootc internal-tests verify-selinux /target/ostree --warn
6 changes: 6 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ pub(crate) enum TestingOpts {
image: String,
blockdev: Utf8PathBuf,
},
#[clap(name = "verify-selinux")]
VerifySELinux {
root: String,
#[clap(long)]
warn: bool,
},
}

/// Deploy and transactionally in-place with bootable container images.
Expand Down
87 changes: 52 additions & 35 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ pub(crate) mod baseline;
pub(crate) mod config;
pub(crate) mod osconfig;

use std::io::BufWriter;
use std::io::Write;
use std::os::fd::AsFd;
use std::os::unix::process::CommandExt;
Expand Down Expand Up @@ -301,17 +300,17 @@ pub(crate) struct State {
}

impl State {
// Wraps core lsm labeling functionality, conditionalizing based on source state
pub(crate) fn lsm_label(
&self,
target: &Utf8Path,
as_path: &Utf8Path,
recurse: bool,
) -> Result<()> {
if !self.source.selinux {
return Ok(());
#[context("Loading SELinux policy")]
pub(crate) fn load_policy(&self) -> Result<Option<ostree::SePolicy>> {
use std::os::fd::AsRawFd;
if !self.source.selinux || self.override_disable_selinux {
return Ok(None);
}
crate::lsm::lsm_label(target, as_path, recurse)
// We always use the physical container root to bootstrap policy
let rootfs = &Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
let r = ostree::SePolicy::new_at(rootfs.as_raw_fd(), gio::Cancellable::NONE)?;
tracing::debug!("Loaded SELinux policy: {}", r.name());
Ok(Some(r))
}
}

Expand Down Expand Up @@ -508,13 +507,17 @@ async fn initialize_ostree_root_from_self(
state: &State,
root_setup: &RootSetup,
) -> Result<InstallAleph> {
let sepolicy = state.load_policy()?;
let sepolicy = sepolicy.as_ref();

// Load a fd for the mounted target physical root
let rootfs_dir = &root_setup.rootfs_fd;
let rootfs = root_setup.rootfs.as_path();
let cancellable = gio::Cancellable::NONE;

// Ensure that the physical root is labeled.
// Another implementation: https://github.com/coreos/coreos-assembler/blob/3cd3307904593b3a131b81567b13a4d0b6fe7c90/src/create_disk.sh#L295
state.lsm_label(rootfs, "/".into(), false)?;
crate::lsm::ensure_dir_labeled(rootfs_dir, "", Some("/".into()), 0o755.into(), sepolicy)?;

// TODO: make configurable?
let stateroot = STATEROOT_DEFAULT;
Expand All @@ -527,7 +530,7 @@ async fn initialize_ostree_root_from_self(
// And also label /boot AKA xbootldr, if it exists
let bootdir = rootfs.join("boot");
if bootdir.try_exists()? {
state.lsm_label(&bootdir, "/boot".into(), false)?;
crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?;
}

// Default to avoiding grub2-mkconfig etc., but we need to use zipl on s390x.
Expand Down Expand Up @@ -555,8 +558,17 @@ async fn initialize_ostree_root_from_self(
.cwd(rootfs_dir)?
.run()?;

// Ensure everything in the ostree repo is labeled
state.lsm_label(&rootfs.join("ostree"), "/usr".into(), true)?;
// Bootstrap the initial labeling of the /ostree directory as usr_t
if let Some(policy) = sepolicy {
let ostree_dir = rootfs_dir.open_dir("ostree")?;
crate::lsm::ensure_dir_labeled(
&ostree_dir,
".",
Some("/usr".into()),
0o755.into(),
Some(policy),
)?;
}

let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
sysroot.load(cancellable)?;
Expand Down Expand Up @@ -618,8 +630,6 @@ async fn initialize_ostree_root_from_self(
println!("Installed: {target_image}");
println!(" Digest: {digest}");

// Write the entry for /boot to /etc/fstab. TODO: Encourage OSes to use the karg?
// Or better bind this with the grub data.
sysroot.load(cancellable)?;
let deployment = sysroot
.deployments()
Expand All @@ -631,28 +641,35 @@ async fn initialize_ostree_root_from_self(
let root = rootfs_dir
.open_dir(path.as_str())
.context("Opening deployment dir")?;
let root_path = &rootfs.join(&path.as_str());
let mut f = {
let mut opts = cap_std::fs::OpenOptions::new();
root.open_with("etc/fstab", opts.append(true).write(true).create(true))
.context("Opening etc/fstab")
.map(BufWriter::new)?
};
if let Some(boot) = root_setup.boot.as_ref() {
writeln!(f, "{}", boot.to_fstab())?;

// And do another recursive relabeling pass over the ostree-owned directories
// but avoid recursing into the deployment root (because that's a *distinct*
// logical root).
if let Some(policy) = sepolicy {
let deployment_root_meta = root.dir_metadata()?;
let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
for d in ["ostree", "boot"] {
let mut pathbuf = Utf8PathBuf::from(d);
crate::lsm::ensure_dir_labeled_recurse(
rootfs_dir,
&mut pathbuf,
policy,
Some(deployment_root_devino),
)
.with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
}
}
f.flush()?;

let fstab_path = root_path.join("etc/fstab");
state.lsm_label(&fstab_path, "/etc/fstab".into(), false)?;
// Write the entry for /boot to /etc/fstab. TODO: Encourage OSes to use the karg?
// Or better bind this with the grub data.
if let Some(boot) = root_setup.boot.as_ref() {
crate::lsm::atomic_replace_labeled(&root, "etc/fstab", 0o644.into(), sepolicy, |w| {
writeln!(w, "{}", boot.to_fstab()).map_err(Into::into)
})?;
}

if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
osconfig::inject_root_ssh_authorized_keys(
&root,
&root_path,
|target, path, recurse| state.lsm_label(target, path, recurse),
contents,
)?;
osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
}

let uname = rustix::system::uname();
Expand Down
14 changes: 9 additions & 5 deletions lib/src/install/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ pub(crate) fn install_create_rootfs(
.transpose()
.context("Parsing root size")?;

// Load the policy from the container root, which also must be our install root
let sepolicy = state.load_policy()?;
let sepolicy = sepolicy.as_ref();

// Create a temporary directory to use for mount points. Note that we're
// in a mount namespace, so these should not be visible on the host.
let rootfs = mntdir.join("rootfs");
Expand Down Expand Up @@ -368,15 +372,15 @@ pub(crate) fn install_create_rootfs(
.collect::<Vec<_>>();

mount::mount(&rootdev, &rootfs)?;
state.lsm_label(&rootfs, "/".into(), false)?;
let target_rootfs = Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?;
crate::lsm::ensure_dir_labeled(&target_rootfs, "", Some("/".into()), 0o755.into(), sepolicy)?;
let rootfs_fd = Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority())?;
let bootfs = rootfs.join("boot");
std::fs::create_dir(&bootfs).context("Creating /boot")?;
// The underlying directory on the root should be labeled
state.lsm_label(&bootfs, "/boot".into(), false)?;
// Create the underlying mount point directory, which should be labeled
crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;
mount::mount(bootdev, &bootfs)?;
// And we want to label the root mount of /boot
state.lsm_label(&bootfs, "/boot".into(), false)?;
crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?;

// Create the EFI system partition, if applicable
if let Some(esp_partno) = esp_partno {
Expand Down
65 changes: 19 additions & 46 deletions lib/src/install/osconfig.rs
Original file line number Diff line number Diff line change
@@ -1,78 +1,51 @@
use std::io::Write;

use anyhow::Result;
use camino::Utf8Path;
use cap_std::fs::Dir;
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
use cap_std_ext::cap_std;
use fn_error_context::context;
use ostree_ext::ostree;

const ETC_TMPFILES: &str = "etc/tmpfiles.d";
const ROOT_SSH_TMPFILE: &str = "bootc-root-ssh.conf";

#[context("Injecting root authorized_keys")]
pub(crate) fn inject_root_ssh_authorized_keys<F>(
pub(crate) fn inject_root_ssh_authorized_keys(
root: &Dir,
root_path: &Utf8Path,
lsm_label_fn: F,
sepolicy: Option<&ostree::SePolicy>,
contents: &str,
) -> Result<()>
where
F: Fn(&Utf8Path, &Utf8Path, bool) -> Result<()>,
{
) -> Result<()> {
// While not documented right now, this one looks like it does not newline wrap
let b64_encoded = ostree_ext::glib::base64_encode(contents.as_bytes());
// See the example in https://systemd.io/CREDENTIALS/
let tmpfiles_content = format!("f~ /root/.ssh/authorized_keys 600 root root - {b64_encoded}\n");

let tmpfiles_dir = Utf8Path::new(ETC_TMPFILES);
root.create_dir_all(tmpfiles_dir)?;
let target = tmpfiles_dir.join(ROOT_SSH_TMPFILE);
root.atomic_write(&target, &tmpfiles_content)?;

let as_path = Utf8Path::new(ETC_TMPFILES).join(ROOT_SSH_TMPFILE);
lsm_label_fn(
&root_path.join(&as_path),
&Utf8Path::new("/").join(&as_path),
false,
crate::lsm::ensure_dir_labeled(root, ETC_TMPFILES, None, 0o755.into(), sepolicy)?;
let tmpfiles_dir = root.open_dir(ETC_TMPFILES)?;
crate::lsm::atomic_replace_labeled(
&tmpfiles_dir,
ROOT_SSH_TMPFILE,
0o644.into(),
sepolicy,
|w| w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into),
)?;

println!("Injected: {target}");
println!("Injected: {ETC_TMPFILES}/{ROOT_SSH_TMPFILE}");
Ok(())
}

#[test]
fn test_inject_root_ssh() -> Result<()> {
use camino::Utf8PathBuf;
use std::cell::Cell;

let fake_lsm_label_called = Cell::new(0);
let fake_lsm_label = |target: &Utf8Path, as_path: &Utf8Path, recurse: bool| -> Result<()> {
assert_eq!(
target,
format!("/root/path/etc/tmpfiles.d/{ROOT_SSH_TMPFILE}")
);
assert_eq!(as_path, format!("/etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"));
assert_eq!(recurse, false);

fake_lsm_label_called.set(fake_lsm_label_called.get() + 1);
Ok(())
};

let root_path = &Utf8PathBuf::from("/root/path");
let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;

inject_root_ssh_authorized_keys(
root,
root_path,
fake_lsm_label,
"ssh-ed25519 ABCDE example@demo\n",
)
.unwrap();
// The code expects this to exist, reasonably so
root.create_dir("etc")?;
inject_root_ssh_authorized_keys(root, None, "ssh-ed25519 ABCDE example@demo\n").unwrap();

let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
assert_eq!(
content,
"f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
);
assert_eq!(fake_lsm_label_called, 1.into());

Ok(())
}
Loading

0 comments on commit d039f26

Please sign in to comment.