Skip to content

Commit 084e5af

Browse files
authored
Merge pull request #182 from cgwalters/install-check-sigverify
install: Verify target image fetch by default
2 parents e362eaf + be35678 commit 084e5af

File tree

4 files changed

+88
-30
lines changed

4 files changed

+88
-30
lines changed

docs/install.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ The `--pid=host --security-opt label=type:unconfined_t` today
5959
make it more convenient for bootc to perform some privileged
6060
operations; in the future these requirement may be dropped.
6161

62+
### "day 2" updates, security and fetch configuration
63+
64+
Note that by default the `bootc install` path will find the pull specification used
65+
for the `podman run` invocation and use it to set up "day 2" OS updates that `bootc update`
66+
will use.
67+
68+
For example, if you invoke `podman run --privileged ... quay.io/examplecorp/exampleos:latest bootc install ...`
69+
then the installed operating system will fetch updates from `quay.io/examplecorp/exampleos:latest`.
70+
This can be overridden via `--target_imgref`; this is handy in cases like performing
71+
installation in a manufacturing environment from a mirrored registry.
72+
73+
By default, the installation process will verify that the container (representing the target OS)
74+
can fetch its own updates. A common cause of failure here is not changing the security settings
75+
in `/etc/containers/policy.json` in the target OS to verify signatures.
76+
77+
If you are pushing an unsigned image, you must specify `bootc install --target-no-signature-verification`.
78+
79+
Additionally note that to perform an install from an authenticated registry, you must also embed
80+
the pull secret into the image to pass this check. If you are fetching
81+
6282
### Operating system install configuration required
6383

6484
The container image must define its default install configuration. For example,

lib/src/install.rs

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ pub(crate) struct InstallTargetOpts {
7373
/// Enable verification via an ostree remote
7474
#[clap(long)]
7575
pub(crate) target_ostree_remote: Option<String>,
76+
77+
/// By default, the accessiblity of the target image will be verified (just the manifest will be fetched).
78+
/// Specifying this option suppresses the check; use this when you know the issues it might find
79+
/// are addressed.
80+
///
81+
/// Two main reasons this might fail:
82+
///
83+
/// - Forgetting `--target-no-signature-verification` if needed
84+
/// - Using a registry which requires authentication, but not embedding the pull secret in the image.
85+
#[clap(long)]
86+
#[serde(default)]
87+
pub(crate) skip_fetch_check: bool,
7688
}
7789

7890
#[derive(clap::Args, Debug, Clone, Serialize, Deserialize)]
@@ -200,7 +212,7 @@ pub(crate) struct State {
200212
pub(crate) setenforce_guard: Option<crate::lsm::SetEnforceGuard>,
201213
#[allow(dead_code)]
202214
pub(crate) config_opts: InstallConfigOpts,
203-
pub(crate) target_opts: InstallTargetOpts,
215+
pub(crate) target_imgref: ostree_container::OstreeImageReference,
204216
pub(crate) install_config: config::InstallConfiguration,
205217
}
206218

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

