diff --git a/images/golden/Dockerfile b/images/golden/Dockerfile index 6b15a77..51c7b3e 100644 --- a/images/golden/Dockerfile +++ b/images/golden/Dockerfile @@ -17,8 +17,9 @@ ENV DEBIAN_FRONTEND=noninteractive \ # -------- 1. ESSENTIAL -------- # Banger needs: an init (systemd + udev + dbus), sshd (the only # control channel), TLS roots + curl (first-boot installs + mise -# installer), iproute2 (debugging; `ip` is still useful even when -# the kernel sets IP via cmdline). +# installer), gnupg (build-time signing-key verification for the +# Docker apt repo), iproute2 (debugging; `ip` is still useful even +# when the kernel sets IP via cmdline). # # udev is a Recommends of the systemd package on Debian. With # --no-install-recommends it's skipped — and without it systemd never @@ -33,6 +34,7 @@ RUN apt-get update \ openssh-server \ ca-certificates \ curl \ + gnupg \ iproute2 \ && rm -rf /var/lib/apt/lists/* @@ -55,25 +57,57 @@ RUN apt-get update \ # Docker CE (with Compose v2 + buildx) from the official apt repo. # Nested-VM docker gives Compose workflows hostname/port isolation # per banger VM, which is a big part of the sandbox story. -RUN install -m 0755 -d /etc/apt/keyrings \ - && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \ - && chmod a+r /etc/apt/keyrings/docker.asc \ - && printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\n' \ - "$(dpkg --print-architecture)" > /etc/apt/sources.list.d/docker.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ +# +# The apt key is verified against its published fingerprint before +# we commit it to the signed-by keyring, so a tampered download (or +# a TLS compromise against download.docker.com) cannot silently +# swap in an attacker-controlled signing key. Fingerprint source: +# https://docs.docker.com/engine/install/debian/#install-using-the-repository +RUN set -eu; \ + expected_fpr=9DC858229FC7DD38854AE2D88D81803C0EBFCD88; \ + install -m 0755 -d /etc/apt/keyrings; \ + curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.asc; \ + got="$(gpg --with-colons --show-keys --fingerprint /tmp/docker.asc | awk -F: '/^fpr:/ {print $10; exit}')"; \ + if [ "$got" != "$expected_fpr" ]; then \ + echo "docker apt key fingerprint mismatch: got $got, want $expected_fpr" >&2; \ + exit 1; \ + fi; \ + mv /tmp/docker.asc /etc/apt/keyrings/docker.asc; \ + chmod a+r /etc/apt/keyrings/docker.asc; \ + printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable\n' \ + "$(dpkg --print-architecture)" > /etc/apt/sources.list.d/docker.list; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ docker-ce docker-ce-cli containerd.io \ - docker-buildx-plugin docker-compose-plugin \ - && rm -rf /var/lib/apt/lists/* + docker-buildx-plugin docker-compose-plugin; \ + rm -rf /var/lib/apt/lists/* -# mise — per-repo version manager. Installed system-wide so the -# bashrc activation reaches every shell. -RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \ - && chmod 0755 /usr/local/bin/mise \ - && install -d /etc/profile.d \ - && printf '%s\n' 'if [ -x /usr/local/bin/mise ]; then eval "$(/usr/local/bin/mise activate bash)"; fi' \ - > /etc/profile.d/mise.sh \ - && chmod 0644 /etc/profile.d/mise.sh +# mise — per-repo version manager. Installed from a pinned GitHub +# release asset rather than `curl https://mise.run | sh` so a compromise +# of the installer endpoint can't silently push arbitrary code into +# the golden image. +# +# Update protocol: bump MISE_VERSION + MISE_SHA256 together. Source +# for the hash is the `digest` field on the release asset from +# `gh release view --repo jdx/mise --json assets`, or compute from +# the downloaded file and cross-reference against SHASUMS256.txt on +# the release page. +ARG MISE_VERSION=v2026.4.18 +ARG MISE_SHA256_AMD64=6ae2d5f0f23a2f2149bc5d9bf264fe0922a1da843f1903e453516c462b23cc1f +RUN set -eux; \ + arch="$(dpkg --print-architecture)"; \ + if [ "$arch" != "amd64" ]; then \ + echo "mise pin only tracks amd64; add a ${arch} hash to refresh" >&2; \ + exit 1; \ + fi; \ + curl -fsSL -o /tmp/mise "https://github.com/jdx/mise/releases/download/${MISE_VERSION}/mise-${MISE_VERSION}-linux-x64"; \ + echo "${MISE_SHA256_AMD64} /tmp/mise" | sha256sum -c -; \ + install -m 0755 /tmp/mise /usr/local/bin/mise; \ + rm /tmp/mise; \ + install -d /etc/profile.d; \ + printf '%s\n' 'if [ -x /usr/local/bin/mise ]; then eval "$(/usr/local/bin/mise activate bash)"; fi' \ + > /etc/profile.d/mise.sh; \ + chmod 0644 /etc/profile.d/mise.sh # Default branch for any git init inside the sandbox. RUN git config --system init.defaultBranch main diff --git a/scripts/make-generic-kernel.sh b/scripts/make-generic-kernel.sh index a67a6b0..c732048 100755 --- a/scripts/make-generic-kernel.sh +++ b/scripts/make-generic-kernel.sh @@ -44,18 +44,70 @@ while [[ $# -gt 0 ]]; do esac done -for tool in curl tar make gcc; do +for tool in curl tar xz make gcc gpg gpgv; do command -v "$tool" >/dev/null 2>&1 || { log "missing required tool: $tool"; exit 1; } done [[ -f "$CONFIG" ]] || { log "config not found: $CONFIG"; exit 1; } +# kernel.org release signing keys. Stable (Greg KH) signs most point +# releases; mainline (Linus) signs .0 drops; Sasha Levin sometimes +# signs longterm backports. Listing all three keeps the script +# working across every release channel the user might pick. Rotations +# are rare and announced; update this list if gpg complains. +# +# Fingerprints verified against kernel.org: +# https://www.kernel.org/signature.html +KERNEL_SIGNING_KEYS=( + 647F28654894E3BD457199BE38DBBDC86092693E # Greg Kroah-Hartman + ABAF11C65A2970B130ABE3C479BE3E4300411886 # Linus Torvalds + E27E5D8A3403A2EF66873BBCDEA66FF797772CDC # Sasha Levin +) + TARBALL="linux-${KERNEL_VERSION}.tar.xz" -URL="https://cdn.kernel.org/pub/linux/kernel/v${KERNEL_MAJOR}.x/$TARBALL" +SIGNATURE="linux-${KERNEL_VERSION}.tar.sign" +BASE_URL="https://cdn.kernel.org/pub/linux/kernel/v${KERNEL_MAJOR}.x" SRC_DIR="$(mktemp -d)" trap 'rm -rf "$SRC_DIR"' EXIT -log "downloading kernel $KERNEL_VERSION from $URL" -curl -fSL --progress-bar -o "$SRC_DIR/$TARBALL" "$URL" +# Isolated GNUPGHOME so the verification step can't accidentally +# trust whatever the invoking user already has in their keyring. The +# trap above cleans the whole SRC_DIR, including this. +GPG_HOME="$SRC_DIR/gnupg" +install -d -m 0700 "$GPG_HOME" +export GNUPGHOME="$GPG_HOME" + +log "importing kernel.org signing keys" +# keyserver.ubuntu.com first: it returns keys with user IDs intact, +# which gpg needs to mark the key as usable. keys.openpgp.org (the +# current SKS successor) strips unverified UIDs on upload, and the +# kernel.org devs haven't all completed its email verification flow, +# so pulling from there returns UID-less keys that gpg then refuses +# to trust. We fall back to it anyway in case ubuntu is unreachable. +if ! gpg --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys "${KERNEL_SIGNING_KEYS[@]}" 2>/dev/null; then + log "key fetch from keyserver.ubuntu.com failed; trying keys.openpgp.org" + gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "${KERNEL_SIGNING_KEYS[@]}" +fi + +log "downloading kernel $KERNEL_VERSION from $BASE_URL/$TARBALL" +curl -fSL --progress-bar -o "$SRC_DIR/$TARBALL" "$BASE_URL/$TARBALL" +curl -fSL --progress-bar -o "$SRC_DIR/$SIGNATURE" "$BASE_URL/$SIGNATURE" + +log "verifying signature" +# The .tar.sign is a detached signature over the *uncompressed* tar, +# per kernel.org convention. Pipe the xz-decompressed stream into +# gpg --verify so we never materialise an unverified tarball on disk. +# Require VALIDSIG (the cryptographic proof — GOODSIG alone is +# printed even for expired/revoked keys, VALIDSIG requires a usable +# key and a mathematically valid signature). +VERIFY_STATUS="$SRC_DIR/verify.status" +xz -cd "$SRC_DIR/$TARBALL" | gpg --batch --status-fd 3 --verify "$SRC_DIR/$SIGNATURE" - 3>"$VERIFY_STATUS" 2>/dev/null || true +if ! grep -qE '^\[GNUPG:\] VALIDSIG' "$VERIFY_STATUS"; then + log "signature verification FAILED — refusing to build" + log "gpg status:" + cat "$VERIFY_STATUS" >&2 || true + exit 1 +fi +log "signature OK (signed by $(awk '/^\[GNUPG:\] VALIDSIG/ {print $3}' "$VERIFY_STATUS"))" log "extracting" tar -xf "$SRC_DIR/$TARBALL" -C "$SRC_DIR" --strip-components=1