fix(linux/build): run derivative-maker as unprivileged builder user (M1.1)
Some checks failed
Build SilverMetal Linux ISO (reproducibility-gated) / build-and-verify (push) Failing after 1m14s

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 11:09:42 +01:00
parent 1d0e58739c
commit b20e568b19
3 changed files with 68 additions and 30 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 ---------------------------------------------------------