From be119a7d61243a8852bf030fb4330a9bb29c1399 Mon Sep 17 00:00:00 2001 From: gabriel engvall Date: Fri, 25 Sep 2026 20:18:37 +0200 Subject: [PATCH 1/2] fix(debian-11): install without the bullseye-security suite Debian has removed every bullseye binary from security.debian.org, but the bullseye-security index still lists them. With the security suite selected, pkgsel fetches those packages, gets 404 for each, and the install stops at "Select and install software" (pkgsel exit 100). archive.debian.org has not received bullseye-security yet, so no official mirror serves it. Pass apt-setup/services-select=updates on the boot command so the installer uses main and bullseye-updates only. It goes there rather than in preseed.cfg because the Debian preseeds stay byte-identical across releases. Bullseye's cloud.cfg sets apt_preserve_sources_list, so clones keep this sources.list and Proxmox's default package_upgrade does not hit the dead suite on first boot. --- docs/recipes.md | 6 ++++++ recipes/debian-11.pkr.hcl | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/recipes.md b/docs/recipes.md index 1e62d48..4279aa1 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -154,6 +154,12 @@ without `git clean`. The hostname line uses `__PACKER_RECIPE_NAME__`, replaced b the same script, so the preseed files stay identical across Debian releases (enforced by `tests/recipe-consistency.test.ts`). +An answer that only one release needs goes on that recipe's `boot_command` +instead, where the installer reads it as a preseed value. `debian-11` passes +`apt-setup/services-select=updates` there: the bullseye-security index still +lists packages whose binaries security.debian.org no longer serves, so +selecting the security suite fails the package step. + ## AlmaLinux and Rocky Linux kickstart Copy the nearest `ks.cfg` and update repository and release details. These builds diff --git a/recipes/debian-11.pkr.hcl b/recipes/debian-11.pkr.hcl index 8c42f0b..02a1cfb 100644 --- a/recipes/debian-11.pkr.hcl +++ b/recipes/debian-11.pkr.hcl @@ -173,6 +173,14 @@ source "proxmox-iso" "debian-11" { "console-setup/ask_detect=false ", "console-keymaps-at/keymap=us ", "grub-installer/bootdev=/dev/sda ", + # Install from main and bullseye-updates only. Debian has removed every + # bullseye binary from security.debian.org while the bullseye-security + # index still lists them, so selecting that suite makes pkgsel fetch 404s + # and the install stops at "Select and install software". Passed here + # rather than in preseed.cfg, which the Debian recipes share byte for + # byte. Bullseye's cloud.cfg sets apt_preserve_sources_list, so clones + # keep this sources.list instead of cloud-init re-adding the suite. + "apt-setup/services-select=updates ", "", ] From 01cb92aa5a8ce1e7b3fd3cdf0bb22fa36ee88b00 Mon Sep 17 00:00:00 2001 From: Eric Wang <37554696+ericwang401@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:14:03 -0400 Subject: [PATCH 2/2] fix(debian-11): install from archive.debian.org bullseye's LTS has ended, so deb.debian.org will drop it for archive.debian.org, and the build would fail again the day it does. Install from the archive now; it already serves bullseye and bullseye-updates and keeps them. The mirror cannot go on the boot command like services-select does. The installer loads preseed.cfg with debconf-set-selections after applying boot parameters, and the shared preseed sets mirror/http/hostname, so a boot-command value would be silently overridden. The preseed now carries a __PACKER_DEBIAN_MIRROR__ placeholder that inject-placeholders.sh fills from the recipe's `# mirror:` header, defaulting to deb.debian.org; the Debian preseeds stay byte-identical. A fallback to deb.debian.org would build fine today and fail later, so the build asserts sources.list points at the archive and omits bullseye-security. --- docs/recipes.md | 10 ++++++++- recipes/debian-11.pkr.hcl | 19 +++++++++++++++-- recipes/debian-11/http/preseed.cfg | 2 +- recipes/debian-12/http/preseed.cfg | 2 +- recipes/debian-13/http/preseed.cfg | 2 +- scripts/inject-placeholders.sh | 8 +++++++ tests/inject-placeholders.test.ts | 34 ++++++++++++++++++++++++++++++ 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 4279aa1..57c2678 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -155,11 +155,19 @@ the same script, so the preseed files stay identical across Debian releases (enforced by `tests/recipe-consistency.test.ts`). An answer that only one release needs goes on that recipe's `boot_command` -instead, where the installer reads it as a preseed value. `debian-11` passes +instead, where the installer reads it as a preseed value, but only for a key +`preseed.cfg` does not set. The installer loads the file after applying boot +parameters, so the file wins any key both name. `debian-11` passes `apt-setup/services-select=updates` there: the bullseye-security index still lists packages whose binaries security.debian.org no longer serves, so selecting the security suite fails the package step. +The package mirror is a key the preseed does set, so it is a placeholder +instead: `__PACKER_DEBIAN_MIRROR__`, filled by `scripts/inject-placeholders.sh` +from the recipe's `# mirror:` header and defaulting to `deb.debian.org`. A +release Debian has archived sets `# mirror: archive.debian.org`, which keeps +serving it after `deb.debian.org` drops it. + ## AlmaLinux and Rocky Linux kickstart Copy the nearest `ks.cfg` and update repository and release details. These builds diff --git a/recipes/debian-11.pkr.hcl b/recipes/debian-11.pkr.hcl index 02a1cfb..6dc42a0 100644 --- a/recipes/debian-11.pkr.hcl +++ b/recipes/debian-11.pkr.hcl @@ -1,5 +1,6 @@ # display: Debian 11 (Bullseye) # group: debian +# mirror: archive.debian.org # build_vmid: 4000 # min_cores: 1 # min_memory: 1024 @@ -178,8 +179,11 @@ source "proxmox-iso" "debian-11" { # index still lists them, so selecting that suite makes pkgsel fetch 404s # and the install stops at "Select and install software". Passed here # rather than in preseed.cfg, which the Debian recipes share byte for - # byte. Bullseye's cloud.cfg sets apt_preserve_sources_list, so clones - # keep this sources.list instead of cloud-init re-adding the suite. + # byte; this works only because preseed.cfg never sets the key, since the + # installer loads that file after the boot parameters and it would win. + # The mirror is archive.debian.org (`# mirror:` above), where bullseye + # stays after deb.debian.org drops it. Bullseye's cloud.cfg sets + # apt_preserve_sources_list, so clones keep this sources.list. "apt-setup/services-select=updates ", "", ] @@ -216,6 +220,17 @@ build { ] } + # A mirror that silently fell back to deb.debian.org would build fine today + # and break the day Debian drops bullseye from it, so check what landed. + provisioner "shell" { + inline = [ + "echo '==> Verifying apt sources'", + "cat /etc/apt/sources.list", + "grep -Eq '^deb http://archive.debian.org/debian/? bullseye main' /etc/apt/sources.list || { echo 'sources.list does not use archive.debian.org' >&2; exit 1; }", + "! grep -Eq '^deb .*bullseye-security' /etc/apt/sources.list || { echo 'sources.list still enables bullseye-security' >&2; exit 1; }", + ] + } + provisioner "file" { source = "${path.root}/_shared/cloud-init-cleanup.sh" destination = "/tmp/cloud-init-cleanup.sh" diff --git a/recipes/debian-11/http/preseed.cfg b/recipes/debian-11/http/preseed.cfg index e1d9bc0..9fca11f 100644 --- a/recipes/debian-11/http/preseed.cfg +++ b/recipes/debian-11/http/preseed.cfg @@ -10,7 +10,7 @@ d-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__ d-i netcfg/wireless_wep string d-i hw-detect/load_firmware boolean false d-i mirror/country string manual -d-i mirror/http/hostname string deb.debian.org +d-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__ d-i mirror/http/directory string /debian d-i mirror/http/proxy string d-i passwd/root-login boolean false diff --git a/recipes/debian-12/http/preseed.cfg b/recipes/debian-12/http/preseed.cfg index e1d9bc0..9fca11f 100644 --- a/recipes/debian-12/http/preseed.cfg +++ b/recipes/debian-12/http/preseed.cfg @@ -10,7 +10,7 @@ d-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__ d-i netcfg/wireless_wep string d-i hw-detect/load_firmware boolean false d-i mirror/country string manual -d-i mirror/http/hostname string deb.debian.org +d-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__ d-i mirror/http/directory string /debian d-i mirror/http/proxy string d-i passwd/root-login boolean false diff --git a/recipes/debian-13/http/preseed.cfg b/recipes/debian-13/http/preseed.cfg index e1d9bc0..9fca11f 100644 --- a/recipes/debian-13/http/preseed.cfg +++ b/recipes/debian-13/http/preseed.cfg @@ -10,7 +10,7 @@ d-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__ d-i netcfg/wireless_wep string d-i hw-detect/load_firmware boolean false d-i mirror/country string manual -d-i mirror/http/hostname string deb.debian.org +d-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__ d-i mirror/http/directory string /debian d-i mirror/http/proxy string d-i passwd/root-login boolean false diff --git a/scripts/inject-placeholders.sh b/scripts/inject-placeholders.sh index 2ddb019..73696bf 100755 --- a/scripts/inject-placeholders.sh +++ b/scripts/inject-placeholders.sh @@ -21,6 +21,13 @@ rm -f "$VARS_FILE" RECIPE_DIR="recipes/${RECIPE}" +# Debian package mirror, from the recipe's `# mirror:` header. It has to reach +# the preseed file rather than the boot command: the installer loads the file +# with debconf-set-selections after applying boot parameters, so a key the +# shared preseed sets would silently override a boot-command value. +DEBIAN_MIRROR="$(sed -nE 's/^#[[:space:]]*mirror:[[:space:]]*([^[:space:]]+).*/\1/p' "recipes/${RECIPE}.pkr.hcl" 2>/dev/null | head -n1 || true)" +DEBIAN_MIRROR="${DEBIAN_MIRROR:-deb.debian.org}" + # ── Detect installer files and generate ephemeral SSH keypair ──────────────── PRESEED="${RECIPE_DIR}/http/preseed.cfg" USER_DATA="${RECIPE_DIR}/http/user-data" @@ -47,6 +54,7 @@ if [ "$NEEDS_KEY" = "1" ]; then "s|__PACKER_SSH_PUBLIC_KEY__|${PUB_KEY}|g; \ s|ssh-ed25519 AAAA[^ ]+ packer-${RECIPE}-[^ '\"]*|${PUB_KEY}|g; \ s|__PACKER_RECIPE_NAME__|${RECIPE}|g; \ + s|__PACKER_DEBIAN_MIRROR__|${DEBIAN_MIRROR}|g; \ s|__PACKER_BUILD_IP__|${CF_BUILD_IP:-}|g; \ s|__PACKER_BUILD_GW__|${CF_BUILD_GW:-}|g; \ s|__PACKER_BUILD_DNS__|${CF_BUILD_DNS:-1.1.1.1}|g" \ diff --git a/tests/inject-placeholders.test.ts b/tests/inject-placeholders.test.ts index 69a570b..f78b19e 100644 --- a/tests/inject-placeholders.test.ts +++ b/tests/inject-placeholders.test.ts @@ -33,6 +33,7 @@ describe('inject-placeholders.sh', () => { preseed, [ 'd-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__', + 'd-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__', 'd-i netcfg/get_ipaddress string __PACKER_BUILD_IP__', 'd-i netcfg/get_gateway string __PACKER_BUILD_GW__', 'd-i netcfg/get_nameservers string __PACKER_BUILD_DNS__', @@ -57,6 +58,10 @@ describe('inject-placeholders.sh', () => { expect(injected).toContain( 'd-i netcfg/get_hostname string packer-debian-test' ) + // No recipe file, so no `# mirror:` header: the default mirror. + expect(injected).toContain( + 'd-i mirror/http/hostname string deb.debian.org' + ) expect(injected).toContain('d-i netcfg/get_ipaddress string 10.99.0.5') expect(injected).toContain('d-i netcfg/get_gateway string 10.99.0.1') expect(injected).toContain('d-i netcfg/get_nameservers string 9.9.9.9') @@ -69,6 +74,35 @@ describe('inject-placeholders.sh', () => { ) }) + // A release Debian has archived installs from archive.debian.org. This has + // to reach the preseed file: a boot-command value loses to it. + test("takes the Debian mirror from the recipe's # mirror: header", () => { + const root = mkdtempSync(join(tmpdir(), 'cf-inject-')) + tempDirs.push(root) + const httpDir = join(root, 'recipes', 'debian-old', 'http') + const runnerTemp = join(root, 'runner-temp') + mkdirSync(httpDir, { recursive: true }) + mkdirSync(runnerTemp) + writeFileSync( + join(root, 'recipes', 'debian-old.pkr.hcl'), + '# display: Debian Old\n# mirror: archive.debian.org\n' + ) + const preseed = join(httpDir, 'preseed.cfg') + writeFileSync( + preseed, + 'd-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__\n' + ) + + const result = Bun.spawnSync(['bash', script, 'debian-old'], { + cwd: root, + env: { ...process.env, RUNNER_TEMP: runnerTemp }, + }) + expect(result.exitCode).toBe(0) + expect(readFileSync(preseed, 'utf8')).toBe( + 'd-i mirror/http/hostname string archive.debian.org\n' + ) + }) + test('protects generated Windows credentials with mode 0600', () => { const root = mkdtempSync(join(tmpdir(), 'cf-inject-')) tempDirs.push(root)