Reorganized hal CLI into subcommand groups + MIT licensed

CLI structure now:
  hal {status,diagnose,unlock,forget} HOST
  hal connect {rescue,chroot,server} HOST [CMD]
  hal setup   {image,dropbear,grub,encrypt-root} HOST
  hal fix     {boot,network,grub,kernel,static-ip,upgrade,expand-fs} HOST

Added subcommands cover the previously-manual sections of the README:
  setup image       — upload autosetup + run installimage
  setup dropbear    — install dropbear + mkinitcpio plugins + patch HOOKS
  setup grub        — initial grub install for LUKS boot
  setup encrypt-root — full LUKS conversion of installed root
  connect server    — SSH to booted Arch (vs rescue/chroot)
  unlock            — cryptroot-unlock via dropbear with passphrase from keyring
  fix expand-fs     — lvresize + btrfs resize

Renames (breaking):
  upgrade-system    -> fix upgrade
  expand-fs         -> fix expand-fs
  forget-passphrase -> forget
  reinstall-grub    -> fix grub
  downgrade-kernel  -> fix kernel
  use-static-ip     -> fix static-ip
  fix-{boot,network} -> fix {boot,network}
  install-{image,grub} -> setup {image,grub}
  setup-dropbear    -> setup dropbear
  encrypt-root      -> setup encrypt-root

Removed downgrade-initramfs (never verified, narrow use case).

README rewritten to reference only hal commands; raw bash blocks for
pacman/cryptsetup/grub-install/mount/chroot are gone. Added autosetup.example
as a template for `hal setup image --autosetup PATH`.

Licensed under MIT (LICENSE file added). Author and homepage shown in
hal --version, hal --help, pyproject.toml, and README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-Birkenbach
2026-05-12 18:10:06 +02:00
parent 181240eae7
commit 3cf66640b5
10 changed files with 755 additions and 603 deletions

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Runs on the BOOTED Arch system (post-installimage, pre-encryption).
# Wires up dropbear + encryptssh + netconf for later remote-LUKS-unlock.
#
# Performs sections 3.13.5 of the README:
# - install busybox / mkinitcpio-{dropbear,utils,netconf}
# - copy authorized_keys to /etc/dropbear/root_key
# - regenerate OpenSSH host keys in PEM format
# - convert RSA host key to dropbear format
# - replace the HOOKS line in /etc/mkinitcpio.conf
#
# Idempotent: re-running is safe. A backup of /etc/mkinitcpio.conf is taken
# at first patch as /etc/mkinitcpio.conf.hal-backup.
set -e
banner() { printf "\n========== %s ==========\n" "$1"; }
banner "installing dropbear + mkinitcpio plugins"
pacman -S --noconfirm --needed \
busybox mkinitcpio-dropbear mkinitcpio-utils mkinitcpio-netconf
banner "copying authorized_keys to /etc/dropbear/root_key"
install -d -m 0755 /etc/dropbear
install -m 0600 /root/.ssh/authorized_keys /etc/dropbear/root_key
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
banner "enabling sshd"
systemctl enable sshd
banner "regenerating OpenSSH host keys (PEM format)"
rm -f /etc/ssh/ssh_host_*
ssh-keygen -A -m PEM
banner "importing RSA host key into dropbear"
dropbearconvert openssh dropbear \
/etc/ssh/ssh_host_rsa_key /etc/dropbear/dropbear_rsa_host_key
banner "patching HOOKS in /etc/mkinitcpio.conf"
[ -f /etc/mkinitcpio.conf.hal-backup ] \
|| cp -a /etc/mkinitcpio.conf /etc/mkinitcpio.conf.hal-backup
# Replace any existing HOOKS=(...) line with the encryptssh-enabled set.
sed -i -E \
's|^HOOKS=.*|HOOKS=(base udev autodetect modconf block mdadm_udev lvm2 netconf dropbear encryptssh filesystems keyboard fsck)|' \
/etc/mkinitcpio.conf
echo "HOOKS line is now:"
grep '^HOOKS=' /etc/mkinitcpio.conf
banner "done"
cat <<EOF
Next steps:
1. Activate Hetzner Rescue in the Robot, then reboot the server.
2. From your client: hal connect rescue <host>
3. Inside rescue: hal encrypt-root <host>
4. After that: hal install-grub <host>
EOF

