Skip to content

Commit d039f26

Browse files
authored
Merge pull request #397 from cgwalters/more-labeling
Rework SELinux labeling more
2 parents 67a597b + 28e5bed commit d039f26

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;
@@ -301,17 +300,17 @@ pub(crate) struct State {
301300
}
302301

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

@@ -508,13 +507,17 @@ async fn initialize_ostree_root_from_self(
508507
state: &State,
509508
root_setup: &RootSetup,
510509
) -> Result<InstallAleph> {
510+
let sepolicy = state.load_policy()?;
511+
let sepolicy = sepolicy.as_ref();
512+
513+
// Load a fd for the mounted target physical root
511514
let rootfs_dir = &root_setup.rootfs_fd;
512515
let rootfs = root_setup.rootfs.as_path();
513516
let cancellable = gio::Cancellable::NONE;
514517

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

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

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

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

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

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

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

649671
if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
650-
osconfig::inject_root_ssh_authorized_keys(
651-
&root,
652-
&root_path,
653-
|target, path, recurse| state.lsm_label(target, path, recurse),
654-
contents,
655-
)?;
672+
osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
656673
}
657674

658675
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)