Skip to content

Commit

Permalink
Merge pull request #182 from cgwalters/install-check-sigverify
Browse files Browse the repository at this point in the history
install: Verify target image fetch by default
  • Loading branch information
jmarrero authored Nov 6, 2023
2 parents e362eaf + be35678 commit 084e5af
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 30 deletions.
20 changes: 20 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ The `--pid=host --security-opt label=type:unconfined_t` today
make it more convenient for bootc to perform some privileged
operations; in the future these requirement may be dropped.

### "day 2" updates, security and fetch configuration

Note that by default the `bootc install` path will find the pull specification used
for the `podman run` invocation and use it to set up "day 2" OS updates that `bootc update`
will use.

For example, if you invoke `podman run --privileged ... quay.io/examplecorp/exampleos:latest bootc install ...`
then the installed operating system will fetch updates from `quay.io/examplecorp/exampleos:latest`.
This can be overridden via `--target_imgref`; this is handy in cases like performing
installation in a manufacturing environment from a mirrored registry.

By default, the installation process will verify that the container (representing the target OS)
can fetch its own updates. A common cause of failure here is not changing the security settings
in `/etc/containers/policy.json` in the target OS to verify signatures.

If you are pushing an unsigned image, you must specify `bootc install --target-no-signature-verification`.

Additionally note that to perform an install from an authenticated registry, you must also embed
the pull secret into the image to pass this check. If you are fetching

### Operating system install configuration required

The container image must define its default install configuration. For example,
Expand Down
94 changes: 66 additions & 28 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ pub(crate) struct InstallTargetOpts {
/// Enable verification via an ostree remote
#[clap(long)]
pub(crate) target_ostree_remote: Option<String>,

/// By default, the accessiblity of the target image will be verified (just the manifest will be fetched).
/// Specifying this option suppresses the check; use this when you know the issues it might find
/// are addressed.
///
/// Two main reasons this might fail:
///
/// - Forgetting `--target-no-signature-verification` if needed
/// - Using a registry which requires authentication, but not embedding the pull secret in the image.
#[clap(long)]
#[serde(default)]
pub(crate) skip_fetch_check: bool,
}

#[derive(clap::Args, Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -200,7 +212,7 @@ pub(crate) struct State {
pub(crate) setenforce_guard: Option<crate::lsm::SetEnforceGuard>,
#[allow(dead_code)]
pub(crate) config_opts: InstallConfigOpts,
pub(crate) target_opts: InstallTargetOpts,
pub(crate) target_imgref: ostree_container::OstreeImageReference,
pub(crate) install_config: config::InstallConfiguration,
}