View File

@@ -0,0 +1,106 @@
#!/bin/bash
# Runs IN HETZNER RESCUE (NOT in chroot). Re-creates the root LV stack on
# top of LUKS, preserving the installed Arch by copying it through /oldroot.
#
# Performs sections 4.44.15 of the README in one go:
# 4.4 mount the unencrypted /dev/mapper/vg0-root
# 4.5 cp -va /mnt → /oldroot at full RAID resync speed
# 4.6 umount /mnt
# 4.7 vgremove vg0
# 4.8 cat /proc/mdstat (display)
# 4.9 luksFormat /dev/md1 (prompts for NEW passphrase!)
# luksOpen + recreate LVM (vg0 with swap + root)
# mkfs.btrfs / mkswap
# 4.10 mount the encrypted root at /mnt
# 4.12 cp -va /oldroot back into /mnt at full RAID resync speed
# 4.13 bind /dev /sys /proc, mount /boot
# 4.14 echo cryptroot line into /mnt/etc/crypttab
# 4.15 chroot + mkinitcpio -P
#
# DESTRUCTIVE: /dev/md1 will be re-formatted with LUKS. Any data not under
# /mnt (vg0-root) is lost. Confirmation prompted before the format step.
set -e
banner() { printf "\n========== %s ==========\n" "$1"; }
banner "4.4 mount existing unencrypted root"
vgscan -v
vgchange -a y
mount /dev/mapper/vg0-root /mnt
banner "4.5 copy current system to /oldroot (full RAID resync speed)"
mkdir -p /oldroot
echo 0 > /proc/sys/dev/raid/speed_limit_max
cp -va /mnt/. /oldroot/.
echo 200000 > /proc/sys/dev/raid/speed_limit_max
banner "4.6 unmount original root"
umount /mnt
banner "4.7 remove unencrypted VG (frees /dev/md1)"
vgremove -f vg0
banner "4.8 RAID state"
cat /proc/mdstat
banner "CONFIRMATION REQUIRED"
echo "About to luksFormat /dev/md1. This is DESTRUCTIVE for /dev/md1."
echo "Type 'YES' to continue (anything else aborts):"
read -r confirm
if [ "$confirm" != "YES" ]; then
echo "Aborted by user before luksFormat. /oldroot still has your data;"
echo "you can re-create the original LVM by hand from there if needed."
exit 1
fi
banner "4.9 LUKS format /dev/md1 (you will be prompted for the NEW passphrase)"
cryptsetup --cipher aes-xts-plain64 --key-size 256 --hash sha256 \
--iter-time 10000 luksFormat /dev/md1
banner "4.9b open the LUKS volume (re-enter the same passphrase)"
cryptsetup luksOpen /dev/md1 cryptroot
banner "4.9c recreate LVM on top of /dev/mapper/cryptroot"
pvcreate /dev/mapper/cryptroot
vgcreate vg0 /dev/mapper/cryptroot
lvcreate -n swap -L 8G vg0
lvcreate -n root -l 100%FREE vg0
mkfs.btrfs /dev/vg0/root
mkswap /dev/vg0/swap
banner "4.10 mount the encrypted root"
mount /dev/vg0/root /mnt
banner "4.12 copy system back into the encrypted root"
echo 0 > /proc/sys/dev/raid/speed_limit_max
cp -va /oldroot/. /mnt/.
echo 200000 > /proc/sys/dev/raid/speed_limit_max
banner "4.13 bind-mount /dev /sys /proc, mount /boot"
mount /dev/md0 /mnt/boot
mount --bind /dev /mnt/dev
mount --bind /sys /mnt/sys
mount --bind /proc /mnt/proc
banner "4.14 append cryptroot line to /etc/crypttab"
if ! grep -qE '^cryptroot[[:space:]]' /mnt/etc/crypttab 2>/dev/null; then
echo "cryptroot /dev/md1 none luks" >> /mnt/etc/crypttab
fi
grep cryptroot /mnt/etc/crypttab
banner "4.15 regenerate initramfs inside chroot"
chroot /mnt /bin/bash -c "mkinitcpio -P"
banner "done"
cat <<EOF
Encryption setup complete. /oldroot can be deleted manually after you've
confirmed the encrypted boot works.
Recommended next steps:
hal install-grub <host> # configures GRUB for LUKS-encrypted root
hal connect rescue <host> reboot
# Disable rescue in Hetzner Robot
hal status <host> # poll for dropbear / sshd
hal unlock <host> # send LUKS passphrase to dropbear
EOF

