Encrypted image setups can now bake a Tor onion service into the initramfs so the dropbear unlock shell stays reachable behind NAT or a dynamic IP. When the user opts in, configure_encryption installs tor + busybox, drops the mkinitcpio hooks (ordered `netconf tor dropbear encryptssh`), generates the v3 onion keys offline in the image chroot, and prints the stable .onion address. Unlock with `torsocks ssh root@<onion-address>`. The runtime hook syncs the clock via NTP first (RTC-less boards boot at 1970, which Tor's consensus checks reject) and starts the onion service pointing at dropbear on 127.0.0.1:22. Hardening baked in from an adversarial review of the shipped path: - cmdline.txt boot path (RPi4-class firmware boot) now sets the same ip=::::<host>:eth0:dhcp net.ifnames=0 params as the boot.txt path, so the initramfs actually gets a network and the onion can publish. - the initramfs bakes in libnss_dns.so.2 so the NTP hostname resolves. - the hook extracts DHCP DNS with sed instead of sourcing the lease files, which would run attacker-controlled DHCP option strings as root pre-boot. - NTP is attempted unconditionally (bounded), not gated on DHCP-provided DNS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
"""Tor onion service in the initramfs for remote LUKS unlock.
|
|
|
|
The onion keys are generated offline inside the image chroot
|
|
(``tor --DisableNetwork 1`` writes them without touching the network)
|
|
and baked into the initramfs by the "tor" mkinitcpio hook, so the
|
|
dropbear unlock shell stays reachable under a stable .onion address
|
|
even behind NAT or a dynamic IP.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
from lim import config, packages, runner, ui
|
|
from lim.errors import LimError
|
|
from lim.image.plan import ImagePlan
|
|
from lim.image.session import chroot_bash, install_packages
|
|
|
|
# Paths inside the image (relative to the mounted root).
|
|
ONION_DIR = "etc/tor/initramfs-onion"
|
|
TORRC_PATH = "etc/tor/initramfs-torrc"
|
|
|
|
# An empty -f torrc keeps the image's /etc/tor/torrc (User tor, ...) out of
|
|
# the keygen run; with DisableNetwork the keys appear within a second, the
|
|
# loop only cushions slow qemu-emulated chroots.
|
|
_KEYGEN_SCRIPT = f"""
|
|
mkdir -p /{ONION_DIR}
|
|
chmod 0700 /{ONION_DIR}
|
|
: > /tmp/tor-keygen-torrc
|
|
tor -f /tmp/tor-keygen-torrc --DisableNetwork 1 \\
|
|
--DataDirectory /tmp/tor-keygen-data \\
|
|
--HiddenServiceDir /{ONION_DIR} \\
|
|
--HiddenServicePort "22 127.0.0.1:22" \\
|
|
--SocksPort 0 --RunAsDaemon 0 --Log "notice stderr" &
|
|
tor_pid=$!
|
|
for _ in $(seq 1 30); do
|
|
[ -s /{ONION_DIR}/hostname ] && break
|
|
sleep 1
|
|
done
|
|
kill "$tor_pid" 2>/dev/null || true
|
|
rm -rf /tmp/tor-keygen-data /tmp/tor-keygen-torrc
|
|
[ -s /{ONION_DIR}/hostname ]
|
|
"""
|
|
|
|
|
|
def _install_initcpio_files(root: Path) -> None:
|
|
source_dir = config.CONFIGURATION_PATH / "initcpio"
|
|
for source, target in (
|
|
(source_dir / "tor_install", root / "etc/initcpio/install/tor"),
|
|
(source_dir / "tor_hook", root / "etc/initcpio/hooks/tor"),
|
|
(source_dir / "torrc", root / TORRC_PATH),
|
|
):
|
|
ui.info(f"Installing {target}...")
|
|
runner.run(
|
|
["install", "-D", "-m", "0644", str(source), str(target)], sudo=True
|
|
)
|
|
|
|
|
|
def _generate_onion_keys(root: Path) -> str:
|
|
hostname_path = root / ONION_DIR / "hostname"
|
|
if hostname_path.is_file():
|
|
ui.info("Onion keys already exist, keeping the existing address.")
|
|
else:
|
|
ui.info("Generating onion service keys (offline, inside the chroot)...")
|
|
chroot_bash(root, _KEYGEN_SCRIPT, error_msg="Onion key generation failed.")
|
|
if not hostname_path.is_file():
|
|
raise LimError(f"Onion key generation produced no {hostname_path}.")
|
|
return hostname_path.read_text().strip()
|
|
|
|
|
|
def configure_tor_unlock(plan: ImagePlan, root: Path) -> str:
|
|
"""Install everything the "tor" mkinitcpio hook bakes in; return the onion address."""
|
|
ui.info("Setting up remote unlock via Tor onion service...")
|
|
install_packages(
|
|
plan.distribution, root, " ".join(packages.get_packages("server/tor"))
|
|
)
|
|
_install_initcpio_files(root)
|
|
onion_address = _generate_onion_keys(root)
|
|
ui.success(f"Onion unlock address: {onion_address}")
|
|
ui.info(f"Unlock later with: torsocks ssh root@{onion_address}")
|
|
return onion_address
|