linux-image-manager 2.3.0__tar.gz → 2.4.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 (64) hide show
  1. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/PKG-INFO +1 -1
  2. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/packages/server/luks.txt +1 -0
  3. linux_image_manager-2.4.0/lim/image/hostkeys.py +77 -0
  4. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/initramfs/base.py +4 -0
  5. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/initramfs/initramfs_tools.py +19 -0
  6. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/initramfs/mkinitcpio.py +6 -1
  7. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/unlock.py +21 -12
  8. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/wizard.py +30 -9
  9. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/linux_image_manager.egg-info/PKG-INFO +1 -1
  10. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/linux_image_manager.egg-info/SOURCES.txt +1 -0
  11. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/pyproject.toml +1 -1
  12. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/LICENSE.txt +0 -0
  13. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/README.md +0 -0
  14. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/__init__.py +0 -0
  15. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/catalog.py +0 -0
  16. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/cli.py +0 -0
  17. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/config.py +0 -0
  18. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initcpio/tor_hook +0 -0
  19. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initcpio/tor_install +0 -0
  20. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initcpio/torrc +0 -0
  21. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initramfs-tools/tor_bottom +0 -0
  22. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initramfs-tools/tor_hook +0 -0
  23. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initramfs-tools/tor_premount +0 -0
  24. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/initramfs-tools/torrc +0 -0
  25. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/packages/general.txt +0 -0
  26. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/packages/server/luks-debian.txt +0 -0
  27. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/packages/server/tor-debian.txt +0 -0
  28. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/configuration/packages/server/tor.txt +0 -0
  29. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/data/__init__.py +0 -0
  30. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/data/crypt.py +0 -0
  31. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/data/sync.py +0 -0
  32. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/device.py +0 -0
  33. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/distributions.yml +0 -0
  34. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/errors.py +0 -0
  35. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/fsutil.py +0 -0
  36. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/__init__.py +0 -0
  37. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/backup.py +0 -0
  38. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/choosers.py +0 -0
  39. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/chroot.py +0 -0
  40. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/crossarch.py +0 -0
  41. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/encryption.py +0 -0
  42. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/initramfs/__init__.py +0 -0
  43. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/initramfs/keygen.py +0 -0
  44. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/loopimg.py +0 -0
  45. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/plan.py +0 -0
  46. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/raspberry.py +0 -0
  47. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/session.py +0 -0
  48. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/setup.py +0 -0
  49. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/transfer.py +0 -0
  50. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/image/verify.py +0 -0
  51. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/luks.py +0 -0
  52. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/packages.py +0 -0
  53. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/runner.py +0 -0
  54. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/storage/__init__.py +0 -0
  55. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/storage/common.py +0 -0
  56. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/storage/raid1.py +0 -0
  57. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/storage/single_drive.py +0 -0
  58. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/system.py +0 -0
  59. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/lim/ui.py +0 -0
  60. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/linux_image_manager.egg-info/dependency_links.txt +0 -0
  61. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/linux_image_manager.egg-info/entry_points.txt +0 -0
  62. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/linux_image_manager.egg-info/requires.txt +0 -0
  63. {linux_image_manager-2.3.0 → linux_image_manager-2.4.0}/linux_image_manager.egg-info/top_level.txt +0 -0
  64. {linux_image_manager-2.3.0 → linux_image_manager-2.4.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.0
3
+ Version: 2.4.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>
@@ -6,3 +6,4 @@ dropbear
6
6
  mkinitcpio-utils
7
7
  mkinitcpio-netconf
8
8
  mkinitcpio-dropbear
9
+ openssh
@@ -0,0 +1,77 @@
1
+ import os
2
+ import pwd
3
+ import re
4
+ from pathlib import Path
5
+
6
+ KNOWN_HOSTS_SUBPATH = ".config/lim/known_hosts"
7
+ SSH_CONFIG_SUBPATH = ".ssh/config"
8
+
9
+
10
+ def host_key_alias(name: str) -> str:
11
+ return f"boot.{name}"
12
+
13
+
14
+ def known_hosts_path(home: Path) -> Path:
15
+ return home / KNOWN_HOSTS_SUBPATH
16
+
17
+
18
+ def replace_known_hosts(text: str, alias: str, public_keys: list[str]) -> str:
19
+ kept = [line for line in text.splitlines() if line.split(" ", 1)[0] != alias]
20
+ pinned = [f"{alias} {' '.join(key.split()[:2])}" for key in public_keys]
21
+ return "\n".join([*kept, *pinned]) + "\n"
22
+
23
+
24
+ def replace_config_block(text: str, alias: str, block: str) -> str:
25
+ begin, end = f"# lim:{alias}:begin", f"# lim:{alias}:end"
26
+ pattern = rf"^{re.escape(begin)}\n.*?^{re.escape(end)}\n?"
27
+ kept = re.sub(pattern, "", text, flags=re.MULTILINE | re.DOTALL)
28
+ if kept and not kept.endswith("\n"):
29
+ kept += "\n"
30
+ return f"{kept}{begin}\n{block}{end}\n"
31
+
32
+
33
+ def config_block(alias: str, target: str, identity_file: str, known_hosts: Path) -> str:
34
+ lines = [
35
+ f"Host {alias}",
36
+ f" HostName {target}",
37
+ " User root",
38
+ f" HostKeyAlias {alias}",
39
+ f" UserKnownHostsFile {known_hosts}",
40
+ " StrictHostKeyChecking yes",
41
+ ]
42
+ if identity_file:
43
+ lines.append(f" IdentityFile {identity_file}")
44
+ return "\n".join(lines) + "\n"
45
+
46
+
47
+ def _write_owned(path: Path, text: str, owner: pwd.struct_passwd) -> None:
48
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
49
+ path.write_text(text)
50
+ path.chmod(0o600)
51
+ for entry in (path, path.parent):
52
+ os.chown(entry, owner.pw_uid, owner.pw_gid)
53
+
54
+
55
+ def pin(
56
+ owner: pwd.struct_passwd, name: str, target: str, identity_file: str, public_keys: list[str]
57
+ ) -> str:
58
+ """Write the pinned keys and a ``Host boot.<name>`` ssh config block; return the alias.
59
+
60
+ Args:
61
+ owner: passwd entry of the operator whose home receives the files.
62
+ name: unlock record name; the alias becomes ``boot.<name>``.
63
+ target: onion address or host/IP the initramfs answers on.
64
+ identity_file: private key path for the unlock, empty for ssh defaults.
65
+ public_keys: OpenSSH ``type base64 [comment]`` lines the initramfs serves.
66
+
67
+ """
68
+ home = Path(owner.pw_dir)
69
+ alias = host_key_alias(name)
70
+ known_hosts = known_hosts_path(home)
71
+ current = known_hosts.read_text() if known_hosts.is_file() else ""
72
+ _write_owned(known_hosts, replace_known_hosts(current, alias, public_keys), owner)
73
+ ssh_config = home / SSH_CONFIG_SUBPATH
74
+ current = ssh_config.read_text() if ssh_config.is_file() else ""
75
+ block = config_block(alias, target, identity_file, known_hosts)
76
+ _write_owned(ssh_config, replace_config_block(current, alias, block), owner)
77
+ return alias
@@ -40,3 +40,7 @@ class InitramfsBackend(ABC):
40
40
  @abstractmethod
41
41
  def configure_bootloader(self, plan: ImagePlan, session: ImageSession, root: Path) -> None:
42
42
  """Point the bootloader at the encrypted root with early networking."""
43
+
44
+ @abstractmethod
45
+ def host_public_keys(self, root: Path) -> list[str]:
46
+ """OpenSSH public key lines of the host keys baked into the initramfs."""
@@ -22,6 +22,7 @@ from lim.image.session import ImageSession, chroot_bash, install_packages
22
22
 
23
23
  # Numeric IP, not a hostname: the initramfs has no DNS resolver.
24
24
  _DEFAULT_NTP = "162.159.200.123"
25
+ _DROPBEAR_KEY_DIRS = ("etc/dropbear/initramfs", "etc/dropbear-initramfs")
25
26
 
26
27
 
27
28
  class InitramfsToolsBackend(InitramfsBackend):
@@ -115,3 +116,21 @@ class InitramfsToolsBackend(InitramfsBackend):
115
116
  text = f"{text} tor_ntp={_DEFAULT_NTP}"
116
117
  cmdline_txt.write_text(text + "\n")
117
118
  ui.info(f"Content of {cmdline_txt}:{cmdline_txt.read_text()}")
119
+
120
+ def host_public_keys(self, root: Path) -> list[str]:
121
+ key_paths = [
122
+ f"{key_dir}/dropbear_{kind}_host_key"
123
+ for key_dir in _DROPBEAR_KEY_DIRS
124
+ for kind in ("ed25519", "ecdsa", "rsa")
125
+ ]
126
+ printed = [
127
+ runner.output(["chroot", str(root), "dropbearkey", "-y", "-f", f"/{path}"], sudo=True)
128
+ for path in key_paths
129
+ if (root / path).is_file()
130
+ ]
131
+ return [
132
+ line
133
+ for text in printed
134
+ for line in text.splitlines()
135
+ if line.startswith(("ssh-", "ecdsa-"))
136
+ ]
@@ -16,6 +16,7 @@ MKINITCPIO_HOOKS_PREFIX = (
16
16
  "base udev autodetect microcode modconf kms keyboard keymap consolefont block"
17
17
  )
18
18
  MKINITCPIO_HOOKS_SUFFIX = "filesystems fsck"
19
+ DROPBEAR_KEY_TYPES = ("ecdsa", "rsa")
19
20
 
20
21
 
21
22
  class MkinitcpioBackend(InitramfsBackend):
@@ -80,7 +81,11 @@ class MkinitcpioBackend(InitramfsBackend):
80
81
  )
