12 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
b3bd5b6740 Release version 2.3.0
Some checks failed
tests / lint (push) Has been cancelled
tests / pytest (push) Has been cancelled
tests / tor-network-e2e (push) Has been cancelled
tests / qemu-e2e-debian (push) Has been cancelled
2026-07-24 18:16:57 +02:00
Kevin Veen-Birkenbach
496268fa39 feat(cli): numbered selection menus for distributions and versions
Some checks failed
tests / lint (push) Has been cancelled
tests / pytest (push) Has been cancelled
tests / tor-network-e2e (push) Has been cancelled
tests / qemu-e2e-debian (push) Has been cancelled
Replace the free-text distribution/version prompts with numbered menus via a
shared _choose helper. Name-first resolution keeps typing the name working, so
numeric catalog keys (e.g. arch "4") still select by name, not by position.
Applied to the distribution, arch, manjaro, retropie and raspios choosers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:57 +02:00
Kevin Veen-Birkenbach
919651277e fix(image): keep the Tor onion reachable on RTC-less boards
An RTC-less Pi boots at 1970; Tor then rejects the consensus and the unlock
onion never publishes. Bake a clock floor (build epoch) into the initramfs and
jump the clock forward to it before Tor starts, and pass a numeric tor_ntp= in
the cmdline so busybox ntpd syncs without DNS. Proven on hardware: the onion
unlock succeeded on a Pi 3 after this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:57 +02:00
Kevin Veen-Birkenbach
0c621f60ae fix(session): mount the boot FAT at /boot/firmware on Bookworm
Raspberry Pi OS Bookworm keeps the FAT boot partition at /boot/firmware, where
the raspi-firmware post-update hook syncs the initramfs. Binding it at /boot in
the chroot sent update-initramfs's output to the ext4 root, so the firmware
booted a stock initramfs without cryptsetup -> (initramfs) emergency shell, no
LUKS prompt. boot_bind_target() detects the layout and binds (and unmounts) the
FAT at the right place; legacy single-FAT layouts still use /boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
30ee1101ce feat(cli): guided encrypted-image wizard (default) + remote-unlock
Add a guided setup: one interactive command that asks everything up front then
builds an encrypted, Tor-remote-unlockable image unattended (distribution,
target device, hostname, login user + key, password), creating or renaming the
login user and installing the SSH key for both unlock and post-boot login.

- wizard.py: _collect (all prompts) + _execute (autonomous build); renames a
  stock pi/alarm user or creates one, grants sudo, installs the login key.
- unlock.py + `lim --type remote-unlock`: reach the initramfs over Tor (onion,
  torsocks) or plain SSH (host/IP), run cryptroot-unlock or the passphrase
  prompt; the wizard persists a target record under ~/.config/lim/unlocks.
- cli.py: register guided (default --type) and remote-unlock; drop the
  deprecated --extra argument.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
14094e9436 feat(image): auto-enable cross-arch chroot via qemu binfmt
Building a foreign-arch image (e.g. arm64 Raspberry Pi OS on an x86 host) needs
a qemu binfmt handler, or the chroot fails with "Exec format error". crossarch
detects this before the target is erased and, on confirmation, installs
qemu-user-static via the host package manager (apt-get/dnf/zypper/pacman) and
registers binfmt; otherwise it aborts with per-distro instructions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
5bef6177ac feat(image): flash full disk images into LUKS; add Raspberry Pi OS
Support distributions that ship a full .img (Raspberry Pi OS, moode, RetroPie,
Manjaro ARM) rather than a rootfs tarball, by loop-mounting the image and
copying its boot + root partitions into a fresh LUKS container.

- distributions.yml/catalog.py/choosers.py: raspios catalog (lite64/desktop64/
  lite32 via the stable _latest redirects) + choose_raspios.
- plan.py: source_url override so a _latest redirect downloads under an .img.xz
  name that decompress_command recognises.
- transfer.py: transfer_disk_image (loop-mount -> repartition -> LUKS -> rsync
  copy with progress, cp fallback -> fix boot fstab); transfer_image gains
  interactive= and routes encrypted non-arch images here; download_image gains
  force_prompt.
- loopimg.py: losetup attach/detach/partition helper.
- fsutil.drop_fstab_mount + register_encrypted_root: replace a stock image's
  existing / fstab line instead of colliding with it.
- encryption.configure_encryption returns the onion address.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
68ea39b784 chore(session): harden chroot package installs (PATH + apt-get update)
- chroot_bash exports a Debian-safe PATH so /usr/sbin tools (update-initramfs,
  useradd, chpasswd, ...) resolve inside the chroot instead of failing with
  code 127.
- install_packages runs apt-get update before install (a stock image ships
  stale lists whose superseded .deb URLs 404) and non-interactive
  apt-get install -y; pacman gains -Sy for the same index-refresh reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:56 +02:00
Kevin Veen-Birkenbach
c3a41db796 style: apply ruff format across the tree; refresh moved-module doc refs
Bring 14 files that predated the ruff-format run into line with the configured
formatter (line-length 100); provably formatting-only (ruff format of HEAD ==
working tree, and ruff format is semantics-preserving). Also refresh two
lim.image.tor._KEYGEN_SCRIPT doc references in tor_harness.py to
lim.image.initramfs.keygen after the module moved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:55 +02:00
Kevin Veen-Birkenbach
336e68845e ci: run the Debian QEMU e2e on push; add make target and docs
- test.yml: add a qemu-e2e-debian job (build -> boot -> LUKS-unlock in QEMU
  via the deterministic direct transport) running on push/PR, plus a
  workflow_dispatch trigger for manual runs.
- Makefile: test-qemu-debian target (LIM_E2E_OS=debian); document the direct
  vs tor transport on test-qemu.
