Skip to content

Commit 28e5bed

Browse files
committed
Rework SELinux labeling more
First, in the install code, acquire a proper policy object. Add helpers for writing files/directories that take a policy object and operate *solely* using fd-relative operations and don't fork off helper processes. This is a notable cleanup because we don't need to juggle absolute file paths *and* fds, which avoids a lot of confusion. Our usage of a wrapper for the cap-std-ext atomic write API for generating files ensures that if the file is present, it will always have the correct label without any race conditions. Change the one place we now call `chcon` as a helper process to be an explicit recursive selinux relabeling. In the future we should switch to using a direct API instead of forking off `/usr/bin/chcon` - then everything would be fd-relative. Signed-off-by: Colin Walters <[email protected]>
1 parent bae7e0d commit 28e5bed

File tree

7 files changed

+372
-114
lines changed

7 files changed

+372
-114
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,10 @@ jobs:
132132
- name: Integration tests
133133
run: |
134134
set -xeuo pipefail
135+
image=quay.io/centos-bootc/centos-bootc-dev:stream9
135136
echo 'ssh-ed25519 ABC0123 [email protected]' > test_authorized_keys
136137
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 \
137-
quay.io/centos-bootc/centos-bootc-dev:stream9 bootc install to-filesystem \
138+
${image} bootc install to-filesystem \
138139
--karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target
139140
ls -al /boot/loader/
140141
sudo grep foo=bar /boot/loader/entries/*.conf
@@ -143,5 +144,5 @@ jobs:
143144
sudo chattr -i /ostree/deploy/default/deploy/*
144145
sudo rm /ostree/deploy/default -rf
145146
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 \
146-
quay.io/centos-bootc/centos-bootc-dev:stream9 bootc install to-existing-root
147-
sudo ls -ldZ / /ostree/deploy/default/deploy/* /ostree/deploy/default/deploy/*/etc
147+
${image} bootc install to-existing-root
148+
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

lib/src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ pub(crate) enum TestingOpts {
150150
image: String,
151151
blockdev: Utf8PathBuf,
152152
},
153+
#[clap(name = "verify-selinux")]
154+
VerifySELinux {
155+
root: String,
156+
#[clap(long)]
157+
warn: bool,
158+
},
153159
}
154160

155161
/// Deploy and transactionally in-place with bootable container images.

lib/src/install.rs

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ pub(crate) mod baseline;
1010
pub(crate) mod config;
1111
pub(crate) mod osconfig;
1212

13-
use std::io::BufWriter;
1413
use std::io::Write;
1514
use std::os::fd::AsFd;
1615
use std::os::unix::process::CommandExt;
@@ -303,17 +302,17 @@ pub(crate) struct State {
303302
}
304303

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