81
82
  ui.info(f"Content of {mkinitcpio_path}:{mkinitcpio_path.read_text()}")
82
83
  ui.info("Generating mkinitcpio...")
83
- chroot_bash(root, "mkinitcpio -vP")
84
+ chroot_bash(root, "ssh-keygen -A\nmkinitcpio -vP")
85
+
86
+ def host_public_keys(self, root: Path) -> list[str]:
87
+ paths = (root / f"etc/ssh/ssh_host_{kind}_key.pub" for kind in DROPBEAR_KEY_TYPES)
88
+ return [path.read_text().strip() for path in paths if path.is_file()]
84
89
 
85
90
  def configure_bootloader(self, plan: ImagePlan, session: ImageSession, root: Path) -> None:
86
91
  cryptdevice = (
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
 
12
12
  from lim import runner, ui
13
13
  from lim.errors import LimError
14
+ from lim.image import hostkeys
14
15
 
15
16
  RECORDS_SUBPATH = ".config/lim/unlocks"
16
17
 
@@ -19,13 +20,17 @@ def records_dir(home: Path | None = None) -> Path:
19
20
  return (home or Path.home()) / RECORDS_SUBPATH
20
21
 
21
22
 
22
- def build_unlock_argv(target: str, key: str, unlock_command: str) -> list[str]:
23
- """SSH invocation for the unlock; torsocks-wrapped for .onion targets.
24
-
25
- The initramfs dropbear key differs from the booted host key, so host-key
26
- checking is disabled; a v3 onion address authenticates the endpoint itself.
27
- """
28
- ssh = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
23
+ def build_unlock_argv(target: str, key: str, unlock_command: str, alias: str) -> list[str]:
24
+ """SSH invocation for the unlock; torsocks-wrapped for .onion targets."""
25
+ ssh = [
26
+ "ssh",
27
+ "-o",
28
+ f"HostKeyAlias={alias}",
29
+ "-o",
30
+ f"UserKnownHostsFile={hostkeys.known_hosts_path(Path.home())}",
31
+ "-o",
32
+ "StrictHostKeyChecking=ask",
33
+ ]
29
34
  if key:
30
35
  ssh += ["-i", str(Path(key).expanduser())]
31
36
  if unlock_command:
@@ -66,7 +71,7 @@ def _load_records() -> list[tuple[str, dict]]:
66
71
  return [(name, record) for name, record in loaded if record is not None]
67
72
 
68
73
 
69
- def _select_record() -> dict | None:
74
+ def _select_record() -> tuple[str, dict] | None:
70
75
  records = _load_records()
71
76
  if not records:
72
77
  return None
@@ -75,13 +80,14 @@ def _select_record() -> dict | None:
75
80
  ui.info(f" {index}) {name} -> {record.get('target')}")
76
81
  answer = ui.ask("Pick a number, or Enter for manual entry:")
77
82
  if answer.isdigit() and 1 <= int(answer) <= len(records):
78
- return records[int(answer) - 1][1]
83
+ return records[int(answer) - 1]
79
84
  return None
80
85
 
81
86
 
82
87
  def remote_unlock() -> None:
83
- record = _select_record()
84
- if record:
88
+ selected = _select_record()
89
+ if selected:
90
+ name, record = selected
85
91
  target = record["target"]
86
92
  key = record.get("key", "")
87
93
  unlock_command = record.get("unlock_command", "")
@@ -89,6 +95,7 @@ def remote_unlock() -> None:
89
95
  target = ui.ask("Onion address or host/IP to unlock:")
90
96
  if not target:
91
97
  raise LimError("No target given.")
98
+ name = target
92
99
  key = ui.ask("Path to the SSH private key (empty for ssh defaults):")
93
100
  unlock_command = (
94
101
  "cryptroot-unlock"
@@ -97,4 +104,6 @@ def remote_unlock() -> None:
97
104
  )
98
105
  if target.endswith(".onion"):
99
106
  ui.info("Routing through Tor (torsocks); a local tor must be running.")
100
- runner.run(build_unlock_argv(target, key, unlock_command), check=False)
107
+ alias = hostkeys.host_key_alias(name)
108
+ hostkeys.known_hosts_path(Path.home()).parent.mkdir(mode=0o700, parents=True, exist_ok=True)
109
+ runner.run(build_unlock_argv(target, key, unlock_command, alias), check=False)
@@ -17,8 +17,9 @@ from pathlib import Path
17
17
  from lim import device as device_module
18
18
  from lim import ui
19
19
  from lim.errors import LimError
20
- from lim.image import choosers, crossarch, transfer, unlock
20
+ from lim.image import choosers, crossarch, hostkeys, transfer, unlock
21
21
  from lim.image.encryption import configure_encryption
22
+ from lim.image.initramfs import get_backend
22
23
  from lim.image.plan import ImagePlan
23
24
  from lim.image.session import ImageSession, chroot_bash
24
25
 
@@ -140,12 +141,31 @@ chown -R "$target:$target" "/home/$target/.ssh"
140
141
  chroot_bash(session.root_mount_path, script)
141
142
 
142
143
 
144
+ def _record_name(hostname: str) -> str:
145
+ return hostname or "device"
146
+
147
+
148
+ def _pin_host_keys(
149
+ entry: pwd.struct_passwd, name: str, target: str, private_key: str, host_keys: list[str]
150
+ ) -> None:
151
+ if not host_keys:
152
+ ui.warning("No initramfs host key found; the first unlock asks to confirm it.")
153
+ return
154
+ try:
155
+ alias = hostkeys.pin(entry, name, target, private_key, host_keys)
156
+ except OSError as exc:
157
+ ui.warning(f"Could not pin the initramfs host key: {exc}")
158
+ return
159
+ ui.info(f"Pinned the initramfs host key as {alias} in ~/{hostkeys.SSH_CONFIG_SUBPATH}.")
160
+
161
+
143
162
  def _save_unlock_record(
144
- plan: ImagePlan, host_user: str, hostname: str, onion: str | None, pubkey: Path
163
+ plan: ImagePlan, answers: _Answers, hostname: str, onion: str | None, host_keys: list[str]
145
164
  ) -> None:
146
165
  if not onion:
147
166
  return
148
- private_key = str(pubkey).removesuffix(".pub")
167
+ name = _record_name(hostname)
168
+ private_key = str(answers.pubkey).removesuffix(".pub")
149
169
  unlock_command = "" if plan.distribution in ("arch", "manjaro") else "cryptroot-unlock"
150
170
  record = {
151
171
  "target": onion,
@@ -153,22 +173,22 @@ def _save_unlock_record(
153
173
  "unlock_command": unlock_command,
154
174
  "hostname": hostname,
155
175
  }
156
- entry = pwd.getpwnam(host_user)
176
+ entry = pwd.getpwnam(answers.host_user)
157
177
  try:
158
- unlock.save_record(
159
- Path(entry.pw_dir), entry.pw_uid, entry.pw_gid, hostname or "device", record
160
- )
178
+ unlock.save_record(Path(entry.pw_dir), entry.pw_uid, entry.pw_gid, name, record)
161
179
  ui.info("Saved unlock target; run later with: lim --type remote-unlock")
162
180
  except OSError as exc:
163
181
  ui.warning(f"Could not save unlock record: {exc}")
182
+ _pin_host_keys(entry, name, onion, private_key, host_keys)
164
183
 
165
184
 
166
185
  def _print_summary(answers: _Answers, hostname: str, onion: str | None) -> None:
167
186
  ui.success("Encrypted Linux image is ready.")
168
187
  ui.info(f"Target device : {answers.device.path} (hostname: {hostname})")
169
188
  if onion:
189
+ alias = hostkeys.host_key_alias(_record_name(hostname))
170
190
  ui.info(f"Onion address : {onion}")
171
- ui.info(f"Unlock later : lim --type remote-unlock (or: torsocks ssh root@{onion})")
191
+ ui.info(f"Unlock later : lim --type remote-unlock (or: torsocks ssh {alias})")
172
192
  ui.info(f"After unlock, log in: ssh {answers.login_user}@<the box>")
173
193
  ui.info("Golden image? Clone this device onto the SSD, then grow it:")
174
194
  ui.info(" parted <ssd> resizepart 2 100% && cryptsetup resize <mapper> && resize2fs <mapper>")
@@ -187,8 +207,9 @@ def _execute(plan: ImagePlan, answers: _Answers) -> None:
187
207
  session.mount_chroot_binds()
188
208
  session.copy_resolv_conf()
189
209
  onion = configure_encryption(plan, session, authorized_keys)
210
+ host_keys = get_backend(plan.distribution).host_public_keys(session.root_mount_path)
190
211
  _configure_login_user(session, answers.login_user, answers.login_password, answers.pubkey)
191
- _save_unlock_record(plan, answers.host_user, hostname, onion, answers.pubkey)
212
+ _save_unlock_record(plan, answers, hostname, onion, host_keys)
192
213
  _print_summary(answers, hostname, onion)
193
214
  finally:
194
215
  if authorized_keys is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linux-image-manager
3
- Version: 2.3.0
3
+ Version: 2.4.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>
@@ -35,6 +35,7 @@ lim/image/choosers.py
35
35
  lim/image/chroot.py
36
36
  lim/image/crossarch.py
37
37
  lim/image/encryption.py
38
+ lim/image/hostkeys.py
38
39
  lim/image/loopimg.py
39
40
  lim/image/plan.py
40
41
  lim/image/raspberry.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "linux-image-manager"
8
- version = "2.3.0"
8
+ version = "2.4.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"