linux-image-manager 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/PKG-INFO +74 -3
  2. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/README.md +73 -2
  3. linux_image_manager-2.2.0/lim/configuration/initcpio/tor_hook +47 -0
  4. linux_image_manager-2.2.0/lim/configuration/initcpio/tor_install +37 -0
  5. linux_image_manager-2.2.0/lim/configuration/initcpio/torrc +7 -0
  6. linux_image_manager-2.2.0/lim/configuration/packages/server/tor.txt +3 -0
  7. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/encryption.py +14 -2
  8. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/plan.py +1 -0
  9. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/setup.py +4 -0
  10. linux_image_manager-2.2.0/lim/image/tor.py +79 -0
  11. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/linux_image_manager.egg-info/PKG-INFO +74 -3
  12. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/linux_image_manager.egg-info/SOURCES.txt +5 -0
  13. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/pyproject.toml +6 -2
  14. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/LICENSE.txt +0 -0
  15. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/__init__.py +0 -0
  16. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/catalog.py +0 -0
  17. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/cli.py +0 -0
  18. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/config.py +0 -0
  19. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/configuration/packages/general.txt +0 -0
  20. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/configuration/packages/server/luks.txt +0 -0
  21. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/data/__init__.py +0 -0
  22. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/data/crypt.py +0 -0
  23. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/data/sync.py +0 -0
  24. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/device.py +0 -0
  25. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/distributions.yml +0 -0
  26. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/errors.py +0 -0
  27. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/fsutil.py +0 -0
  28. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/__init__.py +0 -0
  29. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/backup.py +0 -0
  30. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/choosers.py +0 -0
  31. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/chroot.py +0 -0
  32. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/raspberry.py +0 -0
  33. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/session.py +0 -0
  34. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/transfer.py +0 -0
  35. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/image/verify.py +0 -0
  36. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/luks.py +0 -0
  37. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/packages.py +0 -0
  38. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/runner.py +0 -0
  39. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/storage/__init__.py +0 -0
  40. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/storage/common.py +0 -0
  41. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/storage/raid1.py +0 -0
  42. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/storage/single_drive.py +0 -0
  43. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/system.py +0 -0
  44. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/lim/ui.py +0 -0
  45. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/linux_image_manager.egg-info/dependency_links.txt +0 -0
  46. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/linux_image_manager.egg-info/entry_points.txt +0 -0
  47. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/linux_image_manager.egg-info/requires.txt +0 -0
  48. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/linux_image_manager.egg-info/top_level.txt +0 -0
  49. {linux_image_manager-2.1.0 → linux_image_manager-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linux-image-manager
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance
5
5
  Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
6
6
  Maintainer-email: Kevin Veen-Birkenbach <kevin@veen.world>
@@ -28,6 +28,7 @@ Linux Image Manager (lim) is a Python tool for downloading, configuring, and man
28
28
 
29
29
  - **Image Download & Setup:** Automatically download, verify (checksum + GPG signature) and prepare Linux distributions.
30
30
  - **Encrypted Storage:** Configure LUKS encryption for secure image management.
31
+ - **Tor Onion Unlock:** Optionally bake a Tor onion service into the initramfs so the dropbear LUKS unlock shell stays reachable behind NAT or dynamic IPs.
31
32
  - **Virtual RAID1:** Easily set up virtual Btrfs RAID1 for data redundancy.
32
33
  - **Backup & Restore:** Create image backups from devices using dd.
33
34
  - **Chroot Environment:** Easily enter a chroot shell to maintain or modify Linux images.
@@ -44,7 +45,7 @@ package-manager install lim
44
45
 
45
46
  This command makes Linux Image Manager globally available as `lim` in your terminal. The `lim` alias points to the **main.py** entry point.
46
47
 
47
- There are no Python dependencies beyond the standard library. The commands call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
48
+ The only Python dependency is **PyYAML** (`>=6`), used to read the image catalog. The pip/wheel install pulls it in automatically; on the `package-manager`/symlink install path make sure it is present (`python-yaml` on Arch). The commands also call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
48
49
 
49
50
  ## Usage ⚙️
50
51
 
@@ -112,6 +113,8 @@ lim/
112
113
  distributions.yml # single point of truth for the image catalog
113
114
  configuration/ # package collections used during image setup
114
115
  tests/unit/ # unit tests (all external commands mocked)
116
+ tests/e2e/ # Tor onion unlock end-to-end tests (rootless + QEMU, opt-in)
117
+ tests/e2e/qemu/ # full virtualized build+boot+unlock harness
115
118
  tests/lint/ # architecture guards (e.g. max file length)
116
119
  ```
117
120
 
@@ -120,15 +123,83 @@ tests/lint/ # architecture guards (e.g. max file length)
120
123
  Customize your environment in the `lim/configuration/` folder:
121
124
  - **General Packages:** Contains common packages for all setup scripts.
122
125
  - **Server LUKS Packages:** Contains packages needed for setting up LUKS encryption on servers.
126
+ - **initcpio Hooks:** The mkinitcpio install/runtime hooks and torrc baked into images for the Tor onion unlock.
127
+
128
+ ## Remote LUKS Unlock via Tor 🧅
129
+
130
+ When you answer yes to *"Should the system be remotely unlockable via a Tor
131
+ onion service?"* during an encrypted `lim --type image` setup, the image gets:
132
+
133
+ - `tor` and `busybox` installed, plus a custom `tor` mkinitcpio hook ordered
134
+ between `netconf` and `dropbear`.
135
+ - A v3 onion service key generated **offline** inside the image chroot; the
136
+ resulting `.onion` address is printed at the end of the setup and stays
137
+ stable across reboots and IP changes.
138
+ - An NTP clock sync in early userspace (boards like the Raspberry Pi have no
139
+ RTC and would otherwise boot with a clock Tor rejects). Override the server
140
+ with the kernel parameter `tor_ntp=<host>`.
141
+
142
+ After booting the device, unlock it from any machine with a running Tor
143
+ client:
144
+
145
+ ```bash
146
+ torsocks ssh root@<onion-address>
147
+ ```
148
+
149
+ Typing the LUKS passphrase into that shell resumes the boot. The direct
150
+ `ssh root@<ip>` unlock keeps working as before; Tor is an additional path,
151
+ useful behind NAT/CGNAT where no port forwarding is possible.
152
+
153
+ **Security note:** the onion private key is stored in the initramfs on the
154
+ unencrypted boot partition. Anyone with physical access to the SD card can
155
+ read it and impersonate the onion service — treat the address as
156
+ non-secret. Access control remains the SSH public key
157
+ (`/etc/dropbear/root_key`), exactly as with the direct unlock.
123
158
 
124
159
  ## Development & Tests 🧪
125
160
 
126
- The test suite mocks all external commands, so it runs safely on any machine:
161
+ The unit suite mocks all external commands, so it runs safely on any machine:
127
162
 
128
163
  ```bash
129
164
  pytest
130
165
  ```
131
166
 
167
+ ### Tor unlock end-to-end test
168
+
169
+ `tests/e2e/test_tor_unlock_e2e.py` reproduces the operator-visible half of the
170
+ Tor decryption process **rootless** — no block devices, no `cryptsetup`, no
171
+ `chroot`. It generates the v3 onion keys offline (exactly as `lim.image.tor`
172
+ does inside the image), starts a real Tor onion service from a torrc mirroring
173
+ the baked-in one, then connects to the `.onion` address through Tor and
174
+ delivers a passphrase to a dropbear stand-in, asserting it arrives and the
175
+ endpoint "unlocks".
176
+
177
+ It needs the real `tor` binary and live Tor network access (onion bootstrap is
178
+ slow), so it is opt-in and skipped otherwise:
179
+
180
+ ```bash
181
+ LIM_E2E_TOR=1 pytest tests/e2e/test_tor_unlock_e2e.py -v
182
+ ```
183
+
184
+ The offline-only checks in that file (onion keygen, production-flag guard) run
185
+ without network. The physical flashing half (`dd`, `cryptsetup luksFormat`,
186
+ `mount`, `chroot`, `mkinitcpio`) requires root and a matching CPU/qemu setup and
187
+ is not covered by this rootless test.
188
+
189
+ ### Full virtualized unlock (QEMU)
190
+
191
+ For the *whole* process — build an encrypted image, boot it, run the real
192
+ initramfs Tor+dropbear chain, and unlock it — `tests/e2e/qemu/` drives a QEMU
193
+ virtual machine end to end and asserts the real system booted only after the
194
+ passphrase was delivered over Tor. It is opt-in (`LIM_E2E_QEMU=1`) and needs
195
+ QEMU, `arch-install-scripts`, `cryptsetup`, `tor` and `ncat`; the build stage
196
+ needs root (ideally inside a throwaway VM to keep the host rootless). A private,
197
+ offline Tor network via [chutney](https://gitlab.torproject.org/tpo/core/chutney)
198
+ is supported. See [tests/e2e/qemu/README.md](tests/e2e/qemu/README.md).
199
+
200
+ The harness's pure logic and drift guards run in the normal suite
201
+ (`test_qemu_harness_unit.py`) — no QEMU, root, or network needed.
202
+
132
203
  ## License 📜
133
204
 
134
205
  This project is licensed under the GNU General Public License Version 3. See the [LICENSE.txt](./LICENSE.txt) file for details.
@@ -10,6 +10,7 @@ Linux Image Manager (lim) is a Python tool for downloading, configuring, and man
10
10
 
11
11
  - **Image Download & Setup:** Automatically download, verify (checksum + GPG signature) and prepare Linux distributions.
12
12
  - **Encrypted Storage:** Configure LUKS encryption for secure image management.
13
+ - **Tor Onion Unlock:** Optionally bake a Tor onion service into the initramfs so the dropbear LUKS unlock shell stays reachable behind NAT or dynamic IPs.
13
14
  - **Virtual RAID1:** Easily set up virtual Btrfs RAID1 for data redundancy.
14
15
  - **Backup & Restore:** Create image backups from devices using dd.
15
16
  - **Chroot Environment:** Easily enter a chroot shell to maintain or modify Linux images.
@@ -26,7 +27,7 @@ package-manager install lim
26
27
 
27
28
  This command makes Linux Image Manager globally available as `lim` in your terminal. The `lim` alias points to the **main.py** entry point.
28
29
 
29
- There are no Python dependencies beyond the standard library. The commands call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
30
+ The only Python dependency is **PyYAML** (`>=6`), used to read the image catalog. The pip/wheel install pulls it in automatically; on the `package-manager`/symlink install path make sure it is present (`python-yaml` on Arch). The commands also call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
30
31
 
31
32
  ## Usage ⚙️
32
33
 
@@ -94,6 +95,8 @@ lim/
94
95
  distributions.yml # single point of truth for the image catalog
95
96
  configuration/ # package collections used during image setup
96
97
  tests/unit/ # unit tests (all external commands mocked)
98
+ tests/e2e/ # Tor onion unlock end-to-end tests (rootless + QEMU, opt-in)
99
+ tests/e2e/qemu/ # full virtualized build+boot+unlock harness
97
100
  tests/lint/ # architecture guards (e.g. max file length)
98
101
  ```
99
102
 
@@ -102,15 +105,83 @@ tests/lint/ # architecture guards (e.g. max file length)
102
105
  Customize your environment in the `lim/configuration/` folder:
103
106
  - **General Packages:** Contains common packages for all setup scripts.
104
107
  - **Server LUKS Packages:** Contains packages needed for setting up LUKS encryption on servers.
108
+ - **initcpio Hooks:** The mkinitcpio install/runtime hooks and torrc baked into images for the Tor onion unlock.
109
+
110
+ ## Remote LUKS Unlock via Tor 🧅
111
+
112
+ When you answer yes to *"Should the system be remotely unlockable via a Tor
113
+ onion service?"* during an encrypted `lim --type image` setup, the image gets:
114
+
115
+ - `tor` and `busybox` installed, plus a custom `tor` mkinitcpio hook ordered
116
+ between `netconf` and `dropbear`.
117
+ - A v3 onion service key generated **offline** inside the image chroot; the
118
+ resulting `.onion` address is printed at the end of the setup and stays
119
+ stable across reboots and IP changes.
120
+ - An NTP clock sync in early userspace (boards like the Raspberry Pi have no
121
+ RTC and would otherwise boot with a clock Tor rejects). Override the server
122
+ with the kernel parameter `tor_ntp=<host>`.
123
+
124
+ After booting the device, unlock it from any machine with a running Tor
125
+ client:
126
+
127
+ ```bash
128
+ torsocks ssh root@<onion-address>
129
+ ```
130
+
131
+ Typing the LUKS passphrase into that shell resumes the boot. The direct
132
+ `ssh root@<ip>` unlock keeps working as before; Tor is an additional path,
133
+ useful behind NAT/CGNAT where no port forwarding is possible.
134
+
135
+ **Security note:** the onion private key is stored in the initramfs on the
136
+ unencrypted boot partition. Anyone with physical access to the SD card can
137
+ read it and impersonate the onion service — treat the address as
138
+ non-secret. Access control remains the SSH public key
139
+ (`/etc/dropbear/root_key`), exactly as with the direct unlock.
105
140
 
106
141
  ## Development & Tests 🧪
107
142
 
108
- The test suite mocks all external commands, so it runs safely on any machine:
143
+ The unit suite mocks all external commands, so it runs safely on any machine:
109
144
 
110
145
  ```bash
111
146
  pytest
112
147
  ```
113
148
 
149
+ ### Tor unlock end-to-end test
150
+
151
+ `tests/e2e/test_tor_unlock_e2e.py` reproduces the operator-visible half of the
152
+ Tor decryption process **rootless** — no block devices, no `cryptsetup`, no
153
+ `chroot`. It generates the v3 onion keys offline (exactly as `lim.image.tor`
154
+ does inside the image), starts a real Tor onion service from a torrc mirroring
155
+ the baked-in one, then connects to the `.onion` address through Tor and
156
+ delivers a passphrase to a dropbear stand-in, asserting it arrives and the
157
+ endpoint "unlocks".
158
+
159
+ It needs the real `tor` binary and live Tor network access (onion bootstrap is
160
+ slow), so it is opt-in and skipped otherwise:
161
+
162
+ ```bash
163
+ LIM_E2E_TOR=1 pytest tests/e2e/test_tor_unlock_e2e.py -v
164
+ ```
165
+
166
+ The offline-only checks in that file (onion keygen, production-flag guard) run
167
+ without network. The physical flashing half (`dd`, `cryptsetup luksFormat`,
168
+ `mount`, `chroot`, `mkinitcpio`) requires root and a matching CPU/qemu setup and
169
+ is not covered by this rootless test.
170
+
171
+ ### Full virtualized unlock (QEMU)
172
+
173
+ For the *whole* process — build an encrypted image, boot it, run the real
174
+ initramfs Tor+dropbear chain, and unlock it — `tests/e2e/qemu/` drives a QEMU
175
+ virtual machine end to end and asserts the real system booted only after the
176
+ passphrase was delivered over Tor. It is opt-in (`LIM_E2E_QEMU=1`) and needs
177
+ QEMU, `arch-install-scripts`, `cryptsetup`, `tor` and `ncat`; the build stage
178
+ needs root (ideally inside a throwaway VM to keep the host rootless). A private,
179
+ offline Tor network via [chutney](https://gitlab.torproject.org/tpo/core/chutney)
180
+ is supported. See [tests/e2e/qemu/README.md](tests/e2e/qemu/README.md).
181
+
182
+ The harness's pure logic and drift guards run in the normal suite
183
+ (`test_qemu_harness_unit.py`) — no QEMU, root, or network needed.
184
+
114
185
  ## License 📜
115
186
 
116
187
  This project is licensed under the GNU General Public License Version 3. See the [LICENSE.txt](./LICENSE.txt) file for details.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/ash
2
+ # mkinitcpio runtime hook: start Tor so the dropbear unlock shell is
3
+ # reachable as an onion service while encryptssh waits for the passphrase.
4
+ # Installed to /etc/initcpio/hooks/tor by linux-image-manager.
5
+
6
+ # netconf's ipconfig drops its DHCP lease data (incl. DNS) into
7
+ # /tmp/net-*.conf; busybox's resolver only reads /etc/resolv.conf.
8
+ _tor_write_resolv_conf() {
9
+ [ -s /etc/resolv.conf ] && return 0
10
+ local conf dns
11
+ for conf in /tmp/net-*.conf; do
12
+ [ -f "$conf" ] || continue
13
+ # Extract ONLY the DNS fields with sed; never source these files —
14
+ # they hold attacker-controllable DHCP option strings (hostname,
15
+ # domain, rootpath), and sourcing would run them as root pre-boot.
16
+ for dns in $(sed -n 's/^IPV4DNS[01]=//p' "$conf"); do
17
+ [ -n "$dns" ] && [ "$dns" != "0.0.0.0" ] \
18
+ && echo "nameserver $dns" >> /etc/resolv.conf
19
+ done
20
+ done
21
+ [ -s /etc/resolv.conf ]
22
+ }
23
+
24
+ run_hook() {
25
+ # Tor rejects consensus documents when the clock is far off; boards
26
+ # without an RTC boot in 1970, so sync before starting Tor. Bounded:
27
+ # a failed sync must never block the boot.
28
+ msg "tor: syncing clock via NTP..."
29
+ # Best-effort DNS; a tor_ntp IP literal needs none, so never gate on it.
30
+ _tor_write_resolv_conf || msg "tor: no DNS from DHCP (fine if tor_ntp is an IP)"
31
+ /usr/local/bin/busybox timeout 30 \
32
+ /usr/local/bin/busybox ntpd -n -q -p "${tor_ntp:-pool.ntp.org}" \
33
+ || msg "tor: NTP sync failed, keeping current clock"
34
+
35
+ msg "tor: starting onion service for remote unlock..."
36
+ mkdir -p /var/lib/tor
37
+ chmod 0700 /var/lib/tor /etc/tor/onion
38
+ chmod 0600 /etc/tor/onion/hs_ed25519_secret_key
39
+ tor -f /etc/tor/torrc --RunAsDaemon 1 --Log "notice file /tmp/tor.log" \
40
+ || msg "tor: failed to start, unlock stays reachable via direct IP"
41
+ }
42
+
43
+ run_cleanuphook() {
44
+ # Nothing from the initramfs may keep running after the pivot.
45
+ /usr/local/bin/busybox killall tor 2>/dev/null
46
+ return 0
47
+ }
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # mkinitcpio install hook: Tor onion service for remote LUKS unlock.
3
+ # Installed to /etc/initcpio/install/tor by linux-image-manager.
4
+
5
+ build() {
6
+ add_binary /usr/bin/tor
7
+ # Full busybox for the ntpd applet: boards without an RTC (e.g. most
8
+ # Raspberry Pis) wake up in 1970, which Tor's consensus checks reject.
9
+ # A different target path keeps mkinitcpio's own busybox untouched.
10
+ add_binary /usr/bin/busybox /usr/local/bin/busybox
11
+ # glibc resolves the NTP server hostname via getaddrinfo(), which dlopen()s
12
+ # these NSS modules at runtime — add_binary only follows NEEDED libs, so
13
+ # without them DNS silently fails, ntpd never syncs, and the clock stays
14
+ # at 1970. glibc's built-in default (no nsswitch.conf) is "dns ... files".
15
+ add_binary /usr/lib/libnss_dns.so.2
16
+ add_binary /usr/lib/libnss_files.so.2
17
+ add_file /etc/tor/initramfs-torrc /etc/tor/torrc
18
+ add_file /etc/tor/initramfs-onion/hostname /etc/tor/onion/hostname
19
+ add_file /etc/tor/initramfs-onion/hs_ed25519_public_key /etc/tor/onion/hs_ed25519_public_key
20
+ add_file /etc/tor/initramfs-onion/hs_ed25519_secret_key /etc/tor/onion/hs_ed25519_secret_key
21
+ add_runscript
22
+ }
23
+
24
+ help() {
25
+ cat <<HELPEOF
26
+ Starts a Tor onion service in early userspace so the dropbear unlock
27
+ shell stays reachable under the .onion address baked into the image,
28
+ even behind NAT or a dynamic IP.
29
+
30
+ Place it between netconf and dropbear:
31
+ HOOKS=(... netconf tor dropbear encryptssh ...)
32
+
33
+ Optional kernel parameter:
34
+ tor_ntp=<server> NTP server used to set the clock before Tor starts
35
+ (default: pool.ntp.org)
36
+ HELPEOF
37
+ }
@@ -0,0 +1,7 @@
1
+ # Tor configuration for the initramfs onion unlock service.
2
+ # Baked into the initramfs as /etc/tor/torrc by the "tor" mkinitcpio hook;
3
+ # the onion keys come from /etc/tor/initramfs-onion on the system.
4
+ DataDirectory /var/lib/tor
5
+ HiddenServiceDir /etc/tor/onion
6
+ HiddenServicePort 22 127.0.0.1:22
7
+ SocksPort 0
@@ -0,0 +1,3 @@
1
+ # Packages for the Tor onion unlock service in the initramfs
2
+ tor
3
+ busybox # full applet set: ntpd for clock sync on RTC-less boards
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from lim import catalog, fsutil, packages, runner, ui
10
10
  from lim.image.plan import ImagePlan
11
11
  from lim.image.session import ImageSession, chroot_bash, install_packages
12
+ from lim.image.tor import configure_tor_unlock
12
13
 
13
14
  MKINITCPIO_HOOKS_PREFIX = (
14
15
  "base udev autodetect microcode modconf kms keyboard keymap consolefont block"
@@ -32,9 +33,10 @@ def _configure_mkinitcpio(plan: ImagePlan, root: Path) -> None:
32
33
  fsutil.replace_in_file(
33
34
  "BINARIES=()", "BINARIES=(/usr/lib/libgcc_s.so.1)", mkinitcpio_path
34
35
  )
36
+ tor_hook = "tor " if plan.tor_unlock else ""
35
37
  fsutil.replace_in_file(
36
38
  f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} {MKINITCPIO_HOOKS_SUFFIX})",
37
- f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} sleep netconf dropbear encryptssh "
39
+ f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} sleep netconf {tor_hook}dropbear encryptssh "
38
40
  f"{MKINITCPIO_HOOKS_SUFFIX})",
