Run #4267 finally got the bind mount through (Merged Binds includes /root/.docker:/root/.docker:ro), but docker build then died: failed to update builder last activity time: open /root/.docker/buildx/activity/.tmp-...: read-only file system The catthehacker job container uses buildx, which writes activity tracking to /root/.docker/buildx/. Mounting the whole host /root/.docker read-only made that path read-only too. Right scope is the file, not the dir: -v /root/.docker/config.json:/root/.docker/config.json:ro That gives the cli the registry auth it needs while leaving the rest of /root/.docker on the container's writable overlay so buildx can populate its own activity dir without colliding with the host's. Also matches the principle of mounting the minimum the secret requires. valid_volumes entry updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SilverMetal Linux — reproducible ISO build pipeline
Milestone: Phase 1 / M1.1 — Kicksecure fork builds reproducibly. Exit criterion: two clean builds of the same commit produce a byte-identical SHA256.
This directory holds everything that turns a SilverMetal commit into a SilverMetal Linux ISO. M1.1 ships only the base (un-hardened) Kicksecure derivative. Hardening overlay, kernel swap, AppArmor profiles, etc. land in M1.2+ and must not be added here in the M1.1 PR.
Layout
linux/build/
├── README.md (this file)
├── derivative-maker/ git submodule -> Kicksecure/derivative-maker
├── config/
│ ├── silvermetal-base.conf derivative selection + branding
│ ├── snapshot-pin.env pinned snapshot.debian.org timestamp
│ └── source-date-epoch.env optional SOURCE_DATE_EPOCH override
├── docker/
│ └── Dockerfile.builder pinned debian:bookworm-slim builder image
└── scripts/
├── build.sh wrapper: container run -> derivative-maker
├── verify-reproducibility.sh build twice, compare SHA256
└── diagnose-divergence.sh diffoscope on mismatch
How reproducibility is achieved
The same levers any deterministic Debian build relies on, stacked together:
| Lever | Where it lives |
|---|---|
Pinned snapshot.debian.org mirror |
config/snapshot-pin.env |
SOURCE_DATE_EPOCH from commit time |
scripts/build.sh (auto) |
| Pinned builder image (by digest) | docker/Dockerfile.builder + BUILDER_IMAGE |
Deterministic mksquashfs flags |
MKSQUASHFS_OPTIONS in base conf |
| Pinned upstream toolchain | derivative-maker/ submodule |
LC_ALL=C.UTF-8, TZ=UTC |
scripts/build.sh |
diffoscope is the diagnostic tool used by diagnose-divergence.sh; the gate itself is plain sha256sum.
Reproduce a release locally
Procedure mirrors
docs/trust-model.md§ Reproducible builds.
Prerequisites: a Linux host (or WSL2) with Docker, ~30 GB free disk, ~8 GB RAM.
# 1. Clone the repo at the release tag.
git clone --recurse-submodules https://git.silverlabs.uk/SilverLABS/SilverMetal.git
cd SilverMetal
git checkout v1.1.0 # whichever release you want to verify
# 2. Build twice and compare. ~60-90 minutes per build.
linux/build/scripts/verify-reproducibility.sh
# 3. Compare against the published release.
sha256sum -c <(curl -fsSL https://git.silverlabs.uk/SilverLABS/SilverMetal/releases/download/v1.1.0/SHA256SUMS)
Mismatch with the published artefact = supply-chain anomaly. Report channel: security@silverlabs.uk.
Build once (no reproducibility check)
linux/build/scripts/build.sh
# Output lands in linux/build/output/<short-sha>/
The wrapper requires BUILDER_IMAGE to be pinned by digest. Local dev that hasn't built and pushed an image yet should override:
BUILDER_IMAGE=docker-registry:5000/silvermetal-builder@sha256:<digest> \
linux/build/scripts/build.sh
Gitea Actions
The CI workflow (.gitea/workflows/build-iso-linux.yaml) is the authority for "did this commit build reproducibly?". It:
- Checks out the commit with submodules.
- Runs
build.shtwice in${GITHUB_WORKSPACE}/build-{a,b}. - Fails the run if the two ISO SHA256s differ, and uploads a
diffoscopereport as an artefact. - On a tag push, attaches the verified ISO +
SHA256SUMS+BUILD_INFOto a Gitea release.
Self-hosted runner setup
The workflow runs on runs-on: silvermetal-builder, a self-hosted, privileged-capable Gitea Actions runner. Create it before merging the workflow:
- Provision a Debian 12 VM on the cluster with ≥ 8 vCPU, ≥ 16 GB RAM, ≥ 100 GB disk.
- Install Docker (
apt install docker.io); ensure the runner user can rundocker run --privileged. - Register
act_runneragainstgit.silverlabs.ukwith labelsilvermetal-builder. - Pre-pull the builder image so the first reproducibility run isn't a cold start:
docker pull docker-registry:5000/silvermetal-builder:latest - Cache the apt snapshot in a Docker volume to avoid throttling:
docker volume create silvermetal-apt-cache
The runner host name must not leak into ISO content. LC_ALL=C.UTF-8 and a constant TZ in the wrapper guard against that, but spot-check with diagnose-divergence.sh.
Bumping pinned inputs
Each of these is a deliberate, reviewed action — never automate:
derivative-makersubmodule — bump in its own PR, with a verification log showing two clean builds match.snapshot-pin.env— same procedure.- Builder image (
Dockerfile.builder) — edit and commit. CI'sbuilder-imagejob rebuilds, pushes, and feeds the new digest tobuild-and-verifyautomatically; no manualdocker build/docker pushstep. The hardcodedBUILDER_IMAGEdigest fallback inbuild.shis for local/offline rebuilds only — bump it opportunistically after any merged Dockerfile change so non-CIbuild.shkeeps working at that commit.
What this milestone is not
- No hardening overlay (M1.2)
- No SilverBrowser/SilverVPN/SilverSync/SilverChat integration (M1.6–1.9)
- No installer branding (M1.5)
- No update server (M1.10)
- No SBOM publication (M1.11)
- No signing ceremony / MOK / Secure Boot wiring (separate milestone)
If a change to this directory expands its scope into one of those, push back — the M1.1 gate is intentionally narrow.