diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ef05e0a4..fb2097abd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,10 +71,17 @@ jobs: run: sudo rm -f /bin/skopeo /usr/bin/skopeo - name: Free up disk space on runner run: sudo ./ci/clean-gha-runner.sh + - name: Enable fsverity for / + run: sudo tune2fs -O verity $(findmnt -vno SOURCE /) + - name: Install utils + run: sudo apt -y install fsverity - name: Integration tests run: | set -xeu + # Build images to test; TODO investigate doing single container builds + # via GHA and pushing to a temporary registry to share among workflows? sudo podman build -t localhost/bootc -f hack/Containerfile . + sudo podman build -t localhost/bootc-fsverity -f ci/Containerfile.install-fsverity export CARGO_INCREMENTAL=0 # because we aren't caching the test runner bits cargo build --release -p tests-integration df -h / @@ -83,8 +90,9 @@ jobs: df -h / # Nondestructive but privileged tests sudo bootc-integration-tests host-privileged localhost/bootc - # Finally the install-alongside suite + # Install tests sudo bootc-integration-tests install-alongside localhost/bootc + sudo bootc-integration-tests install-fsverity localhost/bootc-fsverity docs: if: ${{ contains(github.event.pull_request.labels.*.name, 'documentation') }} runs-on: ubuntu-latest diff --git a/ci/Containerfile.install-fsverity b/ci/Containerfile.install-fsverity new file mode 100644 index 000000000..7dc34f545 --- /dev/null +++ b/ci/Containerfile.install-fsverity @@ -0,0 +1,10 @@ +# Enable fsverity at install time +FROM localhost/bootc +RUN < /usr/lib/bootc/install/30-fsverity.toml < All .file objects have fsverity + /// "disabled" => No .file objects have fsverity + /// "inconsistent" => Mixed state + OstreeVerity +} + /// Hidden, internal only options #[derive(Debug, clap::Subcommand, PartialEq, Eq)] pub(crate) enum InternalsOpts { @@ -293,6 +303,8 @@ pub(crate) enum InternalsOpts { FixupEtcFstab, /// Should only be used by `make update-generated` PrintJsonSchema, + /// Perform consistency checking. + Fsck(FsckOpts), /// Perform cleanup actions Cleanup, /// Proxy frontend for the `ostree-ext` CLI. @@ -952,6 +964,9 @@ async fn run_from_opt(opt: Opt) -> Result<()> { ) .await } + InternalsOpts::Fsck(opts) => { + crate::fsck::fsck(storage).await + } InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root), InternalsOpts::PrintJsonSchema => { let schema = schema_for!(crate::spec::Host); diff --git a/lib/src/fsck.rs b/lib/src/fsck.rs new file mode 100644 index 000000000..4f2329bdf --- /dev/null +++ b/lib/src/fsck.rs @@ -0,0 +1,65 @@ +//! # Write deployments merging image with configmap +//! +//! Create a merged filesystem tree with the image and mounted configmaps. + +use std::collections::HashSet; +use std::io::{BufRead, Write}; +use std::os::fd::AsFd; + +use anyhow::Ok; +use anyhow::{anyhow, Context, Result}; +use camino::Utf8Path; +use cap_std::fs::{Dir, MetadataExt}; +use cap_std_ext::cap_std; +use cap_std_ext::dirext::CapStdExtDirExt; +use fn_error_context::context; +use ostree::{gio, glib}; +use ostree_container::OstreeImageReference; +use ostree_ext::container as ostree_container; +use ostree_ext::container::store::{ImportProgress, PrepareResult}; +use ostree_ext::oci_spec::image::{Descriptor, Digest}; +use ostree_ext::ostree::Deployment; +use ostree_ext::ostree::{self, Sysroot}; +use ostree_ext::sysroot::SysrootLock; +use ostree_ext::tokio_util::spawn_blocking_cancellable_flatten; + +use crate::spec::ImageReference; +use crate::spec::{BootOrder, HostSpec}; +use crate::status::labels_of_config; +use crate::store::Storage; +use crate::utils::async_task_with_spinner; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum VerityState { + Enabled, + Disabled, + Inconsistent +} + +pub(crate) struct FsckResult { + pub(crate) errors: bool, + pub(crate) verity: VerityState, +} + +#[context("Computing verity state")] +fn verity_state_of_objects(d: &Dir) -> Result { + for ent in d.entries()? { + let ent = ent?; + let name = ent.file_name(); + let Some(name) = name.to_str() else { + continue; + }; + let name = Utf8Path::new(name); + let Some("file") = name.extension() else { + continue + }; + let f = d.open(name).context(name)?; + let r = crate::fsverity::ioctl::fs_ioc_measure_verity(f.as_fd()); + } +} + +pub(crate) async fn fsck(storage: &Storage) -> Result { + let repo = &storage.repo(); + +} \ No newline at end of file diff --git a/lib/src/fsverity/ioctl.rs b/lib/src/fsverity/ioctl.rs index f4955af61..1c3463e53 100644 --- a/lib/src/fsverity/ioctl.rs +++ b/lib/src/fsverity/ioctl.rs @@ -1,6 +1,6 @@ use std::os::fd::AsFd; +use std::io::Result; -use anyhow::Result; use rustix::ioctl; use super::FsVerityHashValue; diff --git a/lib/src/install.rs b/lib/src/install.rs index 96ba952df..dfd3d2e33 100644 --- a/lib/src/install.rs +++ b/lib/src/install.rs @@ -33,6 +33,7 @@ use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::prelude::CapStdExtDirExt; use chrono::prelude::*; use clap::ValueEnum; +use config::Tristate; use fn_error_context::context; use ostree::gio; use ostree_ext::container as ostree_container; @@ -68,6 +69,15 @@ const SELINUXFS: &str = "/sys/fs/selinux"; const EFIVARFS: &str = "/sys/firmware/efi/efivars"; pub(crate) const ARCH_USES_EFI: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64")); +const DEFAULT_REPO_CONFIG: &[(&str, &str)] = &[ + // Default to avoiding grub2-mkconfig etc. + ("sysroot.bootloader", "none"), + // Always flip this one on because we need to support alongside installs + // to systems without a separate boot partition. + ("sysroot.bootprefix", "true"), + ("sysroot.readonly", "true"), +]; + /// Kernel argument used to specify we want the rootfs mounted read-write by default const RW_KARG: &str = "rw"; @@ -577,6 +587,7 @@ pub(crate) fn print_configuration() -> Result<()> { #[context("Creating ostree deployment")] async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result { + let install_config = state.install_config.as_ref(); let sepolicy = state.load_policy()?; let sepolicy = sepolicy.as_ref(); // Load a fd for the mounted target physical root @@ -602,14 +613,19 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result crate::lsm::ensure_dir_labeled(rootfs_dir, "boot", None, 0o755.into(), sepolicy)?; } - for (k, v) in [ - // Default to avoiding grub2-mkconfig etc. - ("sysroot.bootloader", "none"), - // Always flip this one on because we need to support alongside installs - // to systems without a separate boot partition. - ("sysroot.bootprefix", "true"), - ("sysroot.readonly", "true"), - ] { + let fsverity = install_config + .and_then(|c| c.fsverity.clone()) + .unwrap_or_default(); + let fsverity_ostree_key = "ex-integrity.fsverity"; + let fsverity_ostree_opt = match fsverity { + Tristate::Disabled => None, + Tristate::Optional => Some((fsverity_ostree_key, "maybe")), + Tristate::Enabled => Some((fsverity_ostree_key, "yes")), + }; + for (k, v) in DEFAULT_REPO_CONFIG + .iter() + .chain(fsverity_ostree_opt.as_ref()) + { Command::new("ostree") .args(["config", "--repo", "ostree/repo", "set", k, v]) .cwd_dir(rootfs_dir.try_clone()?) diff --git a/lib/src/install/baseline.rs b/lib/src/install/baseline.rs index f9b8795ca..5da7a36b4 100644 --- a/lib/src/install/baseline.rs +++ b/lib/src/install/baseline.rs @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; use super::MountSpec; use super::RootSetup; use super::State; +use super::Tristate; use super::RUN_BOOTC; use super::RW_KARG; use crate::mount; @@ -147,13 +148,12 @@ pub(crate) fn install_create_rootfs( state: &State, opts: InstallBlockDeviceOpts, ) -> Result { + let install_config = state.install_config.as_ref(); let luks_name = "root"; // Ensure we have a root filesystem upfront let root_filesystem = opts .filesystem - .or(state - .install_config - .as_ref() + .or(install_config .and_then(|c| c.filesystem_root()) .and_then(|r| r.fstype)) .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?; @@ -192,7 +192,7 @@ pub(crate) fn install_create_rootfs( } // Use the install configuration to find the block setup, if we have one - let block_setup = if let Some(config) = state.install_config.as_ref() { + let block_setup = if let Some(config) = install_config { config.get_block_setup(opts.block_setup.as_ref().copied())? } else if opts.filesystem.is_some() { // Otherwise, if a filesystem is specified then we default to whatever was @@ -370,8 +370,18 @@ pub(crate) fn install_create_rootfs( None }; + let fsverity = install_config + .and_then(|c| c.fsverity.clone()) + .unwrap_or_default(); + let mkfs_options = match (root_filesystem, fsverity) { + (Filesystem::Ext4, Tristate::Enabled | Tristate::Optional) => ["-O", "verity"].as_slice(), + _ => [].as_slice(), + } + .iter() + .copied(); + // Initialize rootfs - let root_uuid = mkfs(&rootdev, root_filesystem, "root", opts.wipe, [])?; + let root_uuid = mkfs(&rootdev, root_filesystem, "root", opts.wipe, mkfs_options)?; let rootarg = format!("root=UUID={root_uuid}"); let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}")); let bootarg = bootsrc.as_deref().map(|bootsrc| format!("boot={bootsrc}")); diff --git a/lib/src/install/config.rs b/lib/src/install/config.rs index b0fe3e76a..3d3ae6b6e 100644 --- a/lib/src/install/config.rs +++ b/lib/src/install/config.rs @@ -41,6 +41,18 @@ pub(crate) struct BasicFilesystems { // pub(crate) esp: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub(crate) enum Tristate { + #[default] + // The feature is disabled + Disabled, + // The feature is enabled if supported + Optional, + // The feature is enabled + Enabled, +} + /// The serialized [install] section #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename = "install", rename_all = "kebab-case", deny_unknown_fields)] @@ -50,6 +62,8 @@ pub(crate) struct InstallConfiguration { /// Enabled block storage configurations pub(crate) block: Option>, pub(crate) filesystem: Option, + /// How we should use fsverity. + pub(crate) fsverity: Option, /// Kernel arguments, applied at installation time #[serde(skip_serializing_if = "Option::is_none")] pub(crate) kargs: Option>, @@ -113,6 +127,7 @@ impl Mergeable for InstallConfiguration { { merge_basic(&mut self.root_fs_type, other.root_fs_type, env); merge_basic(&mut self.block, other.block, env); + merge_basic(&mut self.fsverity, other.fsverity, env); self.filesystem.merge(other.filesystem, env); if let Some(other_kargs) = other.kargs { self.kargs @@ -550,3 +565,29 @@ root-fs-type = "xfs" ) ); } + +#[test] +/// Test parsing fsverity +fn test_fsverity() { + let env = EnvProperties { + sys_arch: "aarch64".to_string(), + }; + let mut c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +root-fs-type = "xfs" +fsverity = "enabled" +"##, + ) + .unwrap(); + let install = c.install.as_ref().unwrap(); + assert_eq!(install.fsverity.as_ref().unwrap(), &Tristate::Enabled); + let o: InstallConfigurationToplevel = toml::from_str( + r##"[install] +fsverity = "optional" +"##, + ) + .unwrap(); + c.install.merge(o.install, &env); + let install = c.install.as_ref().unwrap(); + assert_eq!(install.fsverity.as_ref().unwrap(), &Tristate::Optional); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d8255499a..ca6d17969 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,10 +7,13 @@ mod boundimage; pub mod cli; pub(crate) mod deploy; +pub(crate) mod fsck; pub(crate) mod generator; mod image; pub(crate) mod journal; pub(crate) mod kargs; +#[allow(unsafe_code)] +pub(crate) mod fsverity; mod lints; mod lsm; pub(crate) mod metadata; diff --git a/tests-integration/src/install.rs b/tests-integration/src/install.rs index d7c046937..7495e3ee1 100644 --- a/tests-integration/src/install.rs +++ b/tests-integration/src/install.rs @@ -133,6 +133,7 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) "grep authorized_keys etc/tmpfiles.d/bootc-root-ssh.conf" ) .run()?; + drop(cwd); Ok(()) }, @@ -170,3 +171,4 @@ pub(crate) fn run_alongside(image: &str, mut testargs: libtest_mimic::Arguments) libtest_mimic::run(&testargs, tests.into()).exit() } +