linux-image-manager 2.1.0__tar.gz → 2.3.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.
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/PKG-INFO +74 -3
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/README.md +73 -2
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/catalog.py +4 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/cli.py +24 -22
- linux_image_manager-2.3.0/lim/configuration/initcpio/tor_hook +47 -0
- linux_image_manager-2.3.0/lim/configuration/initcpio/tor_install +37 -0
- linux_image_manager-2.3.0/lim/configuration/initcpio/torrc +7 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_bottom +19 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_hook +37 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_premount +55 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/torrc +16 -0
- linux_image_manager-2.3.0/lim/configuration/packages/server/luks-debian.txt +5 -0
- linux_image_manager-2.3.0/lim/configuration/packages/server/tor-debian.txt +3 -0
- linux_image_manager-2.3.0/lim/configuration/packages/server/tor.txt +3 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/data/sync.py +4 -13
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/device.py +1 -3
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/distributions.yml +13 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/fsutil.py +17 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/choosers.py +34 -19
- linux_image_manager-2.3.0/lim/image/crossarch.py +71 -0
- linux_image_manager-2.3.0/lim/image/encryption.py +42 -0
- linux_image_manager-2.3.0/lim/image/initramfs/__init__.py +21 -0
- linux_image_manager-2.3.0/lim/image/initramfs/base.py +42 -0
- linux_image_manager-2.3.0/lim/image/initramfs/initramfs_tools.py +117 -0
- linux_image_manager-2.3.0/lim/image/initramfs/keygen.py +54 -0
- linux_image_manager-2.3.0/lim/image/initramfs/mkinitcpio.py +122 -0
- linux_image_manager-2.3.0/lim/image/loopimg.py +28 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/plan.py +7 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/raspberry.py +3 -7
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/session.py +35 -19
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/setup.py +6 -5
- linux_image_manager-2.3.0/lim/image/transfer.py +236 -0
- linux_image_manager-2.3.0/lim/image/unlock.py +100 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/verify.py +2 -4
- linux_image_manager-2.3.0/lim/image/wizard.py +206 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/luks.py +3 -9
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/runner.py +2 -4
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/raid1.py +1 -3
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/single_drive.py +2 -6
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/PKG-INFO +74 -3
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/SOURCES.txt +19 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/pyproject.toml +7 -2
- linux_image_manager-2.1.0/lim/image/encryption.py +0 -117
- linux_image_manager-2.1.0/lim/image/transfer.py +0 -140
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/LICENSE.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/__init__.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/config.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/configuration/packages/general.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/configuration/packages/server/luks.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/data/__init__.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/data/crypt.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/errors.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/__init__.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/backup.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/chroot.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/packages.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/__init__.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/common.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/system.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/ui.py +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/dependency_links.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/entry_points.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/requires.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/top_level.txt +0 -0
- {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linux-image-manager
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
@@ -27,5 +27,9 @@ def retropie_images() -> dict[str, dict[str, str]]:
|
|
|
27
27
|
return _load()["retropie_images"]
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def raspios_images() -> dict[str, dict[str, str]]:
|
|
31
|
+
return _load()["raspios_images"]
|
|
32
|
+
|
|
33
|
+
|
|
30
34
|
def mkinitcpio_modules_by_rpi() -> dict[str, str]:
|
|
31
35
|
return _load()["mkinitcpio_modules_by_rpi"]
|
|
@@ -10,6 +10,8 @@ from lim.data import crypt, sync
|
|
|
10
10
|
from lim.errors import LimError
|
|
11
11
|
from lim.image import backup, chroot
|
|
12
12
|
from lim.image import setup as image_setup
|
|
13
|
+
from lim.image import unlock as image_unlock
|
|
14
|
+
from lim.image import wizard as image_wizard
|
|
13
15
|
from lim.storage import raid1, single_drive
|
|
14
16
|
|
|
15
17
|
|
|
@@ -29,6 +31,20 @@ COMMANDS: dict[str, Command] = {
|
|
|
29
31
|
" - Configures boot and root partitions.",
|
|
30
32
|
needs_root=True,
|
|
31
33
|
),
|
|
34
|
+
"guided": Command(
|
|
35
|
+
image_wizard.run_guided,
|
|
36
|
+
"Guided Encrypted Linux Image Setup (default command):\n"
|
|
37
|
+
" - Walks through flashing a Linux image (Arch, Raspberry Pi OS, ...) onto a device.\n"
|
|
38
|
+
" - Encrypts the root with LUKS and enables remote unlock over Tor.",
|
|
39
|
+
needs_root=True,
|
|
40
|
+
),
|
|
41
|
+
"remote-unlock": Command(
|
|
42
|
+
image_unlock.remote_unlock,
|
|
43
|
+
"Remote LUKS Boot Unlock:\n"
|
|
44
|
+
" - Reaches a waiting initramfs over Tor (onion) or plain SSH.\n"
|
|
45
|
+
" - Runs cryptroot-unlock or presents the passphrase prompt.",
|
|
46
|
+
needs_root=False,
|
|
47
|
+
),
|
|
32
48
|
"single": Command(
|
|
33
49
|
single_drive.setup,
|
|
34
50
|
"Single Drive Encryption Setup:\n"
|
|
@@ -59,14 +75,12 @@ COMMANDS: dict[str, Command] = {
|
|
|
59
75
|
),
|
|
60
76
|
"mount": Command(
|
|
61
77
|
single_drive.mount,
|
|
62
|
-
"Mount Encrypted Storage:\n"
|
|
63
|
-
" - Unlocks a LUKS partition and mounts it.",
|
|
78
|
+
"Mount Encrypted Storage:\n - Unlocks a LUKS partition and mounts it.",
|
|
64
79
|
needs_root=True,
|
|
65
80
|
),
|
|
66
81
|
"umount": Command(
|
|
67
82
|
single_drive.umount,
|
|
68
|
-
"Unmount Encrypted Storage:\n"
|
|
69
|
-
" - Unmounts a LUKS partition and closes the mapper.",
|
|
83
|
+
"Unmount Encrypted Storage:\n - Unmounts a LUKS partition and closes the mapper.",
|
|
70
84
|
needs_root=True,
|
|
71
85
|
),
|
|
72
86
|
"single-boot": Command(
|
|
@@ -83,26 +97,22 @@ COMMANDS: dict[str, Command] = {
|
|
|
83
97
|
),
|
|
84
98
|
"unlock": Command(
|
|
85
99
|
crypt.unlock,
|
|
86
|
-
"Unlock Data:\n"
|
|
87
|
-
" - Decrypts the encfs data store into the decrypted folder.",
|
|
100
|
+
"Unlock Data:\n - Decrypts the encfs data store into the decrypted folder.",
|
|
88
101
|
needs_root=False,
|
|
89
102
|
),
|
|
90
103
|
"lock": Command(
|
|
91
104
|
crypt.lock,
|
|
92
|
-
"Lock Data:\n"
|
|
93
|
-
" - Unmounts the decrypted encfs folder.",
|
|
105
|
+
"Lock Data:\n - Unmounts the decrypted encfs folder.",
|
|
94
106
|
needs_root=False,
|
|
95
107
|
),
|
|
96
108
|
"import": Command(
|
|
97
109
|
sync.import_from_system,
|
|
98
|
-
"Import Data:\n"
|
|
99
|
-
" - Copies personal data from the system into the encrypted store.",
|
|
110
|
+
"Import Data:\n - Copies personal data from the system into the encrypted store.",
|
|
100
111
|
needs_root=False,
|
|
101
112
|
),
|
|
102
113
|
"export": Command(
|
|
103
114
|
sync.export_to_system,
|
|
104
|
-
"Export Data:\n"
|
|
105
|
-
" - Copies personal data from the encrypted store back to the system.",
|
|
115
|
+
"Export Data:\n - Copies personal data from the encrypted store back to the system.",
|
|
106
116
|
needs_root=False,
|
|
107
117
|
),
|
|
108
118
|
}
|
|
@@ -122,21 +132,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
122
132
|
)
|
|
123
133
|
parser.add_argument(
|
|
124
134
|
"--type",
|
|
125
|
-
|
|
135
|
+
default="guided",
|
|
126
136
|
choices=list(COMMANDS.keys()),
|
|
127
|
-
help="
|
|
137
|
+
help="Command to execute (default: guided).",
|
|
128
138
|
)
|
|
129
139
|
parser.add_argument(
|
|
130
140
|
"--auto-confirm",
|
|
131
141
|
action="store_true",
|
|
132
142
|
help="Automatically confirm execution without prompting the user.",
|
|
133
143
|
)
|
|
134
|
-
parser.add_argument(
|
|
135
|
-
"--extra",
|
|
136
|
-
nargs=argparse.REMAINDER,
|
|
137
|
-
default=[],
|
|
138
|
-
help="Deprecated, ignored. Former extra parameters for the shell scripts.",
|
|
139
|
-
)
|
|
140
144
|
return parser
|
|
141
145
|
|
|
142
146
|
|
|
@@ -144,8 +148,6 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
144
148
|
args = build_parser().parse_args(argv)
|
|
145
149
|
command = COMMANDS[args.type]
|
|
146
150
|
|
|
147
|
-
if args.extra:
|
|
148
|
-
ui.warning("--extra is deprecated and ignored.")
|
|
149
151
|
if command.needs_root:
|
|
150
152
|
system.ensure_root()
|
|
151
153
|
|
|
@@ -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,19 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# initramfs-tools cleanup hook: stop Tor before the pivot to the real root.
|
|
3
|
+
# Installed to /etc/initramfs-tools/scripts/init-bottom/tor by lim.
|
|
4
|
+
# Nothing from the initramfs may keep running once run-init replaces it.
|
|
5
|
+
PREREQ=""
|
|
6
|
+
prereqs() { echo "$PREREQ"; }
|
|
7
|
+
case "$1" in
|
|
8
|
+
prereqs) prereqs; exit 0 ;;
|
|
9
|
+
esac
|
|
10
|
+
|
|
11
|
+
# pidof/killall may be absent; scan /proc for the tor binary (no extra tools).
|
|
12
|
+
for pid in $(ls /proc 2>/dev/null); do
|
|
13
|
+
case "$pid" in
|
|
14
|
+
*[!0-9]*) continue ;;
|
|
15
|
+
esac
|
|
16
|
+
[ "$(readlink "/proc/$pid/exe" 2>/dev/null)" = "/usr/bin/tor" ] \
|
|
17
|
+
&& kill "$pid" 2>/dev/null
|
|
18
|
+
done
|
|
19
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# initramfs-tools build hook: bake the Tor onion service into the initramfs.
|
|
3
|
+
# Installed to /etc/initramfs-tools/hooks/tor by linux-image-manager.
|
|
4
|
+
PREREQ="dropbear"
|
|
5
|
+
prereqs() { echo "$PREREQ"; }
|
|
6
|
+
case "$1" in
|
|
7
|
+
prereqs) prereqs; exit 0 ;;
|
|
8
|
+
esac
|
|
9
|
+
|
|
10
|
+
. /usr/share/initramfs-tools/hook-functions
|
|
11
|
+
|
|
12
|
+
copy_exec /usr/bin/tor /usr/bin
|
|
13
|
+
|
|
14
|
+
# glibc resolves the NTP server hostname via getaddrinfo(), which dlopen()s
|
|
15
|
+
# these NSS modules at runtime; without them DNS silently fails and the clock
|
|
16
|
+
# (RTC-less boards) stays at 1970, so Tor rejects the consensus.
|
|
17
|
+
for nss in /usr/lib/*/libnss_dns.so.2 /usr/lib/*/libnss_files.so.2 \
|
|
18
|
+
/lib/*/libnss_dns.so.2 /lib/*/libnss_files.so.2; do
|
|
19
|
+
[ -e "$nss" ] && copy_exec "$nss"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
# Full busybox for the ntpd applet (the initramfs busybox may lack it); a
|
|
23
|
+
# distinct path keeps the initramfs's own busybox untouched.
|
|
24
|
+
for bb in /bin/busybox /usr/bin/busybox; do
|
|
25
|
+
[ -x "$bb" ] && { copy_exec "$bb" /usr/local/bin/busybox; break; }
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
date -u +%s > "${DESTDIR}/etc/tor-clock-floor"
|
|
29
|
+
|
|
30
|
+
# Onion keys + torrc, staged on the system by lim.
|
|
31
|
+
mkdir -p "${DESTDIR}/etc/tor/onion"
|
|
32
|
+
for key in hostname hs_ed25519_public_key hs_ed25519_secret_key; do
|
|
33
|
+
cp -a "/etc/tor/initramfs-onion/${key}" "${DESTDIR}/etc/tor/onion/${key}"
|
|
34
|
+
done
|
|
35
|
+
chmod 0700 "${DESTDIR}/etc/tor/onion"
|
|
36
|
+
chmod 0600 "${DESTDIR}/etc/tor/onion/hs_ed25519_secret_key"
|
|
37
|
+
cp -a /etc/tor/initramfs-torrc "${DESTDIR}/etc/tor/torrc"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# initramfs-tools runtime hook: bring up networking, sync the clock and start
|
|
3
|
+
# the Tor onion service so the dropbear unlock shell is reachable while
|
|
4
|
+
# cryptsetup waits. Installed to /etc/initramfs-tools/scripts/init-premount/tor.
|
|
5
|
+
PREREQ=""
|
|
6
|
+
prereqs() { echo "$PREREQ"; }
|
|
7
|
+
case "$1" in
|
|
8
|
+
prereqs) prereqs; exit 0 ;;
|
|
9
|
+
esac
|
|
10
|
+
|
|
11
|
+
. /scripts/functions
|
|
12
|
+
|
|
13
|
+
log_begin_msg "tor: bringing up networking and starting the onion service"
|
|
14
|
+
|
|
15
|
+
# init-premount runs BEFORE the cryptroot/dropbear networking, so Tor would
|
|
16
|
+
# start with no network and never publish the onion. Bring the interface up
|
|
17
|
+
# ourselves from the ip= cmdline (idempotent; the later dropbear setup reuses
|
|
18
|
+
# the lease in /run/net-*.conf).
|
|
19
|
+
configure_networking
|
|
20
|
+
|
|
21
|
+
# initramfs-tools does not export arbitrary cmdline params as shell vars.
|
|
22
|
+
tor_ntp=$(sed -n 's/.*\btor_ntp=\([^ ]*\).*/\1/p' /proc/cmdline)
|
|
23
|
+
|
|
24
|
+
# Best-effort DNS from the DHCP lease; extract ONLY the DNS fields with sed,
|
|
25
|
+
# never source the files (attacker-controllable DHCP option strings would run
|
|
26
|
+
# as root pre-boot).
|
|
27
|
+
if [ ! -s /etc/resolv.conf ]; then
|
|
28
|
+
for conf in /run/net-*.conf /tmp/net-*.conf; do
|
|
29
|
+
[ -f "$conf" ] || continue
|
|
30
|
+
for dns in $(sed -n 's/^IPV4DNS[01]=//p' "$conf"); do
|
|
31
|
+
[ -n "$dns" ] && [ "$dns" != "0.0.0.0" ] \
|
|
32
|
+
&& echo "nameserver $dns" >> /etc/resolv.conf
|
|
33
|
+
done
|
|
34
|
+
done
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
floor=$(cat /etc/tor-clock-floor 2>/dev/null || echo 0)
|
|
38
|
+
if [ "$(date +%s)" -lt "$floor" ]; then
|
|
39
|
+
/usr/local/bin/busybox date -s "@$floor" >/dev/null 2>&1 \
|
|
40
|
+
|| date -s "@$floor" >/dev/null 2>&1 || true
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ -x /usr/local/bin/busybox ]; then
|
|
44
|
+
/usr/local/bin/busybox timeout 30 \
|
|
45
|
+
/usr/local/bin/busybox ntpd -n -q -p "${tor_ntp:-pool.ntp.org}" \
|
|
46
|
+
|| log_warning_msg "tor: NTP sync failed, keeping current clock"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
mkdir -p /var/lib/tor
|
|
50
|
+
chmod 0700 /var/lib/tor /etc/tor/onion
|
|
51
|
+
chmod 0600 /etc/tor/onion/hs_ed25519_secret_key
|
|
52
|
+
tor -f /etc/tor/torrc --RunAsDaemon 1 --Log "notice file /run/tor.log" \
|
|
53
|
+
|| log_warning_msg "tor: failed to start, unlock stays reachable via direct IP"
|
|
54
|
+
|
|
55
|
+
log_end_msg
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Tor configuration for the initramfs onion unlock service (initramfs-tools).
|
|
2
|
+
# Baked into the initramfs as /etc/tor/torrc by the "tor" hook; the onion keys
|
|
3
|
+
# 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
|
|
8
|
+
# Single-hop, non-anonymous onion service. A boot-unlock onion needs
|
|
9
|
+
# reachability (behind NAT / a dynamic IP), NOT server-location anonymity, so
|
|
10
|
+
# it can skip the multi-hop, vanguards-restricted circuits that a fresh Tor
|
|
11
|
+
# with a limited relay view fails to build ("Giving up on launching a
|
|
12
|
+
# rendezvous circuit"). Single-hop builds trivial 1-hop circuits that publish
|
|
13
|
+
# and rendezvous reliably. Entry guards are likewise unnecessary here.
|
|
14
|
+
HiddenServiceNonAnonymousMode 1
|
|
15
|
+
HiddenServiceSingleHopMode 1
|
|
16
|
+
UseEntryGuards 0
|
|
@@ -90,31 +90,22 @@ def execute_sync_plan(operations: list[SyncOperation]) -> None:
|
|
|
90
90
|
if not operation.is_directory and Path(operation.destination).is_file():
|
|
91
91
|
ui.info("The destination file already exists!")
|
|
92
92
|
ui.info("Difference:")
|
|
93
|
-
runner.run(
|
|
94
|
-
["diff", operation.destination, operation.source], check=False
|
|
95
|
-
)
|
|
93
|
+
runner.run(["diff", operation.destination, operation.source], check=False)
|
|
96
94
|
runner.run(rsync_command(operation))
|
|
97
95
|
|
|
98
96
|
|
|
99
97
|
def ensure_unlocked() -> None:
|
|
100
98
|
mounts = runner.output(["mount"], check=False)
|
|
101
99
|
if str(config.DECRYPTED_PATH) not in mounts:
|
|
102
|
-
ui.info(
|
|
103
|
-
f"The decrypted folder {config.DECRYPTED_PATH} is locked. "
|
|
104
|
-
"You need to unlock it!"
|
|
105
|
-
)
|
|
100
|
+
ui.info(f"The decrypted folder {config.DECRYPTED_PATH} is locked. You need to unlock it!")
|
|
106
101
|
crypt.unlock()
|
|
107
102
|
|
|
108
103
|
|
|
109
104
|
def import_from_system(mode: str = "import") -> None:
|
|
110
105
|
ensure_unlocked()
|
|
111
|
-
backup_folder = (
|
|
112
|
-
config.BACKUP_PATH / mode / time.strftime("%Y%m%d%H%M%S")
|
|
113
|
-
)
|
|
106
|
+
backup_folder = config.BACKUP_PATH / mode / time.strftime("%Y%m%d%H%M%S")
|
|
114
107
|
backup_folder.mkdir(parents=True, exist_ok=True)
|
|
115
|
-
operations = build_sync_plan(
|
|
116
|
-
mode, system.real_home(), config.DATA_PATH, backup_folder
|
|
117
|
-
)
|
|
108
|
+
operations = build_sync_plan(mode, system.real_home(), config.DATA_PATH, backup_folder)
|
|
118
109
|
execute_sync_plan(operations)
|
|
119
110
|
|
|
120
111
|
|
|
@@ -119,9 +119,7 @@ def overwrite_device(device: Device) -> None:
|
|
|
119
119
|
|
|
120
120
|
def blkid_value(path: str, tag: str) -> str:
|
|
121
121
|
"""Return a blkid tag value ("TYPE", "UUID", ...) or "" when unavailable."""
|
|
122
|
-
return runner.output(
|
|
123
|
-
["blkid", path, "-s", tag, "-o", "value"], sudo=True, check=False
|
|
124
|
-
)
|
|
122
|
+
return runner.output(["blkid", path, "-s", tag, "-o", "value"], sudo=True, check=False)
|
|
125
123
|
|
|
126
124
|
|
|
127
125
|
def is_mounted(path_fragment: str) -> bool:
|