From b20e568b1911387a9bd74b22638940cdd39ef77b Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Thu, 7 May 2026 11:09:42 +0100 Subject: [PATCH] fix(linux/build): run derivative-maker as unprivileged builder user (M1.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run #4251 advanced past checkout and into derivative-maker, then died immediately: ERROR: This must NOT be run as root (sudo)! ERROR: Exiting ./derivative-maker with non-zero exit code 1. Errors Detected: 0. Execution Time: 00:00:00. Kicksecure's derivative-maker explicitly refuses to run as root — it expects a regular user with passwordless sudo and uses sudo internally for the privileged operations (debootstrap, mksquashfs, chroot mounts). Our minimal debian-slim builder image had a `builder` user (uid 1000) but no sudo, no sudoers entry, and the container ran as root. Aligns with the upstream Kicksecure container pattern at linux/build/derivative-maker/docker/derivative-maker-docker-setup (uses USER=user with `${USER} ALL=(ALL) NOPASSWD:ALL`). Changes: - Dockerfile.builder: install `sudo` (and `fakeroot` while we're here — upstream sanity-tests pulls this in via apt at build time, but having it baked avoids a snapshot.debian.org round-trip every run); add passwordless sudoers entry for builder; correct the misleading comment that claimed root was needed. - New scripts/build-inner.sh: the inner derivative-maker invocation pulled out of build.sh's heredoc. Once we needed to drop privileges via runuser, the nested-heredoc / nested-quoting situation became unmaintainable; a regular script with normal quoting is far cleaner. - build.sh: inner heredoc now just chowns the workspace to builder and runuser's into build-inner.sh. ${REPO_ROOT} and ${BUILD_DIR} continue to be forwarded into the container via -e. - build.sh: BUILDER_IMAGE digest re-pinned to sha256:f8f0db37…1bedc (rebuilt and pushed natively on 10.0.0.51 — never on the WSL/aarch64 dev box, see reference_silvermetal_runner.md memory). Verified: bash -n on both scripts; image builds and pushes cleanly. Pushing this commit triggers a fresh CI run that will exercise it. Co-Authored-By: Claude Opus 4.7 (1M context) --- linux/build/docker/Dockerfile.builder | 17 +++++++--- linux/build/scripts/build-inner.sh | 48 +++++++++++++++++++++++++++ linux/build/scripts/build.sh | 33 +++++------------- 3 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 linux/build/scripts/build-inner.sh diff --git a/linux/build/docker/Dockerfile.builder b/linux/build/docker/Dockerfile.builder index 6bbecb8..8924cd4 100644 --- a/linux/build/docker/Dockerfile.builder +++ b/linux/build/docker/Dockerfile.builder @@ -53,6 +53,7 @@ RUN set -eux; \ debootstrap \ diffoscope-minimal \ dosfstools \ + fakeroot \ git \ gnupg \ isolinux \ @@ -61,15 +62,21 @@ RUN set -eux; \ reprepro \ rsync \ squashfs-tools \ + sudo \ syslinux-common \ xorriso; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* -# Non-root user for the parts of the build that don't need privilege. -# live-build itself still needs root inside the container for chroot/mount, -# so build.sh runs the container as root; this user exists for diagnostic -# tooling and matches uid 1000 to play nicely with bind mounts. -RUN useradd --uid 1000 --create-home --shell /bin/bash builder +# Non-root user for derivative-maker. +# Kicksecure's derivative-maker explicitly refuses to run as root and uses +# sudo internally for its privileged operations (debootstrap, mksquashfs, +# chroot mounts). build.sh chowns the workspace to this user inside the +# container, then runuser's to it before invoking derivative-maker. +# uid 1000 is conventional and plays nicely with bind mounts of files +# created by other Linux tools. +RUN useradd --uid 1000 --create-home --shell /bin/bash builder \ + && echo 'builder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/builder \ + && chmod 440 /etc/sudoers.d/builder WORKDIR /work diff --git a/linux/build/scripts/build-inner.sh b/linux/build/scripts/build-inner.sh new file mode 100644 index 0000000..3975f97 --- /dev/null +++ b/linux/build/scripts/build-inner.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# SilverMetal Linux — inner build step. +# +# Runs *inside* the silvermetal-builder container, as the unprivileged +# `builder` user. build.sh sets up the container, chowns the workspace, +# and runuser's into here. derivative-maker takes it from there and uses +# sudo internally for its privileged operations. +# +# Why this is its own file: +# The previous incarnation lived as a heredoc inside build.sh's docker +# run command. Once we needed to drop privileges from root to builder, +# the nested-heredoc / nested-quoting situation became unreadable; a +# plain script with normal quoting is far easier to maintain. +# +# Required env vars (set by build.sh and forwarded into the container): +# REPO_ROOT — absolute path to the SilverMetal repo root +# BUILD_DIR — where to drop the resulting *.iso and manifests +# SOURCE_DATE_EPOCH — reproducibility timestamp (forwarded to live-build) +# SNAPSHOT_TIMESTAMP — apt snapshot pin (forwarded to live-build) + +set -euo pipefail + +: "${REPO_ROOT:?REPO_ROOT must be set}" +: "${BUILD_DIR:?BUILD_DIR must be set}" + +# shellcheck disable=SC1091 +source "${REPO_ROOT}/linux/build/config/silvermetal-base.conf" + +cd "${REPO_ROOT}/linux/build/derivative-maker" + +./derivative-maker \ + --build \ + --target "${DERIVATIVE_BUILD_TARGET}" \ + --flavour "${DERIVATIVE_FLAVOUR}" \ + --arch "${DERIVATIVE_TARGET_ARCH}" \ + --dist "${DERIVATIVE_DIST}" \ + --config "${REPO_ROOT}/linux/build/config/silvermetal-base.conf" + +# derivative-maker writes into its own build/ tree; collect into BUILD_DIR. +# Exact upstream output paths can shift between tags — keep this tolerant. +# Anything matching *.iso under the tree is what we want. +find . -maxdepth 6 -type f -name "*.iso" -print0 \ + | xargs -0 -I{} cp -av "{}" "${BUILD_DIR}/" + +# Manifest of file metadata that lives inside the ISO. Useful when +# diagnosing reproducibility regressions without re-extracting. +find . -maxdepth 6 -type f -name "*.manifest" -print0 \ + | xargs -0 -I{} cp -av "{}" "${BUILD_DIR}/" 2>/dev/null || true diff --git a/linux/build/scripts/build.sh b/linux/build/scripts/build.sh index 686635d..4fa69c6 100755 --- a/linux/build/scripts/build.sh +++ b/linux/build/scripts/build.sh @@ -32,7 +32,7 @@ cd "${REPO_ROOT}" # outside the LAN — it's the entry that fleet-wide /etc/docker/daemon.json # registers as an insecure-registry. The host-style "docker-registry:5000" # is *not* DNS-resolvable; do not use it. -BUILDER_IMAGE="${BUILDER_IMAGE:-docker-registry.silverlabs.uk/silvermetal-builder@sha256:9e7161f9f180483f434074d7f32c27c907955232bd0c44efe6dc0ee1d9e56ae0}" +BUILDER_IMAGE="${BUILDER_IMAGE:-docker-registry.silverlabs.uk/silvermetal-builder@sha256:f8f0db3756df220d3de79371054fd43cf7f824ad27d9900328fef5723821bedc}" if [[ "${BUILDER_IMAGE}" != *"@sha256:"* ]]; then echo "build.sh: BUILDER_IMAGE must be pinned by digest, got: ${BUILDER_IMAGE}" >&2 @@ -121,30 +121,13 @@ docker run --rm --privileged \ -w "${REPO_ROOT}" \ "${BUILDER_IMAGE}" \ bash -euo pipefail -c ' - # shellcheck disable=SC1091 - source "${REPO_ROOT}/linux/build/config/silvermetal-base.conf" - - cd "${REPO_ROOT}/linux/build/derivative-maker" - - ./derivative-maker \ - --build \ - --target "${DERIVATIVE_BUILD_TARGET}" \ - --flavour "${DERIVATIVE_FLAVOUR}" \ - --arch "${DERIVATIVE_TARGET_ARCH}" \ - --dist "${DERIVATIVE_DIST}" \ - --config "${REPO_ROOT}/linux/build/config/silvermetal-base.conf" - - # derivative-maker writes into its own build/ dir; collect into - # BUILD_DIR. Exact upstream output paths can shift between tags — - # keep this tolerant. Anything matching *.iso under the tree is - # what we want. - find . -maxdepth 6 -type f -name "*.iso" -print0 \ - | xargs -0 -I{} cp -av "{}" "${BUILD_DIR}/" - - # Manifest of file metadata that lives inside the ISO. Useful when - # diagnosing reproducibility regressions without re-extracting. - find . -maxdepth 6 -type f -name "*.manifest" -print0 \ - | xargs -0 -I{} cp -av "{}" "${BUILD_DIR}/" 2>/dev/null || true + # derivative-maker refuses to run as root (it uses sudo internally + # for the privileged ops). Hand the workspace ownership to the + # unprivileged builder user (uid 1000, created in the Dockerfile + # with passwordless sudo), then drop privs and let build-inner.sh + # do the actual work. + chown -R builder:builder "${REPO_ROOT}" "${BUILD_DIR}" + runuser -u builder -- "${REPO_ROOT}/linux/build/scripts/build-inner.sh" ' || { echo "build.sh: derivative-maker failed"; exit 3; } # --- Hash artefacts ---------------------------------------------------------