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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
Split the rootless Tor e2e skip so the deterministic offline checks (the
real offline onion keygen and the production-flag guard) run whenever the
tor binary is present, while only the live onion round-trip stays gated on
LIM_E2E_TOR=1. The pytest job now installs tor so the keygen — which
validates the exact production keygen path — runs on every push. A separate
continue-on-error tor-network-e2e job exercises the full round-trip without
making the public Tor network a blocking merge gate.
The full QEMU build/boot/unlock e2e still needs root + KVM + Arch tooling
and stays local/opt-in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
README: Tor onion unlock feature + security note, the rootless and QEMU
end-to-end tests and how to run them, project-structure entries, and a
correction that PyYAML>=6 is required (not stdlib-only) on the
package-manager/symlink install path. Makefile: test-tor, test-qemu and
test-all targets, with an overridable PYTHON interpreter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opt-in via LIM_E2E_TOR=1. Generates the v3 onion keys offline exactly as
the image build does, stands up a real Tor onion service from a torrc
mirroring the baked-in one, and delivers a passphrase through Tor to a
dropbear stand-in — asserting it arrives and the endpoint "unlocks".
Needs the real tor binary and network, so it is skipped otherwise; the
offline keygen and production-flag guard run without network.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encrypted image setups can now bake a Tor onion service into the initramfs
so the dropbear unlock shell stays reachable behind NAT or a dynamic IP.
When the user opts in, configure_encryption installs tor + busybox, drops
the mkinitcpio hooks (ordered `netconf tor dropbear encryptssh`), generates
the v3 onion keys offline in the image chroot, and prints the stable
.onion address. Unlock with `torsocks ssh root@<onion-address>`.
The runtime hook syncs the clock via NTP first (RTC-less boards boot at
1970, which Tor's consensus checks reject) and starts the onion service
pointing at dropbear on 127.0.0.1:22.
Hardening baked in from an adversarial review of the shipped path:
- cmdline.txt boot path (RPi4-class firmware boot) now sets the same
ip=::::<host>:eth0:dhcp net.ifnames=0 params as the boot.txt path, so
the initramfs actually gets a network and the onion can publish.
- the initramfs bakes in libnss_dns.so.2 so the NTP hostname resolves.
- the hook extracts DHCP DNS with sed instead of sourcing the lease files,
which would run attacker-controlled DHCP option strings as root pre-boot.
- NTP is attempted unconditionally (bounded), not gated on DHCP-provided DNS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"lim" is taken on PyPI (a GLMM genomics package); linux-image-manager
is free and matches the repo name. Import package and CLI alias stay
"lim". MIRRORS follows the hetzner-arch-luks schema.
Bash scripts were untestable and duplicated device/LUKS/mount logic;
the lim/ package centralizes it behind one subprocess wrapper and a
YAML image catalog (single point of truth).
BREAKING CHANGE: scripts/*.sh removed. Use `lim --type <cmd>`; new
types mount/umount/single-boot/raid1-boot/lock/unlock/import/export
replace direct script calls. --extra is deprecated and ignored.
- distributions.yml + lim/catalog.py hold the image catalog (PyYAML)
- pytest suite: 102 tests with mocked subprocess (tests/unit) and a
250-line max file-length guard (tests/lint)
- ruff strict (select ALL), GitHub Actions CI, Dependabot; Travis gone
- Makefile: install (symlink ~/.local/bin/lim) and test targets
- fixes over bash: SUDO_USER-aware chown, mmcblk/nvme partition paths,
sha512 checksum support, whole-pipeline failure detection, blkid
UUID fallback for pre-mounted images, conditional fstab seeding for
PARTUUID/LABEL images, clean errors for missing binaries