1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 06:45:13 +01:00

build-sys: Consistently use RUN --network=none and add check

Ensure all RUN instructions after the "external dependency cutoff point"
marker include `--network=none` right after `RUN`.
This enforces that external dependencies are clearly delineated in the early stages of the Dockerfile.

The check is part of `cargo xtask check-buildsys` and includes unit tests.

Assisted-by: OpenCode (Sonnet 4)
Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Colin Walters
2025-12-17 16:36:30 -05:00
parent fab93ac4e3
commit ea0df4fcf9
3 changed files with 177 additions and 53 deletions

View File

@@ -65,8 +65,12 @@ ENV container=oci
STOPSIGNAL SIGRTMIN+3 STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"] CMD ["/sbin/init"]
# -------------
# external dependency cutoff point:
# NOTE: Every RUN instruction past this point should use `--network=none`; we want to ensure # NOTE: Every RUN instruction past this point should use `--network=none`; we want to ensure
# all external dependencies are clearly delineated. # all external dependencies are clearly delineated.
# This is verified in `cargo xtask check-buildsys`.
# -------------
FROM buildroot as build FROM buildroot as build
# Version for RPM build (optional, computed from git in Justfile) # Version for RPM build (optional, computed from git in Justfile)
@@ -75,7 +79,7 @@ ARG pkgversion
ARG SOURCE_DATE_EPOCH ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
# Build RPM directly from source, using cached target directory # Build RPM directly from source, using cached target directory
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm
FROM buildroot as sdboot-signed FROM buildroot as sdboot-signed
# The secureboot key and cert are passed via Justfile # The secureboot key and cert are passed via Justfile
@@ -91,11 +95,11 @@ FROM build as units
# A place that we're more likely to be able to set xattrs # A place that we're more likely to be able to set xattrs
VOLUME /var/tmp VOLUME /var/tmp
ENV TMPDIR=/var/tmp ENV TMPDIR=/var/tmp
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none make install-unit-tests RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make install-unit-tests
# This just does syntax checking # This just does syntax checking
FROM buildroot as validate FROM buildroot as validate
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none make validate RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make validate
# Common base for final images: configures variant, rootfs, and injects extra content # Common base for final images: configures variant, rootfs, and injects extra content
FROM base as final-common FROM base as final-common
@@ -105,13 +109,12 @@ RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \
--mount=type=bind,from=sdboot-signed,target=/run/sdboot-signed \ --mount=type=bind,from=sdboot-signed,target=/run/sdboot-signed \
/run/packaging/configure-variant "${variant}" /run/packaging/configure-variant "${variant}"
ARG rootfs="" ARG rootfs=""
RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}" RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}"
COPY --from=packaging /usr-extras/ /usr/ COPY --from=packaging /usr-extras/ /usr/
# Final target: installs pre-built packages from /run/packages volume mount. # Final target: installs pre-built packages from /run/packages volume mount.
# Use with: podman build --target=final -v path/to/packages:/run/packages:ro # Use with: podman build --target=final -v path/to/packages:/run/packages:ro
FROM final-common as final FROM final-common as final
RUN --mount=type=bind,from=packaging,target=/run/packaging \ RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \
--network=none \
/run/packaging/install-rpm-and-setup /run/packages /run/packaging/install-rpm-and-setup /run/packages
RUN bootc container lint --fatal-warnings RUN --network=none bootc container lint --fatal-warnings

View File