- README: document the two build scripts and the direct transport.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:55 +02:00
Kevin Veen-Birkenbach
e7712880f1 test(e2e): Debian QEMU build+boot+unlock harness and deterministic direct transport
Extend the QEMU e2e to cover the initramfs-tools backend and add a
deterministic unlock transport that avoids the flaky public-Tor onion
round-trip inside QEMU.

- build_image_debian.sh: debootstrap Bookworm, install the real
  lim/configuration/initramfs-tools hooks, LUKS + cryptsetup-initramfs +
  dropbear-initramfs, offline onion keys, boot-ok marker; same image.env
  contract as build_image.sh.
- config.py: QemuSpec gains os_family / unlock_command / direct_ssh_port;
  Debian cmdline uses root=/dev/mapper (crypttab-baked, no cryptdevice=);
  direct_ssh_port adds hostfwd to guest dropbear and a plain-SSH target.
- harness.py: unlock_transport="direct" default, _NullNet, per-OS build
  script + unlock command, up-front sudo priming with keepalive.
- boot_unlock.py: background delivery worker holds the SSH session open;
  direct vs tor target and initial delay.
- test_qemu_harness_unit.py: always-on guards for the Debian/direct branches;
  test_qemu_unlock_e2e.py parameterized by ARCH/OS/TRANSPORT env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:55 +02:00
Kevin Veen-Birkenbach
9b7a34989d feat(image): distro-agnostic remote unlock via initramfs backends + Debian support
Split the mkinitcpio-only remote-LUKS-unlock path into an InitramfsBackend
ABC with a get_backend() dispatch, and add the initramfs-tools backend for
Debian / Raspberry Pi OS.

- base.py: six-step backend contract; encryption.py becomes a thin,
  distro-neutral sequencer (get_backend by distribution).
- initramfs_tools.py: crypttab `none luks,initramfs`, cmdline rewritten to
  root=/dev/mapper + ip=::::host:eth0:dhcp, dropbear-initramfs
  authorized_keys, update-initramfs -k all (no build-host uname leak).
- shipped hooks (configuration/initramfs-tools/*): single-hop non-anonymous
  onion, libnss DNS baking, sed-not-source DHCP, kill-tor-before-pivot.
- shared offline onion keygen in keygen.py; tor.py removed (logic moved to
  mkinitcpio.py).
- raspios added to the apt distro family (session.py, raspberry.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:15:54 +02:00
2 changed files with 50 additions and 1 deletions

View File

@@ -1,5 +1,54 @@
# Changelog # Changelog
## [2.3.0] - 2026-07-24
**✨ New**
- Guided setup wizard, now the default command: *lim* with no *--type* (or
*lim --type guided*) asks everything up front — distribution, target device,
hostname, login user + SSH key, password — then builds an encrypted,
Tor-remote-unlockable image unattended.
- Debian / Raspberry Pi OS remote LUKS unlock. The initramfs unlock stack is
now distro-agnostic behind an *InitramfsBackend* abstraction: *mkinitcpio*
for Arch/Manjaro, *initramfs-tools* (cryptsetup-initramfs + dropbear-initramfs
+ a Tor onion) for Raspberry Pi OS / moode / RetroPie.
- Flash full disk images into LUKS. Distributions that ship a *.img* rather
than a rootfs tarball (Raspberry Pi OS, moode, RetroPie, Manjaro ARM) are
loop-mounted and their boot + root partitions copied into a fresh LUKS
container. New Raspberry Pi OS catalog (*lite64* / *desktop64* / *lite32*).
- New *remote-unlock* command: reach a waiting initramfs over Tor (onion, via
*torsocks*) or plain SSH (host/IP), running *cryptroot-unlock* or presenting
the passphrase prompt. The wizard saves a target record under
*~/.config/lim/unlocks* so *lim --type remote-unlock* offers it directly.
- Cross-architecture builds. Building a foreign-arch image (e.g. arm64
Raspberry Pi OS on an x86 host) auto-installs *qemu-user-static* and registers
binfmt, or aborts before the target is erased with per-distro instructions.
- RTC-less clock hardening. A clock floor (build epoch baked into the initramfs)
plus a numeric *tor_ntp=* cmdline let the clock sync without DNS, so the Tor
onion still publishes on boards without a real-time clock (which boot at 1970).
- Numbered selection menus for distribution and version prompts; typing the
name still works.
- CI: a full Debian *build → boot → LUKS-unlock* QEMU end-to-end job runs on
push (*make test-qemu-debian*).
**🐛 Fixes**
- Raspberry Pi OS Bookworm boots the encrypted image: the initramfs is placed on
the */boot/firmware* FAT partition instead of the ext4 root, so cryptsetup is
present at boot (no more *(initramfs)* emergency shell).
- Chroot package installs are robust cross-host: a Debian-safe *PATH* (so
*update-initramfs* / *useradd* resolve) and *apt-get update* before install
(stock images ship stale lists whose superseded *.deb* URLs 404).
- Stock-image *fstab* is normalized (the existing */* line is replaced, not
duplicated) when copying a distro image into LUKS.
- Large copies show progress (*rsync --info=progress2*, *cp* fallback).
**💥 Breaking / behavior**
- *lim* with no *--type* now runs the guided wizard (previously *--type* was
required).
- The long-deprecated *--extra* argument is removed.
## [2.2.0] - 2026-07-21 ## [2.2.0] - 2026-07-21
- Optional remote LUKS unlock via a Tor onion service in the initramfs. - Optional remote LUKS unlock via a Tor onion service in the initramfs.

View File

@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "linux-image-manager" name = "linux-image-manager"
version = "2.2.0" version = "2.3.0"
description = "CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance" description = "CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"