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>
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>
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