@@ -0,0 +1,165 @@
//! Build system validation checks.
use std::collections::BTreeMap;
use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use fn_error_context::context;
use xshell::{cmd, Shell};
const DOCKERFILE_NETWORK_CUTOFF: &str = "external dependency cutoff point";
/// Check build system properties
///
/// - Reproducible builds for the RPM
/// - Dockerfile network isolation after cutoff point
#[context("Checking build system")]
pub fn check_buildsys(sh: &Shell, dockerfile_path: &Utf8Path) -> Result<()> {
check_package_reproducibility(sh)?;
check_dockerfile_network_isolation(dockerfile_path)?;
Ok(())
}
/// Verify that consecutive `just package` invocations produce identical RPM checksums.
#[context("Checking package reproducibility")]
fn check_package_reproducibility(sh: &Shell) -> Result<()> {
println!("Checking reproducible builds...");
// Helper to compute SHA256 of bootc RPMs in target/packages/
fn get_rpm_checksums(sh: &Shell) -> Result<BTreeMap<String, String>> {
// Find bootc*.rpm files in target/packages/
let packages_dir = Utf8Path::new("target/packages");
let mut rpm_files: Vec<Utf8PathBuf> = Vec::new();
for entry in std::fs::read_dir(packages_dir).context("Reading target/packages")? {
let entry = entry?;
let path = Utf8PathBuf::try_from(entry.path())?;
if path.extension() == Some("rpm") {
rpm_files.push(path);
}
}
assert!(!rpm_files.is_empty());
let mut checksums = BTreeMap::new();
for rpm_path in &rpm_files {
let output = cmd!(sh, "sha256sum {rpm_path}").read()?;
let (hash, filename) = output
.split_once(" ")
.with_context(|| format!("failed to parse sha256sum output: '{}'", output))?;
checksums.insert(filename.to_owned(), hash.to_owned());
}
Ok(checksums)
}
cmd!(sh, "just package").run()?;
let first_checksums = get_rpm_checksums(sh)?;
cmd!(sh, "just package").run()?;
let second_checksums = get_rpm_checksums(sh)?;
itertools::assert_equal(first_checksums, second_checksums);
println!("ok package reproducibility");
Ok(())
}
/// Verify that all RUN instructions in the Dockerfile after the network cutoff
/// point include `--network=none`.
#[context("Checking Dockerfile network isolation")]
fn check_dockerfile_network_isolation(dockerfile_path: &Utf8Path) -> Result<()> {
println!("Checking Dockerfile network isolation...");
let dockerfile = std::fs::read_to_string(dockerfile_path).context("Reading Dockerfile")?;
verify_dockerfile_network_isolation(&dockerfile)?;
println!("ok Dockerfile network isolation");
Ok(())
}
const RUN_NETWORK_NONE: &str = "RUN --network=none";
/// Verify that all RUN instructions after the network cutoff marker start with
/// `RUN --network=none`.
///
/// Returns Ok(()) if all RUN instructions comply, or an error listing violations.
pub fn verify_dockerfile_network_isolation(dockerfile: &str) -> Result<()> {
// Find the cutoff point
let cutoff_line = dockerfile
.lines()
.position(|line| line.contains(DOCKERFILE_NETWORK_CUTOFF))
.ok_or_else(|| {
anyhow::anyhow!(
"Dockerfile missing '{}' marker comment",
DOCKERFILE_NETWORK_CUTOFF
)
})?;
// Check all RUN instructions after the cutoff point
let mut errors = Vec::new();
for (idx, line) in dockerfile.lines().enumerate().skip(cutoff_line + 1) {
let line_num = idx + 1; // 1-based line numbers
let trimmed = line.trim();
// Check if this is a RUN instruction
if trimmed.starts_with("RUN ") {
// Must start with exactly "RUN --network=none"
if !trimmed.starts_with(RUN_NETWORK_NONE) {
errors.push(format!(
" line {}: RUN instruction must start with `{}`",
line_num, RUN_NETWORK_NONE
));
}
}
}
if !errors.is_empty() {
anyhow::bail!(
"Dockerfile has RUN instructions after '{}' that don't start with `{}`:\n{}",
DOCKERFILE_NETWORK_CUTOFF,
RUN_NETWORK_NONE,
errors.join("\n")
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_isolation_valid() {
let dockerfile = r#"
FROM base
RUN echo "before cutoff, no network restriction needed"
# external dependency cutoff point
RUN --network=none echo "good"
RUN --network=none --mount=type=bind,from=foo,target=/bar some-command
"#;
verify_dockerfile_network_isolation(dockerfile).unwrap();
}
#[test]
fn test_network_isolation_missing_flag() {
let dockerfile = r#"
FROM base
# external dependency cutoff point
RUN --network=none echo "good"
RUN echo "bad - missing network flag"
"#;
let err = verify_dockerfile_network_isolation(dockerfile).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("line 5"), "error should mention line 5: {msg}");
}
#[test]
fn test_network_isolation_wrong_position() {
// --network=none must come immediately after RUN
let dockerfile = r#"
FROM base
# external dependency cutoff point
RUN --mount=type=bind,from=foo,target=/bar --network=none echo "bad"
"#;
let err = verify_dockerfile_network_isolation(dockerfile).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("line 4"), "error should mention line 4: {msg}");
}
}

View File

@@ -14,6 +14,7 @@ use clap::{Args, Parser, Subcommand};
use fn_error_context::context; use fn_error_context::context;
use xshell::{cmd, Shell}; use xshell::{cmd, Shell};
mod buildsys;
mod man; mod man;
mod tmt; mod tmt;
@@ -137,7 +138,7 @@ fn try_main() -> Result<()> {
Commands::Spec => spec(&sh), Commands::Spec => spec(&sh),
Commands::RunTmt(args) => tmt::run_tmt(&sh, &args), Commands::RunTmt(args) => tmt::run_tmt(&sh, &args),
Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args), Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args),
Commands::CheckBuildsys => check_buildsys(&sh), Commands::CheckBuildsys => buildsys::check_buildsys(&sh, "Dockerfile".into()),
} }
} }
@@ -405,48 +406,3 @@ fn update_generated(sh: &Shell) -> Result<()> {
Ok(()) Ok(())
} }
/// Check build system properties
///
/// - Reproducible builds for the RPM
#[context("Checking build system")]
fn check_buildsys(sh: &Shell) -> Result<()> {
use std::collections::BTreeMap;
println!("Checking reproducible builds...");
// Helper to compute SHA256 of bootc RPMs in target/packages/
fn get_rpm_checksums(sh: &Shell) -> Result<BTreeMap<String, String>> {
// Find bootc*.rpm files in target/packages/
let packages_dir = Utf8Path::new("target/packages");
let mut rpm_files: Vec<Utf8PathBuf> = Vec::new();
for entry in std::fs::read_dir(packages_dir).context("Reading target/packages")? {
let entry = entry?;
let path = Utf8PathBuf::try_from(entry.path())?;
if path.extension() == Some("rpm") {
rpm_files.push(path);
}
}
assert!(!rpm_files.is_empty());
let mut checksums = BTreeMap::new();
for rpm_path in &rpm_files {
let output = cmd!(sh, "sha256sum {rpm_path}").read()?;
let (hash, filename) = output
.split_once(" ")
.with_context(|| format!("failed to parse sha256sum output: '{}'", output))?;
checksums.insert(filename.to_owned(), hash.to_owned());
}
Ok(checksums)
}
cmd!(sh, "just package").run()?;
let first_checksums = get_rpm_checksums(sh)?;
cmd!(sh, "just package").run()?;
let second_checksums = get_rpm_checksums(sh)?;
itertools::assert_equal(first_checksums, second_checksums);
println!("ok package reproducibility");
Ok(())
}