Expand Down Expand Up @@ -435,32 +447,8 @@ async fn initialize_ostree_root_from_self(
) -> Result<InstallAleph> {
let rootfs_dir = &root_setup.rootfs_fd;
let rootfs = root_setup.rootfs.as_path();
let opts = &state.target_opts;
let cancellable = gio::Cancellable::NONE;

// Parse the target CLI image reference options and create the *target* image
// reference, which defaults to pulling from a registry.
let target_sigverify = if opts.target_no_signature_verification {
SignatureSource::ContainerPolicyAllowInsecure
} else if let Some(remote) = opts.target_ostree_remote.as_deref() {
SignatureSource::OstreeRemote(remote.to_string())
} else {
SignatureSource::ContainerPolicy
};
let target_imgname = opts
.target_imgref
.as_deref()
.unwrap_or(state.source.imageref.name.as_str());
let target_transport = ostree_container::Transport::try_from(opts.target_transport.as_str())?;
let target_imgref = ostree_container::OstreeImageReference {
sigverify: target_sigverify,
imgref: ostree_container::ImageReference {
transport: target_transport,
name: target_imgname.to_string(),
},
};
tracing::debug!("Target image reference: {target_imgref}");

// TODO: make configurable?
let stateroot = STATEROOT_DEFAULT;
Task::new_and_run(
Expand Down Expand Up @@ -535,12 +523,12 @@ async fn initialize_ostree_root_from_self(
.collect::<Vec<_>>();
let mut options = ostree_container::deploy::DeployOpts::default();
options.kargs = Some(kargs.as_slice());
options.target_imgref = Some(&target_imgref);
options.target_imgref = Some(&state.target_imgref);
options.proxy_cfg = Some(proxy_cfg);
println!("Creating initial deployment");
let target_image = state.target_imgref.to_string();
let state =
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
let target_image = target_imgref.to_string();
let digest = state.manifest_digest.as_str();
println!("Installed: {target_image}");
println!(" Digest: {digest}");
Expand Down Expand Up @@ -789,6 +777,28 @@ pub(crate) fn propagate_tmp_mounts_to_host() -> Result<()> {
Ok(())
}

/// Verify that we can load the manifest of the target image
#[context("Verifying fetch")]
async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) -> Result<()> {
let tmpdir = tempfile::tempdir()?;
let tmprepo = &ostree::Repo::new_for_path(tmpdir.path());
tmprepo
.create(ostree::RepoMode::Bare, ostree::gio::Cancellable::NONE)
.context("Init tmp repo")?;

tracing::trace!("Verifying fetch for {imgref}");
let mut imp =
ostree_container::store::ImageImporter::new(&tmprepo, imgref, Default::default()).await?;
use ostree_container::store::PrepareResult;
let prep = match imp.prepare().await? {
// SAFETY: It's impossible that the image was already fetched into this newly created temporary repository
PrepareResult::AlreadyPresent(_) => unreachable!(),
PrepareResult::Ready(r) => r,
};
tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
Ok(())
}

/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
async fn prepare_install(
config_opts: InstallConfigOpts,
Expand Down Expand Up @@ -816,6 +826,34 @@ async fn prepare_install(

let source = SourceInfo::from_container(&container_info)?;

// Parse the target CLI image reference options and create the *target* image
// reference, which defaults to pulling from a registry.
let target_sigverify = if target_opts.target_no_signature_verification {
SignatureSource::ContainerPolicyAllowInsecure
} else if let Some(remote) = target_opts.target_ostree_remote.as_deref() {
SignatureSource::OstreeRemote(remote.to_string())
} else {
SignatureSource::ContainerPolicy
};
let target_imgname = target_opts
.target_imgref
.as_deref()
.unwrap_or(source.imageref.name.as_str());
let target_transport =
ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
let target_imgref = ostree_container::OstreeImageReference {
sigverify: target_sigverify,
imgref: ostree_container::ImageReference {
transport: target_transport,
name: target_imgname.to_string(),
},
};
tracing::debug!("Target image reference: {target_imgref}");

if !target_opts.skip_fetch_check {
verify_target_fetch(&target_imgref).await?;
}

ensure_var()?;
propagate_tmp_mounts_to_host()?;

Expand All @@ -841,7 +879,7 @@ async fn prepare_install(
setenforce_guard,
source,
config_opts,
target_opts,
target_imgref,
install_config,
});

Expand Down
2 changes: 1 addition & 1 deletion lib/src/privtests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ fn test_install_filesystem(image: &str, blockdev: &Utf8Path) -> Result<()> {
let mountpoint: &Utf8Path = mountpoint_dir.path().try_into().unwrap();

// And run the install
cmd!(sh, "podman run --rm --privileged --pid=host --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v {mountpoint}:/target-root {image} bootc install-to-filesystem /target-root").run()?;
cmd!(sh, "podman run --rm --privileged --pid=host --env=RUST_LOG -v /usr/bin/bootc:/usr/bin/bootc -v {mountpoint}:/target-root {image} bootc install-to-filesystem --target-no-signature-verification /target-root").run()?;

cmd!(sh, "umount -R {mountpoint}").run()?;

Expand Down
2 changes: 1 addition & 1 deletion tests/kolainst/install
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cd $(mktemp -d)

case "${AUTOPKGTEST_REBOOT_MARK:-}" in
"")
podman run --rm -ti --privileged --pid=host -v /usr/bin/bootc:/usr/bin/bootc ${IMAGE} bootc install --karg=foo=bar ${DEV}
podman run --rm -ti --privileged --pid=host -v /usr/bin/bootc:/usr/bin/bootc ${IMAGE} bootc install --target-no-signature-verification --karg=foo=bar ${DEV}
# In theory we could e.g. wipe the bootloader setup on the primary disk, then reboot;
# but for now let's just sanity test that the install command executes.
lsblk ${DEV}
Expand Down

0 comments on commit 084e5af

Please sign in to comment.