diff --git a/Dockerfile b/Dockerfile index ca821407..11ca9ab5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,18 +23,20 @@ FROM $base as buildroot ARG initramfs=1 # This installs our buildroot, and we want to cache it independently of the rest. # Basically we don't want changing a .rs file to blow out the cache of packages. -RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/install-buildroot +RUN --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + /run/packaging/install-buildroot # Now copy the rest of the source COPY --from=src /src /src WORKDIR /src # See https://www.reddit.com/r/rust/comments/126xeyx/exploring_the_problem_of_faster_cargo_docker/ # We aren't using the full recommendations there, just the simple bits. # First we download all of our Rust dependencies -RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch +RUN --mount=type=tmpfs,target=/run --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch FROM buildroot as sdboot-content # Writes to /out -RUN /src/contrib/packaging/configure-systemdboot download +RUN --mount=type=tmpfs,target=/run /src/contrib/packaging/configure-systemdboot download # We always do a "from scratch" build # https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ @@ -45,14 +47,16 @@ RUN /src/contrib/packaging/configure-systemdboot download # local sources. We'll override it later. # NOTE: All your base belong to me. FROM $base as target-base -RUN /usr/libexec/bootc-base-imagectl build-rootfs --manifest=standard /target-rootfs +RUN --mount=type=tmpfs,target=/run /usr/libexec/bootc-base-imagectl build-rootfs --manifest=standard /target-rootfs FROM scratch as base COPY --from=target-base /target-rootfs/ / -COPY --from=src /src/hack/ /run/hack/ # SKIP_CONFIGS=1 skips LBIs, test kargs, and install configs (for FCOS testing) ARG SKIP_CONFIGS -RUN cd /run/hack/ && SKIP_CONFIGS="${SKIP_CONFIGS}" ./provision-derived.sh +# Use tmpfs,target=/run with bind mounts inside to avoid leaking mount stubs into the image +RUN --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=src,src=/src/hack,target=/run/hack \ + cd /run/hack/ && SKIP_CONFIGS="${SKIP_CONFIGS}" ./provision-derived.sh # Note we don't do any customization here yet # Mark this as a test image LABEL bootc.testimage="1" @@ -79,13 +83,13 @@ ARG pkgversion ARG SOURCE_DATE_EPOCH ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} # Build RPM directly from source, using cached target directory -RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm +RUN --network=none --mount=type=tmpfs,target=/run --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 # The secureboot key and cert are passed via Justfile # We write the signed binary into /out -RUN --network=none \ - --mount=type=bind,from=sdboot-content,target=/run/sdboot-package \ +RUN --network=none --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=sdboot-content,src=/,target=/run/sdboot-package \ --mount=type=secret,id=secureboot_key \ --mount=type=secret,id=secureboot_cert \ /src/contrib/packaging/configure-systemdboot sign @@ -95,26 +99,34 @@ FROM build as units # A place that we're more likely to be able to set xattrs VOLUME /var/tmp ENV TMPDIR=/var/tmp -RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make install-unit-tests +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make install-unit-tests # This just does syntax checking FROM buildroot as validate -RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make validate +RUN --network=none --mount=type=tmpfs,target=/run --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 FROM base as final-common ARG variant -RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \ - --mount=type=bind,from=sdboot-content,target=/run/sdboot-content \ - --mount=type=bind,from=sdboot-signed,target=/run/sdboot-signed \ +RUN --network=none --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --mount=type=bind,from=sdboot-content,src=/,target=/run/sdboot-content \ + --mount=type=bind,from=sdboot-signed,src=/,target=/run/sdboot-signed \ /run/packaging/configure-variant "${variant}" ARG rootfs="" -RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}" +RUN --network=none --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + /run/packaging/configure-rootfs "${variant}" "${rootfs}" COPY --from=packaging /usr-extras/ /usr/ -# Final target: installs pre-built packages from /run/packages volume mount. -# Use with: podman build --target=final -v path/to/packages:/run/packages:ro +# Final target: installs pre-built packages from the 'packages' build context. +# Use with: podman build --target=final --build-context packages=path/to/packages +# We use --build-context instead of -v to avoid volume mount stubs leaking into /run. FROM final-common as final -RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \ +RUN --network=none --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --mount=type=bind,from=packages,src=/,target=/run/packages \ /run/packaging/install-rpm-and-setup /run/packages -RUN --network=none bootc container lint --fatal-warnings +# Use tmpfs on /run to hide any content created by podman for DNS resolution +# (e.g., /run/systemd/resolve/stub-resolv.conf on Ubuntu hosts) +RUN --network=none --mount=type=tmpfs,target=/run bootc container lint --fatal-warnings diff --git a/Dockerfile.cfsuki b/Dockerfile.cfsuki index fc673819..ec7fac00 100644 --- a/Dockerfile.cfsuki +++ b/Dockerfile.cfsuki @@ -3,7 +3,8 @@ ARG base=localhost/bootc FROM $base AS base FROM base as kernel -RUN < Result<()> { check_package_reproducibility(sh)?; - check_dockerfile_network_isolation(dockerfile_path)?; + check_dockerfile_rules(dockerfile_path)?; Ok(()) } @@ -61,24 +62,28 @@ fn check_package_reproducibility(sh: &Shell) -> Result<()> { 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..."); +/// Verify Dockerfile rules: +/// - All RUN instructions must include `--mount=type=tmpfs,target=/run` +/// - After cutoff, all RUN instructions must start with `--network=none` +#[context("Checking Dockerfile rules")] +fn check_dockerfile_rules(dockerfile_path: &Utf8Path) -> Result<()> { + println!("Checking Dockerfile rules..."); let dockerfile = std::fs::read_to_string(dockerfile_path).context("Reading Dockerfile")?; - verify_dockerfile_network_isolation(&dockerfile)?; - println!("ok Dockerfile network isolation"); + verify_dockerfile_rules(&dockerfile)?; + println!("ok Dockerfile rules"); Ok(()) } const RUN_NETWORK_NONE: &str = "RUN --network=none"; +const RUN_TMPFS: &str = "--mount=type=tmpfs,target=/run"; -/// Verify that all RUN instructions after the network cutoff marker start with -/// `RUN --network=none`. +/// Verify Dockerfile rules: +/// - All RUN instructions must include `--mount=type=tmpfs,target=/run` to prevent +/// podman's DNS resolver files from leaking into the image +/// - After the network cutoff, all RUN instructions must start with `--network=none` /// /// Returns Ok(()) if all RUN instructions comply, or an error listing violations. -pub fn verify_dockerfile_network_isolation(dockerfile: &str) -> Result<()> { +pub fn verify_dockerfile_rules(dockerfile: &str) -> Result<()> { // Find the cutoff point let cutoff_line = dockerfile .lines() @@ -90,19 +95,26 @@ pub fn verify_dockerfile_network_isolation(dockerfile: &str) -> Result<()> { ) })?; - // Check all RUN instructions after the cutoff point let mut errors = Vec::new(); - for (idx, line) in dockerfile.lines().enumerate().skip(cutoff_line + 1) { + for (idx, line) in dockerfile.lines().enumerate() { 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) { + // All RUN instructions must include tmpfs mount on /run + if !trimmed.contains(RUN_TMPFS) { errors.push(format!( - " line {}: RUN instruction must start with `{}`", + " line {}: RUN instruction must include `{}`", + line_num, RUN_TMPFS + )); + } + + // After cutoff, must start with exactly "RUN --network=none" + if idx > cutoff_line && !trimmed.starts_with(RUN_NETWORK_NONE) { + errors.push(format!( + " line {}: RUN instruction after cutoff must start with `{}`", line_num, RUN_NETWORK_NONE )); } @@ -111,9 +123,7 @@ pub fn verify_dockerfile_network_isolation(dockerfile: &str) -> Result<()> { if !errors.is_empty() { anyhow::bail!( - "Dockerfile has RUN instructions after '{}' that don't start with `{}`:\n{}", - DOCKERFILE_NETWORK_CUTOFF, - RUN_NETWORK_NONE, + "Dockerfile has invalid RUN instructions:\n{}", errors.join("\n") ); } @@ -126,40 +136,73 @@ mod tests { use super::*; #[test] - fn test_network_isolation_valid() { + fn test_dockerfile_rules_valid() { let dockerfile = r#" FROM base -RUN echo "before cutoff, no network restriction needed" +RUN --mount=type=tmpfs,target=/run echo "before cutoff, network allowed" # external dependency cutoff point -RUN --network=none echo "good" -RUN --network=none --mount=type=bind,from=foo,target=/bar some-command +RUN --network=none --mount=type=tmpfs,target=/run echo "good" +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=bind,from=foo,target=/bar some-command "#; - verify_dockerfile_network_isolation(dockerfile).unwrap(); + verify_dockerfile_rules(dockerfile).unwrap(); } #[test] - fn test_network_isolation_missing_flag() { + fn test_dockerfile_rules_missing_tmpfs_before_cutoff() { let dockerfile = r#" FROM base +RUN echo "bad - missing tmpfs" # external dependency cutoff point -RUN --network=none echo "good" -RUN echo "bad - missing network flag" +RUN --network=none --mount=type=tmpfs,target=/run echo "good" "#; - let err = verify_dockerfile_network_isolation(dockerfile).unwrap_err(); + let err = verify_dockerfile_rules(dockerfile).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("line 3"), "error should mention line 3: {msg}"); + assert!(msg.contains("tmpfs"), "error should mention tmpfs: {msg}"); + } + + #[test] + fn test_dockerfile_rules_missing_network_flag_after_cutoff() { + let dockerfile = r#" +FROM base +RUN --mount=type=tmpfs,target=/run echo "before cutoff" +# external dependency cutoff point +RUN --mount=type=tmpfs,target=/run echo "bad - missing network flag" +"#; + let err = verify_dockerfile_rules(dockerfile).unwrap_err(); let msg = err.to_string(); assert!(msg.contains("line 5"), "error should mention line 5: {msg}"); + assert!( + msg.contains("--network=none"), + "error should mention --network=none: {msg}" + ); } #[test] - fn test_network_isolation_wrong_position() { + fn test_dockerfile_rules_missing_tmpfs_after_cutoff() { + let dockerfile = r#" +FROM base +RUN --mount=type=tmpfs,target=/run echo "before cutoff" +# external dependency cutoff point +RUN --network=none echo "bad - missing tmpfs" +"#; + let err = verify_dockerfile_rules(dockerfile).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("line 5"), "error should mention line 5: {msg}"); + assert!(msg.contains("tmpfs"), "error should mention tmpfs: {msg}"); + } + + #[test] + fn test_dockerfile_rules_network_wrong_position() { // --network=none must come immediately after RUN let dockerfile = r#" FROM base +RUN --mount=type=tmpfs,target=/run echo "before cutoff" # external dependency cutoff point -RUN --mount=type=bind,from=foo,target=/bar --network=none echo "bad" +RUN --mount=type=tmpfs,target=/run --network=none echo "bad - network flag not first" "#; - let err = verify_dockerfile_network_isolation(dockerfile).unwrap_err(); + let err = verify_dockerfile_rules(dockerfile).unwrap_err(); let msg = err.to_string(); - assert!(msg.contains("line 4"), "error should mention line 4: {msg}"); + assert!(msg.contains("line 5"), "error should mention line 5: {msg}"); } }