39
41
  mkinitcpio_path,
40
42
  )
@@ -90,9 +92,15 @@ def _configure_bootloader(plan: ImagePlan, session: ImageSession, root: Path) ->
90
92
  else:
91
93
  cmdline_txt_path = session.boot_mount_path / "cmdline.txt"
92
94
  ui.info(f"Configuring {cmdline_txt_path}...")
95
+ # Firmware-boot boards (e.g. RPi4 cmdline.txt) need the same early
96
+ # networking as the boot.txt branch, or the initramfs netconf hook
97
+ # brings up no interface and remote unlock (dropbear + tor) is
98
+ # unreachable — the onion never even publishes.
99
+ hostname = (root / "etc/hostname").read_text().strip()
93
100
  fsutil.replace_in_file(
94
101
  "root=/dev/mmcblk0p2",
95
- f"{cryptdevice} rootfstype={plan.root_filesystem}",
102
+ f"{cryptdevice} rootfstype={plan.root_filesystem} "
103
+ f"ip=::::{hostname}:eth0:dhcp net.ifnames=0 biosdevname=0",
96
104
  cmdline_txt_path,
97
105
  )
98
106
  ui.info(f"Content of {cmdline_txt_path}:{cmdline_txt_path.read_text()}")
@@ -112,6 +120,10 @@ def configure_encryption(
112
120
  ui.info(f"Adding {authorized_keys} to dropbear...")
113
121
  runner.run(["cp", "-v", str(authorized_keys), str(dropbear_root_key_path)], sudo=True)
114
122
 
123
+ if plan.tor_unlock:
124
+ # Hook files and onion keys must exist before mkinitcpio bakes the image.
125
+ configure_tor_unlock(plan, root)
126
+
115
127
  _configure_mkinitcpio(plan, root)
116
128
  _register_encrypted_root(plan, session, root)
117
129
  _configure_bootloader(plan, session, root)
@@ -17,6 +17,7 @@ class ImagePlan:
17
17
  luks_memory_cost: str | None = None
18
18
  raspberry_pi_version: str | None = None
19
19
  encrypt_system: bool = False
20
+ tor_unlock: bool = False
20
21
  root_filesystem: str = ""
21
22
  image_folder: Path = field(default_factory=Path)
22
23
 
@@ -49,6 +49,10 @@ def _choose_and_verify_image(plan: ImagePlan) -> None:
49
49
  if plan.operation_system == "linux":
50
50
  choosers.choose_linux_image(plan)
51
51
  plan.encrypt_system = ui.confirm("Should the system be encrypted?")
52
+ if plan.encrypt_system:
53
+ plan.tor_unlock = ui.confirm(
54
+ "Should the system be remotely unlockable via a Tor onion service?"
55
+ )
52
56
  ui.info("Generating os-image...")
53
57
  transfer.download_image(plan)
54
58
  else:
@@ -0,0 +1,79 @@
1
+ """Tor onion service in the initramfs for remote LUKS unlock.
2
+
3
+ The onion keys are generated offline inside the image chroot
4
+ (``tor --DisableNetwork 1`` writes them without touching the network)
5
+ and baked into the initramfs by the "tor" mkinitcpio hook, so the
6
+ dropbear unlock shell stays reachable under a stable .onion address
7
+ even behind NAT or a dynamic IP.
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ from lim import config, packages, runner, ui
13
+ from lim.errors import LimError
14
+ from lim.image.plan import ImagePlan
15
+ from lim.image.session import chroot_bash, install_packages
16
+
17
+ # Paths inside the image (relative to the mounted root).
18
+ ONION_DIR = "etc/tor/initramfs-onion"
19
+ TORRC_PATH = "etc/tor/initramfs-torrc"
20
+
21
+ # An empty -f torrc keeps the image's /etc/tor/torrc (User tor, ...) out of
22
+ # the keygen run; with DisableNetwork the keys appear within a second, the
23
+ # loop only cushions slow qemu-emulated chroots.
24
+ _KEYGEN_SCRIPT = f"""
25
+ mkdir -p /{ONION_DIR}
26
+ chmod 0700 /{ONION_DIR}
27
+ : > /tmp/tor-keygen-torrc
28
+ tor -f /tmp/tor-keygen-torrc --DisableNetwork 1 \\
29
+ --DataDirectory /tmp/tor-keygen-data \\
30
+ --HiddenServiceDir /{ONION_DIR} \\
31
+ --HiddenServicePort "22 127.0.0.1:22" \\
32
+ --SocksPort 0 --RunAsDaemon 0 --Log "notice stderr" &
33
+ tor_pid=$!
34
+ for _ in $(seq 1 30); do
35
+ [ -s /{ONION_DIR}/hostname ] && break
36
+ sleep 1
37
+ done
38
+ kill "$tor_pid" 2>/dev/null || true
39
+ rm -rf /tmp/tor-keygen-data /tmp/tor-keygen-torrc
40
+ [ -s /{ONION_DIR}/hostname ]
41
+ """
42
+
43
+
44
+ def _install_initcpio_files(root: Path) -> None:
45
+ source_dir = config.CONFIGURATION_PATH / "initcpio"
46
+ for source, target in (
47
+ (source_dir / "tor_install", root / "etc/initcpio/install/tor"),
48
+ (source_dir / "tor_hook", root / "etc/initcpio/hooks/tor"),
49
+ (source_dir / "torrc", root / TORRC_PATH),
50
+ ):
51
+ ui.info(f"Installing {target}...")
52
+ runner.run(
53
+ ["install", "-D", "-m", "0644", str(source), str(target)], sudo=True
54
+ )
55
+
56
+
57
+ def _generate_onion_keys(root: Path) -> str:
58
+ hostname_path = root / ONION_DIR / "hostname"
59
+ if hostname_path.is_file():
60
+ ui.info("Onion keys already exist, keeping the existing address.")
61
+ else:
62
+ ui.info("Generating onion service keys (offline, inside the chroot)...")
63
+ chroot_bash(root, _KEYGEN_SCRIPT, error_msg="Onion key generation failed.")
64
+ if not hostname_path.is_file():
65
+ raise LimError(f"Onion key generation produced no {hostname_path}.")
66
+ return hostname_path.read_text().strip()
67
+
68
+
69
+ def configure_tor_unlock(plan: ImagePlan, root: Path) -> str:
70
+ """Install everything the "tor" mkinitcpio hook bakes in; return the onion address."""
71
+ ui.info("Setting up remote unlock via Tor onion service...")
72
+ install_packages(
73
+ plan.distribution, root, " ".join(packages.get_packages("server/tor"))
74
+ )
75
+ _install_initcpio_files(root)
76
+ onion_address = _generate_onion_keys(root)
77
+ ui.success(f"Onion unlock address: {onion_address}")
78
+ ui.info(f"Unlock later with: torsocks ssh root@{onion_address}")
79
+ return onion_address
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linux-image-manager
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance
5
5
  Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
6
6
  Maintainer-email: Kevin Veen-Birkenbach <kevin@veen.world>
@@ -28,6 +28,7 @@ Linux Image Manager (lim) is a Python tool for downloading, configuring, and man
28
28
 
29
29
  - **Image Download & Setup:** Automatically download, verify (checksum + GPG signature) and prepare Linux distributions.
30
30
  - **Encrypted Storage:** Configure LUKS encryption for secure image management.
31
+ - **Tor Onion Unlock:** Optionally bake a Tor onion service into the initramfs so the dropbear LUKS unlock shell stays reachable behind NAT or dynamic IPs.
31
32
  - **Virtual RAID1:** Easily set up virtual Btrfs RAID1 for data redundancy.
32
33
  - **Backup & Restore:** Create image backups from devices using dd.
33
34
  - **Chroot Environment:** Easily enter a chroot shell to maintain or modify Linux images.
@@ -44,7 +45,7 @@ package-manager install lim
44
45
 
45
46
  This command makes Linux Image Manager globally available as `lim` in your terminal. The `lim` alias points to the **main.py** entry point.
46
47
 
47
- There are no Python dependencies beyond the standard library. The commands call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
48
+ The only Python dependency is **PyYAML** (`>=6`), used to read the image catalog. The pip/wheel install pulls it in automatically; on the `package-manager`/symlink install path make sure it is present (`python-yaml` on Arch). The commands also call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
48
49
 
49
50
  ## Usage ⚙️
50
51
 
@@ -112,6 +113,8 @@ lim/
112
113
  distributions.yml # single point of truth for the image catalog
113
114
  configuration/ # package collections used during image setup
114
115
  tests/unit/ # unit tests (all external commands mocked)
116
+ tests/e2e/ # Tor onion unlock end-to-end tests (rootless + QEMU, opt-in)
117
+ tests/e2e/qemu/ # full virtualized build+boot+unlock harness
115
118
  tests/lint/ # architecture guards (e.g. max file length)
116
119
  ```
117
120
 
@@ -120,15 +123,83 @@ tests/lint/ # architecture guards (e.g. max file length)
120
123
  Customize your environment in the `lim/configuration/` folder:
121
124
  - **General Packages:** Contains common packages for all setup scripts.
122
125
  - **Server LUKS Packages:** Contains packages needed for setting up LUKS encryption on servers.
126
+ - **initcpio Hooks:** The mkinitcpio install/runtime hooks and torrc baked into images for the Tor onion unlock.
127
+
128
+ ## Remote LUKS Unlock via Tor 🧅
129
+
130
+ When you answer yes to *"Should the system be remotely unlockable via a Tor
131
+ onion service?"* during an encrypted `lim --type image` setup, the image gets:
132
+
133
+ - `tor` and `busybox` installed, plus a custom `tor` mkinitcpio hook ordered
134
+ between `netconf` and `dropbear`.
135
+ - A v3 onion service key generated **offline** inside the image chroot; the
136
+ resulting `.onion` address is printed at the end of the setup and stays
137
+ stable across reboots and IP changes.
138
+ - An NTP clock sync in early userspace (boards like the Raspberry Pi have no
139
+ RTC and would otherwise boot with a clock Tor rejects). Override the server
140
+ with the kernel parameter `tor_ntp=<host>`.
141
+
142
+ After booting the device, unlock it from any machine with a running Tor
143
+ client:
144
+
145
+ ```bash
146
+ torsocks ssh root@<onion-address>
147
+ ```
148
+
149
+ Typing the LUKS passphrase into that shell resumes the boot. The direct
150
+ `ssh root@<ip>` unlock keeps working as before; Tor is an additional path,
151
+ useful behind NAT/CGNAT where no port forwarding is possible.
152
+
153
+ **Security note:** the onion private key is stored in the initramfs on the
154
+ unencrypted boot partition. Anyone with physical access to the SD card can
155
+ read it and impersonate the onion service — treat the address as
156
+ non-secret. Access control remains the SSH public key
157
+ (`/etc/dropbear/root_key`), exactly as with the direct unlock.
123
158
 
124
159
  ## Development & Tests 🧪
125
160
 
126
- The test suite mocks all external commands, so it runs safely on any machine:
161
+ The unit suite mocks all external commands, so it runs safely on any machine:
127
162
 
128
163
  ```bash
129
164
  pytest
130
165
  ```
131
166
 
167
+ ### Tor unlock end-to-end test
168
+
169
+ `tests/e2e/test_tor_unlock_e2e.py` reproduces the operator-visible half of the
170
+ Tor decryption process **rootless** — no block devices, no `cryptsetup`, no
171
+ `chroot`. It generates the v3 onion keys offline (exactly as `lim.image.tor`
172
+ does inside the image), starts a real Tor onion service from a torrc mirroring
173
+ the baked-in one, then connects to the `.onion` address through Tor and
174
+ delivers a passphrase to a dropbear stand-in, asserting it arrives and the
175
+ endpoint "unlocks".
176
+
177
+ It needs the real `tor` binary and live Tor network access (onion bootstrap is
178
+ slow), so it is opt-in and skipped otherwise:
179
+
180
+ ```bash
181
+ LIM_E2E_TOR=1 pytest tests/e2e/test_tor_unlock_e2e.py -v
182
+ ```
183
+
184
+ The offline-only checks in that file (onion keygen, production-flag guard) run
185
+ without network. The physical flashing half (`dd`, `cryptsetup luksFormat`,
186
+ `mount`, `chroot`, `mkinitcpio`) requires root and a matching CPU/qemu setup and
187
+ is not covered by this rootless test.
188
+
189
+ ### Full virtualized unlock (QEMU)
190
+
191
+ For the *whole* process — build an encrypted image, boot it, run the real
192
+ initramfs Tor+dropbear chain, and unlock it — `tests/e2e/qemu/` drives a QEMU
193
+ virtual machine end to end and asserts the real system booted only after the
194
+ passphrase was delivered over Tor. It is opt-in (`LIM_E2E_QEMU=1`) and needs
195
+ QEMU, `arch-install-scripts`, `cryptsetup`, `tor` and `ncat`; the build stage
196
+ needs root (ideally inside a throwaway VM to keep the host rootless). A private,
197
+ offline Tor network via [chutney](https://gitlab.torproject.org/tpo/core/chutney)
198
+ is supported. See [tests/e2e/qemu/README.md](tests/e2e/qemu/README.md).
199
+
200
+ The harness's pure logic and drift guards run in the normal suite
201
+ (`test_qemu_harness_unit.py`) — no QEMU, root, or network needed.
202
+
132
203
  ## License 📜
133
204
 
134
205
  This project is licensed under the GNU General Public License Version 3. See the [LICENSE.txt](./LICENSE.txt) file for details.
@@ -14,8 +14,12 @@ lim/packages.py
14
14
  lim/runner.py
15
15
  lim/system.py
16
16
  lim/ui.py
17
+ lim/configuration/initcpio/tor_hook
18
+ lim/configuration/initcpio/tor_install
19
+ lim/configuration/initcpio/torrc
17
20
  lim/configuration/packages/general.txt
18
21
  lim/configuration/packages/server/luks.txt
22
+ lim/configuration/packages/server/tor.txt
19
23
  lim/data/__init__.py
20
24
  lim/data/crypt.py
21
25
  lim/data/sync.py
@@ -28,6 +32,7 @@ lim/image/plan.py
28
32
  lim/image/raspberry.py
29
33
  lim/image/session.py
30
34
  lim/image/setup.py
35
+ lim/image/tor.py
31
36
  lim/image/transfer.py
32
37
  lim/image/verify.py
33
38
  lim/storage/__init__.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "linux-image-manager"
8
- version = "2.1.0"
8
+ version = "2.2.0"
9
9
  description = "CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -28,7 +28,11 @@ lim = "lim.cli:main"
28
28
  include = ["lim*"]
29
29
 
30
30
  [tool.setuptools.package-data]
31
- lim = ["distributions.yml", "configuration/packages/**/*.txt"]
31
+ lim = [
32
+ "distributions.yml",
33
+ "configuration/packages/**/*.txt",
34
+ "configuration/initcpio/*",
35
+ ]
32
36
 
33
37
  [tool.pytest.ini_options]
34
38
  testpaths = ["tests"]