@@ -510,13 +509,17 @@ async fn initialize_ostree_root_from_self(
510509
state: &State,
511510
root_setup: &RootSetup,
512511
) -> Result<InstallAleph> {
512+
let sepolicy = state.load_policy()?;
513+
let sepolicy = sepolicy.as_ref();
514+
515+
// Load a fd for the mounted target physical root
513516
let rootfs_dir = &root_setup.rootfs_fd;
514517
let rootfs = root_setup.rootfs.as_path();
515518
let cancellable = gio::Cancellable::NONE;
516519

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

521524
// TODO: make configurable?
522525
let stateroot = STATEROOT_DEFAULT;
@@ -529,7 +532,7 @@ async fn initialize_ostree_root_from_self(
529532
// And also label /boot AKA xbootldr, if it exists
530533
let bootdir = rootfs.join("boot");
531534
if bootdir.try_exists()? {
532-
state.lsm_label(&bootdir, "/boot".into(), false)?;
535+
crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?;
533536
}
534537

535538
// Default to avoiding grub2-mkconfig etc., but we need to use zipl on s390x.
@@ -557,8 +560,17 @@ async fn initialize_ostree_root_from_self(
557560
.cwd(rootfs_dir)?
558561
.run()?;
559562

560-
// Ensure everything in the ostree repo is labeled
561-
state.lsm_label(&rootfs.join("ostree"), "/usr".into(), true)?;
563+
// Bootstrap the initial labeling of the /ostree directory as usr_t
564+
if let Some(policy) = sepolicy {
565+
let ostree_dir = rootfs_dir.open_dir("ostree")?;
566+
crate::lsm::ensure_dir_labeled(
567+
&ostree_dir,
568+
".",
569+
Some("/usr".into()),
570+
0o755.into(),
571+
Some(policy),
572+
)?;
573+
}
562574

563575
let sysroot = ostree::Sysroot::new(Some(&gio::File::for_path(rootfs)));
564576
sysroot.load(cancellable)?;
@@ -620,8 +632,6 @@ async fn initialize_ostree_root_from_self(
620632
println!("Installed: {target_image}");
621633
println!(" Digest: {digest}");
622634

623-
// Write the entry for /boot to /etc/fstab. TODO: Encourage OSes to use the karg?
624-
// Or better bind this with the grub data.
625635
sysroot.load(cancellable)?;
626636
let deployment = sysroot
627637
.deployments()
@@ -633,28 +643,35 @@ async fn initialize_ostree_root_from_self(
633643
let root = rootfs_dir
634644
.open_dir(path.as_str())
635645
.context("Opening deployment dir")?;
636-
let root_path = &rootfs.join(&path.as_str());
637-
let mut f = {
638-
let mut opts = cap_std::fs::OpenOptions::new();
639-
root.open_with("etc/fstab", opts.append(true).write(true).create(true))
640-
.context("Opening etc/fstab")
641-
.map(BufWriter::new)?
642-
};
643-
if let Some(boot) = root_setup.boot.as_ref() {
644-
writeln!(f, "{}", boot.to_fstab())?;
646+
647+
// And do another recursive relabeling pass over the ostree-owned directories
648+
// but avoid recursing into the deployment root (because that's a *distinct*
649+
// logical root).
650+
if let Some(policy) = sepolicy {
651+
let deployment_root_meta = root.dir_metadata()?;
652+
let deployment_root_devino = (deployment_root_meta.dev(), deployment_root_meta.ino());
653+
for d in ["ostree", "boot"] {
654+
let mut pathbuf = Utf8PathBuf::from(d);
655+
crate::lsm::ensure_dir_labeled_recurse(
656+
rootfs_dir,
657+
&mut pathbuf,
658+
policy,
659+
Some(deployment_root_devino),
660+
)
661+
.with_context(|| format!("Recursive SELinux relabeling of {d}"))?;
662+
}
645663
}
646-
f.flush()?;
647664

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

651673
if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
652-
osconfig::inject_root_ssh_authorized_keys(
653-
&root,
654-
&root_path,
655-
|target, path, recurse| state.lsm_label(target, path, recurse),
656-
contents,
657-
)?;
674+
osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
658675
}
659676

660677
let uname = rustix::system::uname();

lib/src/install/baseline.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ pub(crate) fn install_create_rootfs(
195195
.transpose()
196196
.context("Parsing root size")?;
197197

198+
// Load the policy from the container root, which also must be our install root
199+
let sepolicy = state.load_policy()?;
200+
let sepolicy = sepolicy.as_ref();
201+
198202
// Create a temporary directory to use for mount points. Note that we're
199203
// in a mount namespace, so these should not be visible on the host.
200204
let rootfs = mntdir.join("rootfs");
@@ -368,15 +372,15 @@ pub(crate) fn install_create_rootfs(
368372
.collect::<Vec<_>>();
369373

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

381385
// Create the EFI system partition, if applicable
382386
if let Some(esp_partno) = esp_partno {

lib/src/install/osconfig.rs

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,51 @@
1+
use std::io::Write;
2+
13
use anyhow::Result;
2-
use camino::Utf8Path;
34
use cap_std::fs::Dir;
4-
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
5+
use cap_std_ext::cap_std;
56
use fn_error_context::context;
7+
use ostree_ext::ostree;
68

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

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

25-
let tmpfiles_dir = Utf8Path::new(ETC_TMPFILES);
26-
root.create_dir_all(tmpfiles_dir)?;
27-
let target = tmpfiles_dir.join(ROOT_SSH_TMPFILE);
28-
root.atomic_write(&target, &tmpfiles_content)?;
29-
30-
let as_path = Utf8Path::new(ETC_TMPFILES).join(ROOT_SSH_TMPFILE);
31-
lsm_label_fn(
32-
&root_path.join(&as_path),
33-
&Utf8Path::new("/").join(&as_path),
34-
false,
23+
crate::lsm::ensure_dir_labeled(root, ETC_TMPFILES, None, 0o755.into(), sepolicy)?;
24+
let tmpfiles_dir = root.open_dir(ETC_TMPFILES)?;
25+
crate::lsm::atomic_replace_labeled(
26+
&tmpfiles_dir,
27+
ROOT_SSH_TMPFILE,
28+
0o644.into(),
29+
sepolicy,
30+
|w| w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into),
3531
)?;
3632

37-
println!("Injected: {target}");
33+
println!("Injected: {ETC_TMPFILES}/{ROOT_SSH_TMPFILE}");
3834
Ok(())
3935
}
4036

4137
#[test]
4238
fn test_inject_root_ssh() -> Result<()> {
43-
use camino::Utf8PathBuf;
44-
use std::cell::Cell;
45-
46-
let fake_lsm_label_called = Cell::new(0);
47-
let fake_lsm_label = |target: &Utf8Path, as_path: &Utf8Path, recurse: bool| -> Result<()> {
48-
assert_eq!(
49-
target,
50-
format!("/root/path/etc/tmpfiles.d/{ROOT_SSH_TMPFILE}")
51-
);
52-
assert_eq!(as_path, format!("/etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"));
53-
assert_eq!(recurse, false);
54-
55-
fake_lsm_label_called.set(fake_lsm_label_called.get() + 1);
56-
Ok(())
57-
};
58-
59-
let root_path = &Utf8PathBuf::from("/root/path");
6039
let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
6140

62-
inject_root_ssh_authorized_keys(
63-
root,
64-
root_path,
65-
fake_lsm_label,
66-
"ssh-ed25519 ABCDE example@demo\n",
67-
)
68-
.unwrap();
41+
// The code expects this to exist, reasonably so
42+
root.create_dir("etc")?;
43+
inject_root_ssh_authorized_keys(root, None, "ssh-ed25519 ABCDE example@demo\n").unwrap();
6944

7045
let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
7146
assert_eq!(
7247
content,
7348
"f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
7449
);
75-
assert_eq!(fake_lsm_label_called, 1.into());
76-
7750
Ok(())
7851
}

0 commit comments

Comments
 (0)