441-
// Parse the target CLI image reference options and create the *target* image
442-
// reference, which defaults to pulling from a registry.
443-
let target_sigverify = if opts.target_no_signature_verification {
444-
SignatureSource::ContainerPolicyAllowInsecure
445-
} else if let Some(remote) = opts.target_ostree_remote.as_deref() {
446-
SignatureSource::OstreeRemote(remote.to_string())
447-
} else {
448-
SignatureSource::ContainerPolicy
449-
};
450-
let target_imgname = opts
451-
.target_imgref
452-
.as_deref()
453-
.unwrap_or(state.source.imageref.name.as_str());
454-
let target_transport = ostree_container::Transport::try_from(opts.target_transport.as_str())?;
455-
let target_imgref = ostree_container::OstreeImageReference {
456-
sigverify: target_sigverify,
457-
imgref: ostree_container::ImageReference {
458-
transport: target_transport,
459-
name: target_imgname.to_string(),
460-
},
461-
};
462-
tracing::debug!("Target image reference: {target_imgref}");
463-
464452
// TODO: make configurable?
465453
let stateroot = STATEROOT_DEFAULT;
466454
Task::new_and_run(
@@ -535,12 +523,12 @@ async fn initialize_ostree_root_from_self(
535523
.collect::<Vec<_>>();
536524
let mut options = ostree_container::deploy::DeployOpts::default();
537525
options.kargs = Some(kargs.as_slice());
538-
options.target_imgref = Some(&target_imgref);
526+
options.target_imgref = Some(&state.target_imgref);
539527
options.proxy_cfg = Some(proxy_cfg);
540528
println!("Creating initial deployment");
529+
let target_image = state.target_imgref.to_string();
541530
let state =
542531
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
543-
let target_image = target_imgref.to_string();
544532
let digest = state.manifest_digest.as_str();
545533
println!("Installed: {target_image}");
546534
println!(" Digest: {digest}");
@@ -789,6 +777,28 @@ pub(crate) fn propagate_tmp_mounts_to_host() -> Result<()> {
789777
Ok(())
790778
}
791779

780+
/// Verify that we can load the manifest of the target image
781+
#[context("Verifying fetch")]
782+
async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) -> Result<()> {
783+
let tmpdir = tempfile::tempdir()?;
784+
let tmprepo = &ostree::Repo::new_for_path(tmpdir.path());
785+
tmprepo
786+
.create(ostree::RepoMode::Bare, ostree::gio::Cancellable::NONE)
787+
.context("Init tmp repo")?;
788+
789+
tracing::trace!("Verifying fetch for {imgref}");
790+
let mut imp =
791+
ostree_container::store::ImageImporter::new(&tmprepo, imgref, Default::default()).await?;
792+
use ostree_container::store::PrepareResult;
793+
let prep = match imp.prepare().await? {
794+
// SAFETY: It's impossible that the image was already fetched into this newly created temporary repository
795+
PrepareResult::AlreadyPresent(_) => unreachable!(),
796+
PrepareResult::Ready(r) => r,
797+
};
798+
tracing::debug!("Fetched manifest with digest {}", prep.manifest_digest);
799+
Ok(())
800+
}
801+
792802
/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
793803
async fn prepare_install(
794804
config_opts: InstallConfigOpts,
@@ -816,6 +826,34 @@ async fn prepare_install(
816826

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

829+
// Parse the target CLI image reference options and create the *target* image
830+
// reference, which defaults to pulling from a registry.
831+
let target_sigverify = if target_opts.target_no_signature_verification {
832+
SignatureSource::ContainerPolicyAllowInsecure
833+
} else if let Some(remote) = target_opts.target_ostree_remote.as_deref() {
834+
SignatureSource::OstreeRemote(remote.to_string())
835+
} else {
836+
SignatureSource::ContainerPolicy
837+
};
838+
let target_imgname = target_opts
839+
.target_imgref
840+
.as_deref()
841+
.unwrap_or(source.imageref.name.as_str());
842+
let target_transport =
843+
ostree_container::Transport::try_from(target_opts.target_transport.as_str())?;
844+
let target_imgref = ostree_container::OstreeImageReference {
845+
sigverify: target_sigverify,
846+
imgref: ostree_container::ImageReference {
847+
transport: target_transport,
848+
name: target_imgname.to_string(),
849+
},
850+
};
851+
tracing::debug!("Target image reference: {target_imgref}");
852+
853+
if !target_opts.skip_fetch_check {
854+
verify_target_fetch(&target_imgref).await?;
855+
}
856+
819857
ensure_var()?;
820858
propagate_tmp_mounts_to_host()?;
821859

@@ -841,7 +879,7 @@ async fn prepare_install(
841879
setenforce_guard,
842880
source,
843881
config_opts,
844-
target_opts,
882+
target_imgref,
845883
install_config,
846884
});
847885

lib/src/privtests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ fn test_install_filesystem(image: &str, blockdev: &Utf8Path) -> Result<()> {
152152
let mountpoint: &Utf8Path = mountpoint_dir.path().try_into().unwrap();
153153

154154
// And run the install
155-
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()?;
155+
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()?;
156156

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

tests/kolainst/install

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ cd $(mktemp -d)
1919

2020
case "${AUTOPKGTEST_REBOOT_MARK:-}" in
2121
"")
22-
podman run --rm -ti --privileged --pid=host -v /usr/bin/bootc:/usr/bin/bootc ${IMAGE} bootc install --karg=foo=bar ${DEV}
22+
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}
2323
# In theory we could e.g. wipe the bootloader setup on the primary disk, then reboot;
2424
# but for now let's just sanity test that the install command executes.
2525
lsblk ${DEV}

0 commit comments

Comments
 (0)