View File

@@ -0,0 +1,78 @@
#!/bin/bash
# Runs INSIDE the chroot. Initial GRUB install for the LUKS-encrypted root.
# Performs sections 5.15.3 of the README:
# - install the grub package
# - write /etc/default/grub with the LUKS cmdline + GRUB_ENABLE_CRYPTODISK=y
# - grub-mkconfig
# - grub-install on every disk backing /boot's RAID
set -e
banner() { printf "\n========== %s ==========\n" "$1"; }
# Convert a partition path to its parent disk. (Same helper as fix/grub.sh.)
parent_disk() {
local part="$1"
case "$part" in
/dev/nvme[0-9]*n[0-9]*p[0-9]*) echo "${part%p[0-9]*}" ;;
/dev/mmcblk[0-9]*p[0-9]*) echo "${part%p[0-9]*}" ;;
/dev/sd[a-z]*[0-9]*) echo "$part" | sed -E 's/[0-9]+$//' ;;
/dev/vd[a-z]*[0-9]*) echo "$part" | sed -E 's/[0-9]+$//' ;;
*)
local d
d=$(lsblk -no PKNAME "$part" 2>/dev/null | head -1)
[ -n "$d" ] && echo "/dev/$d"
;;
esac
}
banner "installing grub package"
pacman -S --noconfirm --needed grub
banner "writing /etc/default/grub for LUKS boot"
[ -f /etc/default/grub.hal-backup ] || cp -a /etc/default/grub /etc/default/grub.hal-backup
cat > /etc/default/grub <<'GRUBEOF'
# hetzner-arch-luks default grub config
GRUB_DEFAULT=0
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="Arch"
GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0"
GRUB_CMDLINE_LINUX="cryptdevice=/dev/md1:cryptroot ip=dhcp"
GRUB_PRELOAD_MODULES="part_gpt part_msdos"
GRUB_ENABLE_CRYPTODISK=y
GRUB_TIMEOUT_STYLE=menu
GRUB_TERMINAL_INPUT=console
GRUB_GFXMODE=auto
GRUB_GFXPAYLOAD_LINUX=keep
GRUB_DISABLE_RECOVERY=true
GRUBEOF
echo "Wrote /etc/default/grub. Showing relevant lines:"
grep -E '^GRUB_(CMDLINE_LINUX|ENABLE_CRYPTODISK|PRELOAD_MODULES)=' /etc/default/grub
banner "identifying boot disks (members of md0)"
BOOT_DISKS=()
for part in $(mdadm --detail /dev/md0 2>/dev/null | awk '/active sync/ {print $NF}'); do
disk=$(parent_disk "$part")
[ -z "$disk" ] && continue
already=0
for d in "${BOOT_DISKS[@]}"; do [ "$d" = "$disk" ] && already=1; done
[ "$already" -eq 0 ] && BOOT_DISKS+=("$disk")
done
echo "Boot disks: ${BOOT_DISKS[*]}"
banner "grub-mkconfig"
grub-mkconfig -o /boot/grub/grub.cfg 2>&1 | tail -10
banner "grub-install on each boot disk"
for disk in "${BOOT_DISKS[@]}"; do
echo "-- grub-install --target=i386-pc --recheck $disk"
grub-install --target=i386-pc --recheck "$disk"
done
banner "done"
cat <<EOF
GRUB installed for LUKS-encrypted boot.
Recommended next step: hal use-static-ip <host> (replaces ip=dhcp with a
static kernel-cmdline IP, making the initramfs network independent of DHCP).
EOF