Scripts that turn an installed Omarchy disk into a downloadable VM image
As they were tested by hand on the 4.0.4 ISO, before any of it is wired
into a workflow:
vm-install unattended install of an ISO into a qcow2, through cidata
vm-seal makes the disk publishable: SSH takes keys only, no machine
ID, host keys, build key or build logs left, compressed
with zstd, with checksums and the kernel pulled out for
direct boot
vm-verify boots the sealed image once and checks all of that
vm-boot boots a throwaway copy with a key made for that boot and
runs a command in it
On this machine: install 114 s, seal 27 s, a 4.2 GB image, SSH about 5 s
after power-on, 17 of 17 verify checks, and 7 of them failing on an
image sealed without these fixes.
This commit is contained in:
1 parent
8b17699997
commit
4e7b18eb7d
4 files changed
+355
No files matched your search
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Boots a sealed Omarchy image (vm-seal) as a throwaway machine.
|
||||||
|
#
|
||||||
|
# vm-boot <sealed_dir> boot, print how to log in, run until Ctrl-C
|
||||||
|
# vm-boot <sealed_dir> <command...> boot, run the command over SSH, power off;
|
||||||
|
# exits with the command's status
|
||||||
|
#
|
||||||
|
# Nothing is written to sealed_dir: the machine runs on a copy-on-write overlay
|
||||||
|
# that is deleted on exit. Each boot gets a new SSH key and a new machine ID;
|
||||||
|
# the image itself holds no key and its SSH takes no password.
|
||||||
|
#
|
||||||
|
# Needs qemu-system-x86_64, qemu-img, ssh and a usable /dev/kvm.
|
||||||
|
# Env: SMP (4), MEM (4G), SSH_PORT (random), VM_UUID (random), APPEND (more
|
||||||
|
# kernel arguments), QEMU_EXTRA (more qemu arguments, split on spaces),
|
||||||
|
# VM_STATE_DIR (/var/tmp; holds the overlay, so it needs room to grow).
|
||||||
|
set -euo pipefail
|
||||||
|
[[ $# -ge 1 ]] || { sed -n '2,16p' "$0" >&2; exit 2; }
|
||||||
|
base=$(realpath "$1"); shift
|
||||||
|
for f in base.qcow2 vmlinuz initrd cmdline; do
|
||||||
|
[[ -s $base/$f ]] || { echo "vm-boot: $base/$f is missing" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
[[ -r /dev/kvm && -w /dev/kvm ]] || { echo "vm-boot: /dev/kvm is not usable" >&2; exit 1; }
|
||||||
|
|
||||||
|
state=$(mktemp -d "${VM_STATE_DIR:-/var/tmp}/vm.XXXXXX")
|
||||||
|
cleanup() {
|
||||||
|
local status=$? pid
|
||||||
|
if [[ -f $state/qemu.pid ]]; then
|
||||||
|
pid=$(cat "$state/qemu.pid")
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
for _ in $(seq 1 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
|
||||||
|
fi
|
||||||
|
rm -rf "$state"
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 130' INT TERM
|
||||||
|
port=${SSH_PORT:-$(( 20000 + RANDOM % 10000 ))}
|
||||||
|
uuid=${VM_UUID:-$(cat /proc/sys/kernel/random/uuid)}
|
||||||
|
|
||||||
|
# A key made for this boot only, handed in through the firmware tables as a
|
||||||
|
# systemd credential that writes authorized_keys.
|
||||||
|
ssh-keygen -q -t ed25519 -N '' -C vm-boot -f "$state/id_ed25519"
|
||||||
|
keys=$(printf 'd /home/omarchy/.ssh 0700 omarchy omarchy -\nf+ /home/omarchy/.ssh/authorized_keys 0600 omarchy omarchy - %s\n' \
|
||||||
|
"$(cat "$state/id_ed25519.pub")" | base64 -w0)
|
||||||
|
ssh_opts=(-i "$state/id_ed25519" -p "$port" -o BatchMode=yes -o IdentitiesOnly=yes
|
||||||
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR)
|
||||||
|
|
||||||
|
qemu-img create -qf qcow2 -b "$base/base.qcow2" -F qcow2 "$state/run.qcow2"
|
||||||
|
t0=$(date +%s%N)
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
qemu-system-x86_64 -cpu host -enable-kvm -machine q35,accel=kvm -smp "${SMP:-4}" -m "${MEM:-4G}" \
|
||||||
|
-kernel "$base/vmlinuz" -initrd "$base/initrd" -append "$(cat "$base/cmdline")${APPEND:+ $APPEND}" \
|
||||||
|
-uuid "$uuid" \
|
||||||
|
-drive "file=$state/run.qcow2,format=qcow2,if=none,id=d0" -device virtio-blk-pci,drive=d0 \
|
||||||
|
-smbios "type=11,value=io.systemd.credential.binary:tmpfiles.extra=$keys" \
|
||||||
|
-device virtio-vga -device virtio-rng-pci -display none \
|
||||||
|
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:$port-:22" -device virtio-net-pci,netdev=n0 \
|
||||||
|
-qmp "unix:$state/qmp.sock,server,nowait" -pidfile "$state/qemu.pid" -daemonize ${QEMU_EXTRA:-}
|
||||||
|
|
||||||
|
# </dev/null: the probe must not eat a script meant for the command below.
|
||||||
|
until ssh "${ssh_opts[@]}" -o ConnectTimeout=1 omarchy@127.0.0.1 true </dev/null 2>/dev/null; do
|
||||||
|
kill -0 "$(cat "$state/qemu.pid")" 2>/dev/null || { echo "vm-boot: qemu exited before ssh came up" >&2; exit 1; }
|
||||||
|
(( ($(date +%s%N) - t0) / 1000000 < 300000 )) || { echo "vm-boot: no ssh after 300s" >&2; exit 1; }
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
echo "vm-boot: ssh up after $(( ($(date +%s%N) - t0) / 1000000 )) ms, machine ID ${uuid//-/}" >&2
|
||||||
|
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
# Stdin passes through, so `vm-boot dir bash -s <script` works.
|
||||||
|
ssh "${ssh_opts[@]}" omarchy@127.0.0.1 "$@"
|
||||||
|
else
|
||||||
|
echo "ssh ${ssh_opts[*]} omarchy@127.0.0.1"
|
||||||
|
echo "Desktop and sudo password: omarchy. QMP socket: $state/qmp.sock. Ctrl-C powers it off."
|
||||||
|
while kill -0 "$(cat "$state/qemu.pid")" 2>/dev/null; do sleep 1; done
|
||||||
|
fi
|
||||||
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Unattended install of an Omarchy ISO into a qcow2, through a cidata drive.
|
||||||
|
#
|
||||||
|
# vm-install <omarchy.iso> <out_dir>
|
||||||
|
#
|
||||||
|
# out_dir gets base.qcow2 (the installed disk, powered off), OVMF_VARS.4m.fd
|
||||||
|
# and the key that can log in to it. vm-seal turns that into a reusable image.
|
||||||
|
# Same answer files and VM shape as omarchy-iso's test/integration.d/base-test.sh.
|
||||||
|
set -uo pipefail
|
||||||
|
[[ $# -eq 2 ]] || { echo "Usage: vm-install <omarchy.iso> <out_dir>" >&2; exit 2; }
|
||||||
|
ISO=$(realpath "$1"); OUT=$(realpath -m "$2"); W=$OUT/work
|
||||||
|
# Arch and Ubuntu keep the firmware in different places.
|
||||||
|
for fw in /usr/share/edk2/x64/OVMF_CODE.4m.fd:/usr/share/edk2/x64/OVMF_VARS.4m.fd /usr/share/OVMF/OVMF_CODE_4M.fd:/usr/share/OVMF/OVMF_VARS_4M.fd; do
|
||||||
|
[[ -f ${fw%%:*} && -f ${fw##*:} ]] && { FW_CODE=${fw%%:*}; FW_VARS=${fw##*:}; break; }
|
||||||
|
done
|
||||||
|
: "${FW_CODE:?no OVMF firmware found}"
|
||||||
|
[[ -r /dev/kvm && -w /dev/kvm ]] || { echo "vm-install: /dev/kvm is not usable" >&2; exit 1; }
|
||||||
|
GUEST_USER=omarchy; GUEST_PASSWORD=omarchy; GUEST_HOSTNAME=omarchy-vm; SSH_PORT=2422
|
||||||
|
rm -rf "$W"; mkdir -p "$W/cidata" "$W/shots"; t0=$(date +%s); el() { echo $(( $(date +%s) - t0 )); }
|
||||||
|
ssh-keygen -q -t ed25519 -N '' -C omarchy-vm-build -f "$W/id_ed25519"
|
||||||
|
hash=$(openssl passwd -6 "$GUEST_PASSWORD")
|
||||||
|
disk_bytes=$((40*1024**3)); mib=$((1024**2)); boot_start=$mib; boot_size=$((2*1024**3)); main_start=$((boot_size+boot_start)); main_size=$((disk_bytes-main_start-mib))
|
||||||
|
jq -n --arg h "$hash" --arg u "$GUEST_USER" '{root_enc_password:$h, users:[{enc_password:$h, groups:[], sudo:true, username:$u}]}' > "$W/cidata/user_credentials.json"
|
||||||
|
cat > "$W/cidata/user_configuration.json" <<EOF
|
||||||
|
{
|
||||||
|
"app_config": null, "archinstall-language": "English", "auth_config": {},
|
||||||
|
"audio_config": { "audio": "pipewire" },
|
||||||
|
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
||||||
|
"custom_commands": [],
|
||||||
|
"omarchy_install": { "mode": "full_disk", "defer_provisioning": false, "target_mount": "/mnt",
|
||||||
|
"boot": { "esp_mount": "/boot", "esp_path": "/EFI/limine", "efi_binary": "limine_x64.efi", "enable_fallback": true },
|
||||||
|
"storage": { "kernel": "linux-omarchy" } },
|
||||||
|
"disk_config": { "config_type": "default_layout", "device_modifications": [ { "device": "/dev/vda", "partitions": [
|
||||||
|
{ "btrfs": [], "dev_path": null, "flags": [ "boot", "esp" ], "fs_type": "fat32", "mount_options": [], "mountpoint": "/boot",
|
||||||
|
"obj_id": "ea21d3f2-82bb-49cc-ab5d-6f81ae94e18d",
|
||||||
|
"size": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $boot_size },
|
||||||
|
"start": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $boot_start }, "status": "create", "type": "primary" },
|
||||||
|
{ "btrfs": [ { "mountpoint": "/", "name": "@" }, { "mountpoint": "/home", "name": "@home" }, { "mountpoint": "/var/log", "name": "@log" }, { "mountpoint": "/var/cache/pacman/pkg", "name": "@pkg" } ],
|
||||||
|
"dev_path": null, "flags": [], "fs_type": "btrfs", "mount_options": [ "compress=zstd" ], "mountpoint": null,
|
||||||
|
"obj_id": "8c2c2b92-1070-455d-b76a-56263bab24aa",
|
||||||
|
"size": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $main_size },
|
||||||
|
"start": { "sector_size": { "unit": "B", "value": 512 }, "unit": "B", "value": $main_start }, "status": "create", "type": "primary" } ],
|
||||||
|
"wipe": true } ] },
|
||||||
|
"hostname": "$GUEST_HOSTNAME", "kernels": [ "linux-omarchy" ], "network_config": { "type": "iso" }, "ntp": true,
|
||||||
|
"parallel_downloads": 8, "script": null, "services": [], "swap": true, "timezone": "UTC",
|
||||||
|
"locale_config": { "kb_layout": "us", "sys_enc": "UTF-8", "sys_lang": "en_US.UTF-8" },
|
||||||
|
"mirror_config": { "custom_repositories": [], "custom_servers": [
|
||||||
|
{"url": "https://mirror.omarchy.org/\$repo/os/\$arch"}, {"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"}, {"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"} ],
|
||||||
|
"mirror_regions": {}, "optional_repositories": [] },
|
||||||
|
"packages": [ "base-devel", "git", "omarchy-keyring", "omarchy-settings", "omarchy" ],
|
||||||
|
"profile_config": { "gfx_driver": null, "greeter": null, "profile": {} }, "version": "3.0.9"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
jq -e . "$W/cidata/user_configuration.json" >/dev/null || { echo "bad json"; exit 1; }
|
||||||
|
echo "Omarchy Test" > "$W/cidata/user_full_name.txt"; echo "test@omarchy.org" > "$W/cidata/user_email_address.txt"; echo false > "$W/cidata/user_encrypt_installation.txt"
|
||||||
|
cp "$W/id_ed25519.pub" "$W/cidata/authorized_keys"
|
||||||
|
truncate -s 4M "$W/cidata.img"; mkfs.vfat -n CIDATA "$W/cidata.img" >/dev/null; mcopy -i "$W/cidata.img" "$W"/cidata/* ::/
|
||||||
|
qemu-img create -f qcow2 "$W/disk.qcow2" 40G >/dev/null; cp "$FW_VARS" "$W/OVMF_VARS.4m.fd"
|
||||||
|
qemu-system-x86_64 -cpu host -enable-kvm -machine q35,accel=kvm -smp 8 -m 8G \
|
||||||
|
-drive if=pflash,format=raw,readonly=on,file="$FW_CODE" -drive if=pflash,format=raw,file="$W/OVMF_VARS.4m.fd" \
|
||||||
|
-drive file="$W/disk.qcow2",format=qcow2,if=none,id=drive0 -device virtio-blk-pci,drive=drive0,bootindex=1 \
|
||||||
|
-device virtio-vga -display none -usb -device usb-tablet \
|
||||||
|
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22 -device virtio-net-pci,netdev=net0 \
|
||||||
|
-qmp "unix:$W/qmp.sock,server,nowait" -serial "file:$W/serial.log" -pidfile "$W/qemu.pid" -daemonize \
|
||||||
|
-drive "file=$ISO,media=cdrom,if=none,format=raw,id=cdrom0,readonly=on" -device ide-cd,drive=cdrom0,bootindex=2 \
|
||||||
|
-drive "file=$W/cidata.img,format=raw,if=none,id=cidata" -device usb-storage,drive=cidata || exit 1
|
||||||
|
qmp() { printf '{"execute":"qmp_capabilities"}\n%s\n' "$1" | socat - "unix-connect:$W/qmp.sock" >/dev/null 2>&1; }
|
||||||
|
shot() { qmp "{\"execute\":\"screendump\",\"arguments\":{\"filename\":\"$W/shots/$1.png\",\"format\":\"png\"}}"; }
|
||||||
|
sshg() { ssh -i "$W/id_ed25519" -p $SSH_PORT -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 -o LogLevel=ERROR $GUEST_USER@127.0.0.1 "$@"; }
|
||||||
|
echo "install started"; n=0
|
||||||
|
while :; do
|
||||||
|
sshg true 2>/dev/null && { echo "ssh_up $(el)s"; break; }
|
||||||
|
kill -0 "$(cat "$W/qemu.pid")" 2>/dev/null || { echo "qemu exited at $(el)s"; exit 1; }
|
||||||
|
(( $(el) > 2400 )) && { shot timeout; echo "timeout"; exit 1; }
|
||||||
|
n=$((n+1)); (( n % 6 == 0 )) && { shot "$(printf 'p-%04d' "$(el)")"; sleep 1
|
||||||
|
txt=$(tesseract "$W/shots/$(printf 'p-%04d' "$(el)")".png - 2>/dev/null || true)
|
||||||
|
grep -qi "Reboot Now" <<<"$txt" && { echo "reboot prompt $(el)s"; qmp '{"execute":"send-key","arguments":{"keys":[{"type":"qcode","data":"ret"}]}}'; }
|
||||||
|
grep -qi "installation stopped" <<<"$txt" && { echo "install stopped $(el)s"; exit 1; }
|
||||||
|
echo "... $(el)s"; }
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
sshg 'timeout 180 systemctl is-system-running --wait; systemd-analyze; cat /etc/os-release | head -3; pacman -Q omarchy linux-omarchy 2>&1; cat ~/.local/share/omarchy/version 2>/dev/null' > "$W/first-boot.txt" 2>&1
|
||||||
|
sshg 'sudo -n cat /var/log/omarchy-install-timing.json' > "$W/install-timing.json" 2>/dev/null
|
||||||
|
shot first-boot; echo "first_boot_checked $(el)s"; cat "$W/first-boot.txt"
|
||||||
|
# Power off cleanly, so the disk is consistent, and leave the three files vm-seal reads.
|
||||||
|
printf '%s\n' "$GUEST_PASSWORD" | sshg "sudo -S -p '' systemctl poweroff" 2>/dev/null || true
|
||||||
|
pid=$(cat "$W/qemu.pid"); for i in $(seq 1 120); do kill -0 "$pid" 2>/dev/null || break; sleep 1; done
|
||||||
|
kill -0 "$pid" 2>/dev/null && { echo "vm-install: the VM did not power off" >&2; exit 1; }
|
||||||
|
mv "$W/disk.qcow2" "$OUT/base.qcow2"; cp "$W/OVMF_VARS.4m.fd" "$W/id_ed25519" "$W/id_ed25519.pub" "$OUT/"
|
||||||
|
echo "installed: $OUT/base.qcow2 ($(du -sh "$OUT/base.qcow2" | cut -f1)) in $(el)s"
|
||||||
Executable
+142
@@ -0,0 +1,142 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Turns an installed Omarchy disk (vm-install) into an image anyone can
|
||||||
|
# download and boot: every boot is its own machine, nothing of the build is
|
||||||
|
# left in it, and it boots straight into its kernel, so a boot takes seconds.
|
||||||
|
#
|
||||||
|
# vm-seal <installed_dir> <sealed_dir>
|
||||||
|
#
|
||||||
|
# installed_dir holds base.qcow2, OVMF_VARS.4m.fd and id_ed25519. sealed_dir
|
||||||
|
# gets a standalone, zstd-compressed base.qcow2 and the UKI's vmlinuz, initrd
|
||||||
|
# and cmdline. It holds no key: each boot is handed one (see vm-boot).
|
||||||
|
#
|
||||||
|
# The image keeps the installer's user and password (GUEST_PASSWORD, default
|
||||||
|
# omarchy) for the desktop and sudo. SSH takes keys only.
|
||||||
|
set -euo pipefail
|
||||||
|
[[ $# -eq 2 ]] || { echo "Usage: vm-seal <installed_dir> <sealed_dir>" >&2; exit 2; }
|
||||||
|
in=$(realpath "$1") out=$(realpath -m "$2")
|
||||||
|
pw=${GUEST_PASSWORD:-omarchy}
|
||||||
|
for fw in /usr/share/edk2/x64/OVMF_CODE.4m.fd /usr/share/OVMF/OVMF_CODE_4M.fd; do
|
||||||
|
[[ -f $fw ]] && { fw_code=$fw; break; }
|
||||||
|
done
|
||||||
|
: "${fw_code:?no OVMF firmware found}"
|
||||||
|
|
||||||
|
state=$(mktemp -d /var/tmp/seal.XXXXXX)
|
||||||
|
cleanup() {
|
||||||
|
local status=$? pid
|
||||||
|
if [[ -f $state/qemu.pid ]]; then
|
||||||
|
pid=$(cat "$state/qemu.pid")
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
for _ in $(seq 1 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
|
||||||
|
fi
|
||||||
|
rm -rf "$state"
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
port=$(( 30000 + RANDOM % 5000 ))
|
||||||
|
|
||||||
|
ssh_vm() {
|
||||||
|
ssh -i "$in/id_ed25519" -p "$port" -o BatchMode=yes -o IdentitiesOnly=yes \
|
||||||
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ConnectTimeout=2 -o LogLevel=ERROR omarchy@127.0.0.1 "$@"
|
||||||
|
}
|
||||||
|
# Runs a script as root in the guest and fails if any line of it fails. The
|
||||||
|
# password goes to sudo on stdin, never into a command line.
|
||||||
|
root() {
|
||||||
|
printf '%s\n' "$pw" | ssh_vm "sudo -S -p '' bash -ec $(printf %q "trap 'echo \"vm-seal: failed in the guest at: \$BASH_COMMAND\" >&2' ERR
|
||||||
|
$1")"
|
||||||
|
}
|
||||||
|
|
||||||
|
qemu-img create -qf qcow2 -b "$in/base.qcow2" -F qcow2 "$state/edit.qcow2"
|
||||||
|
cp "$in/OVMF_VARS.4m.fd" "$state/vars.fd"
|
||||||
|
qemu-system-x86_64 -cpu host -enable-kvm -machine q35,accel=kvm -smp 4 -m 4G \
|
||||||
|
-drive if=pflash,format=raw,readonly=on,file="$fw_code" \
|
||||||
|
-drive if=pflash,format=raw,file="$state/vars.fd" \
|
||||||
|
-drive "file=$state/edit.qcow2,format=qcow2,if=virtio,discard=unmap" -display none \
|
||||||
|
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:$port-:22" -device virtio-net-pci,netdev=n0 \
|
||||||
|
-pidfile "$state/qemu.pid" -daemonize
|
||||||
|
for i in $(seq 1 150); do
|
||||||
|
ssh_vm true 2>/dev/null && break
|
||||||
|
(( i < 150 )) || { echo "vm-seal: no ssh on the installed disk" >&2; exit 1; }
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
# Let first-boot work finish before taking it apart.
|
||||||
|
ssh_vm 'timeout 180 systemctl is-system-running --wait >/dev/null 2>&1 || true'
|
||||||
|
|
||||||
|
mkdir -p "$out"
|
||||||
|
root 'cat /boot/EFI/Linux/omarchy_linux-omarchy.efi' >"$state/uki.efi"
|
||||||
|
|
||||||
|
root '
|
||||||
|
# The ISO writes autologin only for encrypted installs; a test has nobody at
|
||||||
|
# SDDM, and a desktop check that skips for want of a session reads as a pass.
|
||||||
|
printf "[Autologin]\nUser=omarchy\nSession=omarchy.desktop\n" >/etc/sddm.conf.d/autologin.conf
|
||||||
|
|
||||||
|
# The image is public and its password is too, so SSH takes keys only. sshd
|
||||||
|
# keeps the first value it reads, hence a name that sorts first.
|
||||||
|
printf "PasswordAuthentication no\nKbdInteractiveAuthentication no\nAuthenticationMethods publickey\n" \
|
||||||
|
>/etc/ssh/sshd_config.d/00-omarchy-vm.conf
|
||||||
|
ssh-keygen -A >/dev/null # sshd -T needs host keys to parse; they go below
|
||||||
|
sshd -T | grep -qix "passwordauthentication no"
|
||||||
|
sshd -T | grep -qix "authenticationmethods publickey"
|
||||||
|
|
||||||
|
# Empty, not "uninitialized": that string marks a first boot, and a first boot
|
||||||
|
# applies unit presets, which on Arch disable everything not listed. Empty lets
|
||||||
|
# systemd take the VM SMBIOS UUID (qemu -uuid) as this boot machine ID.
|
||||||
|
# Host keys come back from sshdgenkeys.service before sshd starts.
|
||||||
|
: >/etc/machine-id
|
||||||
|
rm -f /var/lib/dbus/machine-id /etc/ssh/ssh_host_*
|
||||||
|
rm -f /home/omarchy/.ssh/authorized_keys /home/omarchy/.bash_history /root/.bash_history
|
||||||
|
|
||||||
|
# The factory snapshot the installer took carries the same machine ID.
|
||||||
|
mkdir -p /run/seal-top
|
||||||
|
mount -o subvolid=5 "$(findmnt -no SOURCE / | sed "s/\[.*//")" /run/seal-top
|
||||||
|
if [ -f /run/seal-top/@factory/etc/machine-id ]; then
|
||||||
|
ro=$(btrfs property get -ts /run/seal-top/@factory ro)
|
||||||
|
[ "$ro" = ro=false ] || btrfs property set -ts /run/seal-top/@factory ro false
|
||||||
|
: >/run/seal-top/@factory/etc/machine-id
|
||||||
|
[ "$ro" = ro=false ] || btrfs property set -ts /run/seal-top/@factory ro true
|
||||||
|
fi
|
||||||
|
umount /run/seal-top
|
||||||
|
|
||||||
|
# Stopping the unit saves a seed; with it stopped, shutdown saves none.
|
||||||
|
systemctl stop systemd-random-seed.service
|
||||||
|
rm -f /var/lib/systemd/random-seed /var/lib/systemd/credential.secret
|
||||||
|
|
||||||
|
# Leases and the key NetworkManager derives stable addresses from.
|
||||||
|
systemctl stop NetworkManager.service
|
||||||
|
rm -f /var/lib/NetworkManager/*.lease /var/lib/NetworkManager/secret_key \
|
||||||
|
/var/lib/NetworkManager/seen-bssids /var/lib/NetworkManager/timestamps \
|
||||||
|
/var/lib/NetworkManager/NetworkManager.state
|
||||||
|
|
||||||
|
# Vacuuming leaves the active journal. Send journald to /run, then the
|
||||||
|
# build logs on disk can go, and shutdown writes nothing back.
|
||||||
|
journalctl --relinquish-var
|
||||||
|
rm -rf /var/log/journal/*
|
||||||
|
|
||||||
|
test ! -s /etc/machine-id
|
||||||
|
test -z "$(ls /etc/ssh/ssh_host_* /var/lib/NetworkManager/*.lease /var/log/journal/* 2>/dev/null)"
|
||||||
|
test ! -e /home/omarchy/.ssh/authorized_keys
|
||||||
|
test ! -e /var/lib/systemd/random-seed
|
||||||
|
|
||||||
|
sync
|
||||||
|
# Hand freed blocks back to the disk image, so the copy skips them.
|
||||||
|
fstrim -av >/dev/null
|
||||||
|
(sleep 1; systemctl poweroff) >/dev/null 2>&1 &'
|
||||||
|
|
||||||
|
# The key that let us in is gone, so the poweroff above was the last word.
|
||||||
|
pid=$(cat "$state/qemu.pid")
|
||||||
|
for _ in $(seq 1 120); do kill -0 "$pid" 2>/dev/null || break; sleep 1; done
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then echo "vm-seal: the VM did not power off" >&2; exit 1; fi
|
||||||
|
rm -f "$state/qemu.pid"
|
||||||
|
|
||||||
|
# Direct boot skips the firmware and Limine, which spend most of a boot reading
|
||||||
|
# and hashing the UKI. resume= points at the image's own swap, so drop it.
|
||||||
|
objcopy -O binary --only-section=.linux "$state/uki.efi" "$out/vmlinuz"
|
||||||
|
objcopy -O binary --only-section=.initrd "$state/uki.efi" "$out/initrd"
|
||||||
|
objcopy -O binary --only-section=.cmdline "$state/uki.efi" "$state/cmdline"
|
||||||
|
tr -d '\0' <"$state/cmdline" | tr ' ' '\n' | { grep -vE '^(resume|resume_offset)=|^$' || true; } | paste -sd' ' >"$out/cmdline"
|
||||||
|
[[ -s $out/vmlinuz && -s $out/initrd && -s $out/cmdline ]] || { echo "vm-seal: the UKI gave no kernel, initrd or cmdline" >&2; exit 1; }
|
||||||
|
|
||||||
|
# zstd clusters: QEMU reads them as they are, so the download boots as it is.
|
||||||
|
qemu-img convert -c -O qcow2 -o compression_type=zstd -m 16 -W "$state/edit.qcow2" "$out/base.qcow2"
|
||||||
|
( cd "$out" && sha256sum base.qcow2 vmlinuz initrd cmdline >SHA256SUMS )
|
||||||
|
echo "sealed: $out ($(du -sh "$out/base.qcow2" | cut -f1)); cmdline: $(cat "$out/cmdline")"
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Boots a sealed image once, as a throwaway machine, and checks that it is
|
||||||
|
# what vm-seal promises: a desktop that comes up, nothing of the build left
|
||||||
|
# in it, and SSH that takes keys only. Exits non-zero on any failed check.
|
||||||
|
#
|
||||||
|
# vm-verify <sealed_dir>
|
||||||
|
set -euo pipefail
|
||||||
|
[[ $# -eq 1 ]] || { echo "Usage: vm-verify <sealed_dir>" >&2; exit 2; }
|
||||||
|
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
base=$(realpath "$1")
|
||||||
|
( cd "$base" && sha256sum --quiet -c SHA256SUMS )
|
||||||
|
uuid=$(cat /proc/sys/kernel/random/uuid)
|
||||||
|
export VM_UUID=$uuid SSH_PORT=$(( 20000 + RANDOM % 10000 ))
|
||||||
|
|
||||||
|
VM_PW=${GUEST_PASSWORD:-omarchy} "$here/vm-boot" "$base" "WANT_ID=${uuid//-/} PW=${GUEST_PASSWORD:-omarchy} bash -s" <<'GUEST'
|
||||||
|
fail=0
|
||||||
|
check() { # description, then a command that must succeed
|
||||||
|
local what=$1; shift
|
||||||
|
if "$@" >/dev/null 2>&1; then echo "ok $what"; else echo "FAIL $what"; fail=1; fi
|
||||||
|
}
|
||||||
|
root() { printf '%s\n' "$PW" | sudo -S -p '' "$@"; }
|
||||||
|
booted=$(( $(date +%s) - $(cut -d. -f1 /proc/uptime) - 5 ))
|
||||||
|
newer() { [ "$(root stat -c %Y "$1")" -ge "$booted" ]; } # written by this boot
|
||||||
|
|
||||||
|
check "the system reaches running or degraded" \
|
||||||
|
sh -c 's=$(timeout 120 systemctl is-system-running --wait); [ "$s" = running ] || [ "$s" = degraded ]'
|
||||||
|
check "no unit failed" sh -c '[ -z "$(systemctl --failed --no-legend)" ]'
|
||||||
|
check "a desktop session is logged in" sh -c 'for i in $(seq 1 60); do pgrep -x Hyprland && exit 0; sleep 0.5; done; exit 1'
|
||||||
|
check "the machine ID is the one this boot was given" [ "$(cat /etc/machine-id)" = "$WANT_ID" ]
|
||||||
|
check "the journal holds this machine only" [ "$(root ls /var/log/journal | tr '\n' ' ')" = "$WANT_ID " ]
|
||||||
|
check "SSH refuses passwords" sh -c "printf '%s\n' '$PW' | sudo -S -p '' sshd -T | grep -qix 'passwordauthentication no'"
|
||||||
|
check "SSH requires a key" sh -c "printf '%s\n' '$PW' | sudo -S -p '' sshd -T | grep -qix 'authenticationmethods publickey'"
|
||||||
|
# Asking with no method makes sshd list the ones it would take.
|
||||||
|
check "sshd offers a key as the only way in" \
|
||||||
|
sh -c 'ssh -o PreferredAuthentications=none -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null omarchy@127.0.0.1 true 2>&1 | grep -qF "Permission denied (publickey)."'
|
||||||
|
check "authorized_keys holds one key, this boot's" [ "$(grep -c . ~/.ssh/authorized_keys)" = 1 ]
|
||||||
|
check "SSH host keys were made by this boot" newer /etc/ssh/ssh_host_ed25519_key
|
||||||
|
check "NetworkManager's key was made by this boot" newer /var/lib/NetworkManager/secret_key
|
||||||
|
check "one DHCP lease, this boot's" [ "$(root sh -c 'ls /var/lib/NetworkManager/*.lease | wc -l')" = 1 ]
|
||||||
|
check "no shell history" sh -c '! [ -e ~/.bash_history ]'
|
||||||
|
check "the package cache is empty" [ -z "$(ls /var/cache/pacman/pkg)" ]
|
||||||
|
root mkdir -p /run/top && root mount -o subvolid=5,ro "$(findmnt -no SOURCE / | sed 's/\[.*//')" /run/top
|
||||||
|
check "the factory snapshot has no machine ID" sh -c "! printf '%s\n' '$PW' | sudo -S -p '' test -s /run/top/@factory/etc/machine-id"
|
||||||
|
check "the factory snapshot has no SSH host keys" sh -c "[ -z \"\$(printf '%s\n' '$PW' | sudo -S -p '' sh -c 'ls /run/top/@factory/etc/ssh/ssh_host_* 2>/dev/null')\" ]"
|
||||||
|
check "no snapshots besides the current system" [ -z "$(root ls /.snapshots)" ]
|
||||||
|
echo "omarchy $(pacman -Q omarchy 2>/dev/null | cut -d' ' -f2), kernel $(uname -r), $(systemd-analyze | head -1)"
|
||||||
|
exit $fail
|
||||||
|
GUEST
|
||||||
Reference in new issue
Block a user