Multi-architecture multi-distro in one root partition
Recently I needed to do offline OS maintainance work on quite a few of my devices, for which I used Ventoy + archiso on x86_64 for Debian 13 / Arch, and ALARM os drive on aarch64 for Debain 13 / ALARM. For which I find archiso more and more annoying as I had to re-do a lot of initial setups.
While I know these could be improved if I have a dedicated persistent fs for configs, or cloud-init scripts, or archiso boot parameters, I don’t quite want immutable live system for the work any more. So, I decided, what if I have a single drive, on which I have all of the following systems booting from the same root partition?
- Arch Linux x86_64 bootable via both UEFI and legacy
- Debian 13 x86_64 bootable via both UEFI and legacy
- Debian 13 aarch64 bootable via UEFI (and U-boot distroboot)
- Arch Linux ARM aarch64 bootable via both UEFI (and U-boot distroboot)
- And of course more!
Background knowledge
Before the actual installation, I’ll explain the background knowledge first. If you don’t bother with, skip to next chapter
Booting on x86_64 UEFI, without CSM
On x86_64 UEFI, without CSM, the booting process is quite simple:
- UEFI powers on and do preparation until booting logic is ready
- If quick boot is enabled, load only necessary drivers, and execute the first available (destination exists and binary exists) BootEntry in current BootOrder, if this succeeds then no remaining steps would be executed
- If external boot sources are available (e.g. network adapters with UEFI ROM) and not disabled, let them scan and register BootEntrys as needed; on most devices this step does not execute at all
- UEFI BIOS loads various drivers and scans for all drives for EFI partition, that is, MSDOS type “EFI (FAT-12/16/32)”, or GPT type “C12A7328-F81F-11D2-BA4B-00A0C93EC93B”, for each FAT fs with such type, open and search for EFI binary at removable path “EFI/BOOT/BOOTX64.EFI”, and register a BootEntry with generated name for it like “UEFI OS”; for different UEFI vendors the logic whether to register them with order earlier or later than existing entries are undetermined
- Go similarly as step 2
Note about MBR on UEFI: as the specification only required support for GPT, the support or no-support for MBR is undeterminable before you get your hands on the actual machine. Windows and systemd-boot simply refuses to install on MBR on UEFI. While all of my devices support such and I use it for local system installation on small drives, I would only focus on GPT on UEFI due to the fact that we want the result drive bootable on variuos machines.
Most of UEFI-compatible boot managers support to be (or has to be at least) installed at removable path. E.g. for grub (expecting EFI partiton mounted at /efi):
grub-install --target x86_64-efi --removable
On Debian 13 this installs the following files to /efi/EFI/BOOT:
BOOTX64.CSV: this contains required entry to be registered once shim atBOOTX64.EFIsuccessfully bootedBOOTX64.EFI: Debian’s signed shim, loads signedgrubx64.efi, and register entries according toBOOTX64.CSV; the one UEFI firmware would pick as removable EFIgrub.cfg: Grub’s config, it just tells grub to scan for real root and look up configs there. An example content:search.fs_uuid 91c69930-b508-4a42-b510-d63544d7eae0 root hd1,gpt2 set prefix=($root)'/boot/grub' configfile $prefix/grub.cfgGrub’s config files look like shell scripts and you can imagine
configfileassourcein shell, so in this case thegrub.cfgin EFI partition just records where to find the root partition (search for fs with uuid91c69930-b508-4a42-b510-d63544d7eae0and records the result in variableroot, if failed then use defaulthd1,gpt2), sets another variableprefix, being the path to folder/boot/grubunder that root fs, then “source” another configgrub.cfgunder there.grubx64.efi: Grub’s core EFI binary, signed by Debian, loadsgrub.cfg- The file would carry a built-in
$prefixvariable equalling/EFI/debianto instruct where to look up for a grub folder; in removable case it would instead try[ESP]/EFI/BOOT, so it looks upgrub.cfghere
- The file would carry a built-in
mmx64.efi: Machine owner key manager, only needed for secure boot, not needed for fully removable use cases, can be safely deleted
On Arch Linux this installs only BOOTX64.EFI, even the config needs to be manually created.
For a removable drive, on which we would have (some) kernels unsigned, we certainly would not want strict Secure Boot, neither would we want permissive Secure Boot with Machine Owner Key managed by ourselves. And we would not want it to register non-removable UEFI BootEntry s if possible.
The dependency tree in Debain’s Grub split packages make it really hard to install secure-boot-less under UEFI, as grub-efi-amd64-bin, a soft depened making grub-install --target x86_64-efi possible, hard depends grub-efi-amd64-signed. And the whole dependency tree becomes locked-in and almost impossible to uninstall due to they considered “essential”. And dpkg hook would “friendly” help use to re-install (update) Grub on version change, not respecting the existing layout. So in later steps we would install and manage Grub the boot loader part from Arch Linux, and install only the booting configuration generation part on Debian.
Don’t worry about “Debian not having its own Grub”. It is $prefix/grub.cfg that normally grub-mkconfig / update-grub updates, these are managed by sytem themselves, and the configuration tool would still be installed.
Other than the per-system grub.cfg, we would want an “outer grub.cfg”, which is directly used by Grub, containing menuentry to instruct which sub grub.cfg to redirect to.
The reason we choose Grub rather than other booting methods:
- While systemd-boot is also a candidate on x86_64 UEFI, the straight-forward installtion tool
bootctldoes not natively support a removable option (so it must write a BootEntry at installation which I find quite annoying); although you could manually copy the binary to the removable EFI path, no maintainance can be done easily withbootctl update; also systemd-boot does not support legacy BIOS so we have to maintain more booting configs, which is a hassle itself - Placing a unified kernel image to the removable place is OK if you only want a single distro, but is impossible if we want multiple distro (well technically you can use kernel image cross-distro, but good luck updating them)
In summary, for the multi-boot logic we would need: GPT + EFI partition + one single removable Grub EFI binary + one single grub.cfg as menuentry selector + one grub.cfg per system maintained by the system itself
Booting on x86 legacy BIOS or on x86_64 UEFI with CSM
This is still “simple” but not straight-forward in the modern perspective. Still, let’s write the main ideas down:
- BIOS powers on and do preparation until booting logic is ready
- BIOS registers newly found drives into its pool, not necessarily at last positions
- For each target in the booting order configuration, if not drive, delegate to external source (e.g. network manager with booting ROM), otherwide load the Master Boot Record (MBR) on sector 0 and try to execute it; in most cases this wouldn’t return even if the MBR is not technically bootable (some partition tools would place a binary here to print “unbootable device”, and for some this means hang i.e. soft locked)
- If all drives in booting order failed, print “no bootable drives found” and hang
Note while MBR was mentioned above, the partition table on the drive does not have to be MBR / msdos. The BIOS actually knows nothing above the partition but rather just reads stuff from fixed offsets (think that as “as-if MBR0”, in fact the whole drive can be a “super floppy” i.e. fs on whole drive, and as long as sector 0 is available it does not matter).
So most of the legacy-BIOS-compatible boot loaders need to be installed to MBR, or also technically the whole drive, e.g. for Grub:
grub-install --target i386-pc /dev/sda
The binary that Grub installs into MBR / sector 0 is called boot.img by Grub itself, the functionality is similar to grubx64.efi in the UEFI case: to load necessary drivers to look up real grub.cfg. But as MBR sector 0 is too small (512 Byte) it’s impossible to do it by the small boot.img itself. For this another part of binary called core.img needs to be looked up and executed. Grub does this differently depending on whether you’re booting on MBR or GPT:
- on MBR,
core.imgis stored from sector 1 onward, before first partition, the space is 1 MiB - 512B, and in real world would never be fully utilized. - on GPT,
core.imgis stored in the partition with typeBIOS boot(21686148-6449-6E6F-744E-656564454649), for the same reason this can be only 1 MiB
Of course core.img itself would carry metadata including how large the actual data is, fs drivers so actual root partition could be opened, and optionally a built-in config.
The logic is simply, boot.img -> core.img -> $prefix/grub.cfg, after that latter steps are similar to UEFI cases.
Similar as UEFI, we would want a single outer Grub with config managed by ourselves to function as selector, and each system managing their own inner grub.cfg (but not having their Grub boot manager) ready to be picked up by ourselves.
The reason we choose Grub rather than other booting methods:
- While syslinux can also provide menu, and is pretty KISS, it would need seperate config, instead of reusing the same config as UEFI
In summary, for the multi-boot logic we would need: GPT + BIOS Boot partition + one single Grub binary in MBR0 and BIOS Boot partition + one single grub.cfg as menuentry selector + one grub.cfg per system maintained by the system itself
Booting on AArch64, UEFI or U-Boot distroboot
Some AArch64 devices support UEFI, many others don’t and use U-Boot. In not-too-old U-Boot builds, the “distroboot” concept scans for various bootable targets, and removable EFI binary EFI/BOOT/BOOTAA64.EFI is one of them, besides extlinux.conf and boot.scr(.uimg) . I build u-boot myself on all of SBCs and TV boxes on my hand and they all support such without explicitly enabling. As we want multi-boot on a single drive on both AArch64 and x86_64 we would only focus the UEFI and distroboot UEFI.
On AArch64 UEFI, the steps are similar as x86_64, except that the fallback EFI binary name is BOOTAA64.EFI
On AArch64 U-Boot that chainloads removable EFI in distroboot logic:
- U-Boot powers on and do preparation until booting logic is ready
- Loads environment variables either from persistent storage or from built-in, in both cases to memory
- Run environment variable
bootcmd, in modern cases,bootcmd='bootflow scan -lb' - So, run
bootflow, the argumentscanmeans to scan all possible sources, in most cases these include all block devices first, then network; the argument-lmeans to print each scanned bootablt target; the argument-bmeans for each target scanned, try to boot immediately. - Let’s only focus on removable/fallback EFI binary, and assumes it being the only possible target and scanned
- Prepare some “UEFI” environments and “UEFI services”, then loads the EFI binary in, then execute it.
Note specifically for per-device DTB to be applied correctly in the U-Boot case, if /boot is in root fs, the job cannot be done by Grub (cannot expect a partition to be readable before you could even tell there’s a block device), rather the DTB has to be loaded by U-Boot
Drive preparation
Boot archiso or do this in a device already running Linux.
Run your perferred partition tool to partition the drive with the following partitions:
- 100 MiB EFI system partition
- 1 MiB BIOT boot partition
- Remaining as a single root partition
- Others as you like
Or simply save the following infos in a temporary file e.g. parts.info:
label: gpt
unit: sectors
sector-size: 512
size=204800, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
size=2048, type=21686148-6449-6E6F-744E-656564454649
type=0FC63DAF-8483-4772-8E79-3D69D8477DE4
Then run sfdisk to format use the info:
sfdisk /dev/[drive] < parts.info
The result partitions shall look like the following (/dev/vda is used in the following example):
Checking that no-one is using this disk right now ... OK
Disk /dev/[drive]: 64 GiB, 68719476736 bytes, 134217728 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
>>> Script header accepted.
>>> Script header accepted.
>>> Script header accepted.
>>> Created a new GPT disklabel (GUID: D8997F52-AFD6-4A0F-845F-FF12CDD718F6).
/dev/[drive]1: Created a new partition 1 of type 'EFI System' and of size 100 MiB.
/dev/[drive]2: Created a new partition 2 of type 'BIOS boot' and of size 1 MiB.
/dev/[drive]3: Created a new partition 3 of type 'Linux filesystem' and of size 63.9 GiB.
/dev/[drive]4: Done.
New situation:
Disklabel type: gpt
Disk identifier: D8997F52-AFD6-4A0F-845F-FF12CDD718F6
Device Start End Sectors Size Type
/dev/[drive]1 2048 206847 204800 100M EFI System
/dev/[drive]2 206848 208895 2048 1M BIOS boot
/dev/[drive]3 208896 134215679 134006784 63.9G Linux filesystem
The partition table has been altered.
Create a FAT fs on the ESP:
mkfs.vfat /dev/[drive]1
Create a Btrfs on the root partition:
mkfs.btrfs /dev/[drive]3
Note about Btrfs compression: if you want it, you could set it at fs-creation time, e.g.:
mkfs.btrfs --compress zstd:15 /dev/[drive]3
But the later steps assume this not set and we would write the compression level manually at mount time and in fstab. You can omit those if you did this at fs-creation time.
Now mount the Btrfs root partition to somewhere, we need to create some subvols
mount --mkdir /dev/[drive]3 /mnt/manyos
cd /mnt/manyos
Let’s create subvols. The main focus is that we want seperate root volumes for each system, and shared home. (@ or + is not strictly needed in subvol names, but it helps to tell them from plain folders)
- The generic one subvol per system except shared home style (very simple for latter mounting and fstab):
btrfs subvolume create @arch @debian @home - My style:
mkdir shared arch-x86_64 debian-x86_64 alarm-aarch64 debian-aarch64 btrfs subvolume create shared/@{home{,_.snapshots},etc_ssh} {arch-x86_64,debian-x86_64,alarm-aarch64,debian-aarch64}/{@{,.snapshots},+nocow} chattr +C {arch-x86_64,debian-x86_64,alarm-aarch64,debian-aarch64}/+nocow mkdir -p arch-x86_64/+nocow/var/{cache,log,spool,tmp} chmod 1777 arch-x86_64/+nocow/var/tmp mkdir template tar -f template/nocow.tar -C arch-x86_64/+nocow -cv . for i in arch-x86_64/@ {debian-x86_64,alarm-aarch64,debian-aarch64}/{@,+nocow}; do tar -f template/nocow.tar -C $i -xv; doneThe layout shall look the the following:
> tree . ├── arch-x86_64 │ ├── @ │ │ └── var │ │ ├── cache │ │ ├── log │ │ ├── spool │ │ └── tmp │ ├── +nocow │ │ └── var │ │ ├── cache │ │ ├── log │ │ ├── spool │ │ └── tmp │ └── @.snapshots ├── ... ├── shared │ ├── @home │ └── @home_.snapshots └── template └── nocow.tar 34 directories, 1 fileThe benefit of the above layout is that stuffs not needing snapshot and compression are enclosed into a single
+nocowsubvol and bind-mounting is used instead of many subvols; and system-specific stuffs are enclosed into one single top-level folder, and shared stuffs not so, and .snapshots subvol for snapper is pre-created.
In later steps I would follow only my own style
Installing Arch Linux x86_64
As discussed earlier we would use Arch Linux as the one to install and maintain Grub x86_64 (as, Grub almost HAS TO BE installed as Secure Boot with shim, which we don’t need at all), so let’s install Arch Linux first.
Mainly you shall follow the official Installation Guide for the most part, to boot archiso on x86_64 and install in UEFI style; of course you could also do this on a device already running Linux. I would only cover the archiso case.
Let’s focus on things that shall go differently from the official way:
- Follow the official guide, until before “Partition the disks”
- Skip “Partition the disks”
- Skip “Format the partitions”
- To mount my layout, mount root and nocow subvol first:
mount -o compress=zstd:15,subvol=arch-x86_64/@ --mkdir /dev/[drive]3 /mnt/root mount -o subvol=arch-x86_64/+nocow --mkdir /dev/[drive]3 /mnt/arch-x86_64+nocowThen pre-create fstab and edit it:
mkdir /mnt/root/etc cp /etc/fstab /mnt/root/etc/ vim /mnt/root/etc/fstabRemember to use vim’s functionality to do multi-line edit (shift + v, crtl + v, etc) and the ability to pipe content into external command (select in multi-line visual, then
:, then!column -tto force a table look)The content shall look like the following (remember to use
blkidto acquire your real UUID for root partition and ESP)# Static information about the filesystems. # See fstab(5) for details. # <file system> <dir> <type> <options> <dump> <pass> UUID=6894094c-a75e-4f1a-b228-283faf7bf003 / btrfs rw,compress=zstd:15,subvol=arch-x86_64/@ 0 0 UUID=6894094c-a75e-4f1a-b228-283faf7bf003 /.snapshots btrfs rw,compress=zstd:15,subvol=arch-x86_64/@.snapshots 0 0 UUID=6894094c-a75e-4f1a-b228-283faf7bf003 /home btrfs rw,compress=zstd:15,subvol=shared/@home 0 0 UUID=6894094c-a75e-4f1a-b228-283faf7bf003 /home/.snapshots btrfs rw,compress=zstd:15,subvol=shared/@home_.snapshots 0 0 UUID=6894094c-a75e-4f1a-b228-283faf7bf003 /mnt/arch-x86_64+nocow btrfs rw,compress=zstd:15,subvol=arch-x86_64/+nocow 0 0 /mnt/arch-x86_64+nocow/var/cache /var/cache none bind,private 0 0 /mnt/arch-x86_64+nocow/var/log /var/log none bind,private 0 0 /mnt/arch-x86_64+nocow/var/spool /var/spool none bind,private 0 0 /mnt/arch-x86_64+nocow/var/tmp /var/tmp none bind,private 0 0 UUID=5D03-ED7A /efi vfat rw,noatime 0 2Then mount everything remaining up:
mount --all --fstab /mnt/root/etc/fstab --target-prefix /mnt/root --mkdirA
lsblkshall look like following now:# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS vda 254:0 0 128G 0 disk ├─vda1 254:1 0 100M 0 part /mnt/root/efi ├─vda2 254:2 0 1M 0 part └─vda3 254:3 0 127.9G 0 part /mnt/root/var/tmp /mnt/root/var/spool /mnt/root/var/log /mnt/root/var/cache /mnt/root/mnt/arch-x86_64+nocow /mnt/root/home/.snapshots /mnt/root/home /mnt/root/.snapshots /mnt/arch-x86_64+nocow /mnt/root - Continue from “Installation”, I’d recommend to choose the following bootstrap packages (pre-configure booster so initramfs is only generated once for universal, skipping the non-universal one):
echo 'universal: true' > /mnt/root/etc/booster.yaml pacstrap -K /mnt/root base booster linux intel-ucode amd-ucode linux-firmware btrfs-progs dosfstools grub vim sudo - Skip “Configure the system / Fstab”, the fstab generated on our Btrfs layout is pretty messy, just use our own
- Continue from “Chroot”,
arch-chroot /mnt/rootand do the remaining parts, until before “Network configuration” - For “Network configuration”, the hostname could be unique for each system, or same, depending on your need; for network manager, I’d recommend just use
systemd-networkd, so:- Enable networkd and resolved:
systemctl enable systemd-{network,resolve}d - Quit from chroot
- Copy archiso’s network config files:
cp -rva /etc/systemd/network/* /mnt/root/etc/systemd/network/ - Re-link resolv.conf
ln -sf /run/systemd/resolve/stub-resolv.conf /mnt/root/etc/resolv.conf - Re-enter chroot
- Enable networkd and resolved:
- Skip “Initramfs”, we’re using
boosterinstead of the defaultmkinitcpio, and the universal initramfs was already created - For “Boot Loader”, we would install grub, the package was already installed into root in earlier bootstrap steps, we only need to install it to bootable:
- Install as removable EFI, note we also specify
--boot-directory /efi, sogrubmodules and first-stage config are saved and loaded from there. We would only want each system’s/bootto store there boot configgrub-install --removable --efi-directory /efi --boot-directory /efi - Install to MBR, similarly note we also specify
--boot-directory /efigrub-install --target i386-pc --boot-directory /efi /dev/[drive] - Hack/fix
/etc/grub.d/10_linuxso it would prefer booster initramfs (without this, booster initramfs would be hidden in a submenu) and ro root; if you’re not using booster only or you don’t require ro root on boot, you can skip this:From 9ad850b2b8842bb673313be08f6a5af66cdf12ea Mon Sep 17 00:00:00 2001 From: Guoxin Pu <pugokushin@gmail.com> Date: Thu, 13 Nov 2025 15:52:27 +0800 Subject: [PATCH] use booster as main initramfs and prefer ro --- 10_linux | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/10_linux b/10_linux index e16cea8..3ce3a9d 100755 --- a/10_linux +++ b/10_linux @@ -147,7 +147,7 @@ linux_entry () message="$(gettext_printf "Loading Linux %s ..." ${version})" sed "s/^/$submenu_indentation/" << EOF echo '$(echo "$message" | grub_quote)' - linux ${rel_dirname}/${basename} root=${linux_root_device_thisversion} rw ${args} + linux ${rel_dirname}/${basename} root=${linux_root_device_thisversion} ro ${args} EOF if test -n "${initrd}" ; then # TRANSLATORS: ramdisk isn't identifier. Should be translated. @@ -227,7 +227,7 @@ for linux in ${reverse_sorted_list}; do done initrd_real= - for i in "initrd.img-${version}" "initrd-${version}.img" \ + for i in "booster-${version}.img" "initrd.img-${version}" "initrd-${version}.img" \ "initrd-${alt_version}.img.old" "initrd-${version}.gz" \ "initrd-${alt_version}.gz.old" "initrd-${version}" \ "initramfs-${version}.img" "initramfs-${alt_version}.img.old" \ @@ -304,28 +304,6 @@ for linux in ${reverse_sorted_list}; do linux_entry "${OS}" "${version}" advanced \ "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" - if test -e "${dirname}/initramfs-${version}-fallback.img" ; then - initrd="${initrd_early} initramfs-${version}-fallback.img" - - if test -n "${initrd}" ; then - gettext_printf "Found fallback initrd image(s) in %s:%s\n" "${dirname}" "${initrd_extra} ${initrd}" >&2 - fi - - linux_entry "${OS}" "${version}" fallback \ - "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" - fi - - if test -e "${dirname}/booster-${version}.img" ; then - initrd="${initrd_early} booster-${version}.img" - - if test -n "${initrd}" ; then - gettext_printf "Found booster initrd image(s) in %s:%s\n" "${dirname}" "${initrd_extra} ${initrd}" >&2 - fi - - linux_entry "${OS}" "${version}" booster \ - "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" - fi - if [ "x${GRUB_DISABLE_RECOVERY}" != "xtrue" ]; then linux_entry "${OS}" "${version}" recovery \ "${GRUB_CMDLINE_LINUX_RECOVERY} ${GRUB_CMDLINE_LINUX}" -- 2.51.2 - Generate
grub.cfgfor Arch Linux as how you do it on a normal installation:mkdir /boot/grub grub-mkconfig -o /boot/grub/grub.cfgNote this would not be the first config Grub loads, but rather just a “to-be-included” config.
- Let’s write a real, outer config for Grub to achieve menu logic by
vim /efi/grub/grub.cfgwith content like following:search.fs_uuid 6894094c-a75e-4f1a-b228-283faf7bf003 root hd0,gpt2 terminal_input console terminal_output console set suffix='@/boot/grub/grub.cfg' menuentry 'Arch Linux (x86_64)' { configfile ($root)/arch-x86_64/$suffix }
- Install as removable EFI, note we also specify
- Finalize the installation, umount everything and poweroff
After the above steps we shall have a UEFI + legacy bootable, you can boot on different machines to validate it, the boot menu shall look like this on both UEFI and legacy:
GNU GRUB version 2:2.14rc1-2
┌────────────────────────────────────────────────────────────────────────────┐
│*Arch Linux (x86_64) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Use the ▲ and ▼ keys to select which entry is highlighted.
Press enter to boot the selected OS, `e' to edit the commands
before booting or `c' for a command-line.
And after pressing Enter it’s the same old Arch Linux Grub menu as always.
While we’re at it, you can of course add timeout, decoration, etc to the outer menu. I’ll stick with the simple look and continue.
After-installation for Arch Linux x86_64
This part is mostly about quality of life improvement and can be skipped. If you want to follow, boot into the installed Arch Linux.
sshd config + key
pacman -S openssh
systemctl enable --now sshd
Verify that sshd pubkeys are generated successfully:
# ls /etc/ssh
moduli ssh_config ssh_config.d sshd_config sshd_config.d ssh_host_ecdsa_key ssh_host_ecdsa_key.pub ssh_host_ed25519_key ssh_host_ed25519_key.pub ssh_host_rsa_key ssh_host_rsa_key.pub
# systemctl status sshdgenkeys
○ sshdgenkeys.service - SSH Key Generation
Loaded: loaded (/usr/lib/systemd/system/sshdgenkeys.service; disabled; preset: disabled)
Active: inactive (dead) since Thu 2025-11-13 16:26:26 CST; 1min 51s ago
Invocation: ef2323bd8d4e4007be02a0e3208f3c31
Process: 578 ExecStart=/usr/bin/ssh-keygen -A (code=exited, status=0/SUCCESS)
Main PID: 578 (code=exited, status=0/SUCCESS)
Mem peak: 1.7M
CPU: 84ms
Nov 13 16:26:25 dud systemd[1]: Starting SSH Key Generation...
Nov 13 16:26:26 dud ssh-keygen[578]: ssh-keygen: generating new host keys: RSA ECDSA ED25519
Nov 13 16:26:26 dud systemd[1]: sshdgenkeys.service: Deactivated successfully.
Nov 13 16:26:26 dud systemd[1]: Finished SSH Key Generation.
Do required sshd_config modification as needed:
vim /etc/ssh/sshd_config
- I would replace
AuthorizedKeysFile .ssh/authorized_keys->AuthorizedKeysFile /etc/ssh/authorized_keys/%u, so no one can add their SSH pubkey except root (me); and I would prepare this folder as needed - I would uncomment
#PasswordAuthenticationand setPasswordAuthentication noso password login is disabled
After the modification restart sshd so the changes take effect:
systemctl restart sshd
snapshot on boot
For the setup I would want after-boot snapshots to be taken, so install snapper:
pacman -S snapper
Create configs and enable needed services
vim /etc/snapper/configs/root
vim /etc/snapper/configs/home
The content shall look like the following:
SUBVOLUME="/"
FSTYPE="btrfs"
QGROUP=""
SPACE_LIMIT="0.5"
FREE_LIMIT="0.2"
ALLOW_USERS=""
ALLOW_GROUPS=""
SYNC_ACL="no"
BACKGROUND_COMPARISON="yes"
NUMBER_CLEANUP="yes"
NUMBER_MIN_AGE="1800"
NUMBER_LIMIT="50"
NUMBER_LIMIT_IMPORTANT="10"
EMPTY_PRE_POST_CLEANUP="yes"
EMPTY_PRE_POST_MIN_AGE="1800"
SUBVOLUME="/home"
FSTYPE="btrfs"
QGROUP=""
SPACE_LIMIT="0.5"
FREE_LIMIT="0.2"
ALLOW_USERS=""
ALLOW_GROUPS=""
SYNC_ACL="no"
BACKGROUND_COMPARISON="yes"
NUMBER_CLEANUP="yes"
NUMBER_MIN_AGE="1800"
NUMBER_LIMIT="50"
NUMBER_LIMIT_IMPORTANT="10"
EMPTY_PRE_POST_CLEANUP="yes"
EMPTY_PRE_POST_MIN_AGE="1800"
And edit the global config to enable these profiles:
vim /etc/conf.d/snapper
With the following line:
SNAPPER_CONFIGS="root home"
By default snapper-boot only snapshots root, so modify the unit:
systemctl edit snapper-boot
The result shall look like this (ExecStart is appended after the original ExecStarted):
> cat /etc/systemd/system/snapper-boot.service.d/override.conf
[Service]
ExecStart=/usr/bin/snapper --config home create --cleanup-algorithm number --description "boot"
Then let’s enable needed units:
systemctl enable --now snapper-{boot,cleanup}.timer
Installing Debian x86_64
Let’s do this on Arch Linux x86_64 with debootstrap and arch-chroot
Of course the tools shall be installed first:
pacman -S debootstrap arch-install-scripts
The installation goes similarly as Arch, but keep the following points in mind:
- After chroot, do
export PATH=with missing/sbinparts, whole command in later steps - Do not install any boot manager! The only system here that has a boot manager installed is Arch x86_64.
The steps are as follows:
- Similarly, mount root and nocow subvol first:
mount -o subvol=debian-x86_64/@ --mkdir /dev/[drive]3 /mnt/root mount -o subvol=debian-x86_64/+nocow --mkdir /dev/[drive]3 /mnt/debian-x86_64+nocowThen duplicate fstab:
mkdir /mnt/root/etc sed 's/arch/debian/g' /etc/fstab > /mnt/root/etc/fstabThen mount everything remaining up:
mount --all --fstab /mnt/root/etc/fstab --target-prefix /mnt/root --mkdirA
lsblkshall look like following now:# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS vda 254:0 0 128G 0 disk ├─vda1 254:1 0 100M 0 part /mnt/root/efi │ /efi ├─vda2 254:2 0 1M 0 part └─vda3 254:3 0 127.9G 0 part /mnt/root/var/tmp /mnt/root/var/spool /mnt/root/var/log /mnt/root/var/cache /mnt/root/mnt/debian-x86_64+nocow /mnt/root/home/.snapshots /mnt/root/home /mnt/root/.snapshots /mnt/debian-x86_64+nocow /mnt/root /var/tmp /var/spool /var/log /home/.snapshots /var/cache /mnt/arch-x86_64+nocow /home /.snapshots / - Do
debootstrapinto the rootdebootstrap trixie /mnt/root http://[mirror_link] - chroot into
/mnt/root, and setPATHarch-chroot /mnt/rootexport PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binThe PATH needs to be set as
arch-chrootkeeps PATH from Arch, which lackssbin, but on Debiansbinis a seperate folder thanbin - Install a few missing packages
apt update apt install vim locales systemd-timesyncd btrfs-progs dosfstools linux-image-amd64 amd64-microcode intel-microcode firmware-linux - Do timezone, locale, hostname setup just like how you did it in Arch
- For network manager, similarly, I’d recommend
systemd-networkd+systemd-resolvedpair, but on Debianresolvedneeds to be installed seperately:apt install systemd-resolved - Exit from chroot and borrow host Network configuration and re-link resolv.conf just like how we did for Arch x86_64 above, then re-enter chroot
- Now, the Grub, we only need Debian to generate
grub.cfg, but definitely not intalling and maintaining the actual Grub boot manager, so install only the system integration part and prepare the folder manually:apt install grub2-common mkdir /boot/grub vim /etc/default/grubWith this setup there would be no pre-configured
grubconfig, so use the following as a starting point:GRUB_DEFAULT=0 GRUB_TIMEOUT=1 GRUB_DISTRIBUTOR=`( . /etc/os-release && echo ${NAME} )` GRUB_CMDLINE_LINUX_DEFAULT="audit=0" GRUB_CMDLINE_LINUX="" GRUB_TERMINAL=consoleRemember to re-generate the one included by our outer grub
update-grub - Exit from chroot and update our outer Grub config
/efi/grub/grub.cfgto include a new menuentry:menuentry 'Debian (x86_64)' { configfile ($root)/debian-x86_64/$suffix } - Finalize the installation, umount everything and poweroff
After the above steps we shall have a UEFI + legacy bootable Arch Linux + Debian installation, you can boot on different machines to validate it, the boot menu shall look like this on both UEFI and legacy:
GNU GRUB version 2:2.14rc1-2
┌────────────────────────────────────────────────────────────────────────────┐
│*Arch Linux (x86_64) │
│ Debian (x86_64) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Use the ▲ and ▼ keys to select which entry is highlighted.
Press enter to boot the selected OS, `e' to edit the commands
before booting or `c' for a command-line.
And after selecting Debian and pressing Enter it’s the same old Debian Grub menu as always.
After-installation for Debian x86_64
This part is mostly about quality of life improvement and can be skipped. If you want to follow, boot into the installed Debian and do similar things as in After-installation for Arch Linux x86_64, but with the following differences:
- If you want to share hostname, it’s better to share host keys, i.e.
/etc/ssh/ssh_host_*_key{,.pub}, so clients would not complain about host key differing - For snapper, Debian comes with all units pre-enabled, disable the timeline as we want only on-boot:
systemctl disable --now snapper-timeline.timer; and note the snapper config folder is the same but the global config is at/etc/default/snapper - While
debootstrapstill prepares an old-style APTsources.list, I recommend to migrate to new APT config style:rm /etc/apt/sources.list vim /etc/apt/sources.list.d/debian.sourcesWith content like the following:
Types: deb URIs: http://[mirror]/debian/ Suites: trixie trixie-updates Components: main contrib non-free non-free-firmware Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg Types: deb URIs: http://[mirror]/debian-security/ Suites: trixie-security Components: main contrib non-free non-free-firmware Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg Types: deb URIs: http://[mirror]/debian/ Suites: trixie-backports Components: main contrib non-free non-free-firmware Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg - It’s good if you remember that we did not configure initramfs-tools to create generic initramfs. In most cases it comes with pre-configured
MODULES=mostand you would not need to modify that, only withdebian-installcould this be set toMODULES=deps. To verify,vim /etc/initramfs-tools/initramfs.confand checkMODULES. If it’sMODULES=depsthen modify it, save, and runupdate-initramfs -u - There’re many firmware not installed by
firmware-linuxmeta package, in most cases these are not needed, if you really really need all of them installed:apt install $(for i in $(apt-cache search '^firmware-' | cut -d ' ' -f 1); do dpkg-query -W -f='${Status}' $i &>/dev/null || echo $i; done | grep -v installer)
Installing Arch Linux ARM aarch64
Let’s do this on Arch Linux x86_64
- Install dependencies first
pacman -S qemu-user-static-binfmt arch-install-scripts - Duplicate host pacman config and do necessary modification
cp /etc/pacman.conf pacman-alarm.conf vim pacman-alarm.conf- Set
Architecture = aarch64instead ofauto - Set repo
Inlucde = mirrorlist-alarminstead of/etc/pacman.d/mirrorlist - Temporarily set
SigLevel = Neveras we don’t have ALARM keyring on Arch and adding those to Arch host would mess up the host, pacakges can later be re-verified once we have installed ALARM - Add my repo, we would use
linux-aarch64-7jias kernel, instead of ALARM’s officiallinux-aarch64, the latter misses some drivers built-in and would not boot on some of my SBCs and it has max CPUs set to a small number so would not work nicely on VM either, and the worst is it has some naive hooks to always expectmkinitcpioinstead of any other initramfs maker.[7Ji] Include = mirrorlist-3rdparty
- Set
- Create mirrorlist for alarm
vim mirrorlist-alarmWith server:
Server = http://[mirror]/archlinuxarm/$arch/$repo - Create mirrorlist for 3rdparty repo
vim mirrorlist-3rdpartyWith server:
Server = http://[mirror]/$repo/$arch - Mount the root tree
mount -o subvol=alarm-aarch64/@ --mkdir /dev/[drive]3 /mnt/root mount -o subvol=alarm-aarch64/+nocow --mkdir /dev/[drive]3 /mnt/alarm-aarch64+nocowThen duplicate fstab:
mkdir /mnt/root/etc sed 's/arch-x86_64/alarm-aarch64/g' /etc/fstab > /mnt/root/etc/fstabThen mount everything remaining up:
mount --all --fstab /mnt/root/etc/fstab --target-prefix /mnt/root --mkdirA
lsblkshall look like following now:# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS vda 254:0 0 128G 0 disk ├─vda1 254:1 0 100M 0 part /mnt/root/efi │ /efi ├─vda2 254:2 0 1M 0 part └─vda3 254:3 0 127.9G 0 part /mnt/root/var/tmp /mnt/root/var/spool /mnt/root/var/log /mnt/root/var/cache /mnt/root/mnt/alarm-aarch64+nocow /mnt/root/home/.snapshots /mnt/root/home /mnt/root/.snapshots /mnt/alarm-aarch64+nocow /mnt/root /var/tmp /var/spool /var/log /home/.snapshots /var/cache /mnt/arch-x86_64+nocow /home /.snapshots / - Do
pacstrapinto the rootecho 'universal: true' > /mnt/root/etc/booster.yaml pacstrap -C pacman-alarm.conf -K -M /mnt/root base booster linux-aarch64-7ji linux-firmware btrfs-progs dosfstools grub vim sudo archlinuxarm-keyring 7ji-keyring - Re-edit (or duplicate from CWD) the pacman configs and mirrorlist under
/mnt/root/etc, as now they come the official package, remember to add 7Ji repovim /mnt/root/etc/pacman.conf vim /mnt/root/etc/pacman.d/mirrorlist vim /mnt/root/etc/pacman.d/mirrorlist-3rdparty - Chroot into the target and confirm we’re running as aarch64
arch-chroot /mnt/rootuname -mNote: as we’re using qemu-static, the stdin/out is technically not directly attached to our terminal, so text editing could be a pain. If text editing is needed, do it from another SSH session from host under
/mnt/rootinstead so VIM and Nano could correctly write to terminal. - Initalize the keyring
pacman-key --init pacman-key --populate - Let’s actually verify the packages, now that we’ve nitialized the keyring
pacman -Sy --downloadonly $(sed -n '/^%NAME%/{n;p}' /var/lib/pacman/local/*/desc) - Continue and finish the setup just like how we did in Arch Linux, until before the boot manager
- Similar to Arch Linux x86_64, let’s install and hack Grub
- Install as removable EFI, note we also specify
--boot-directory /efi, sogrubmodules and first-stage config are saved and loaded from there. We would only want each system’s/bootto store there boot configgrub-install --removable --efi-directory /efi --boot-directory /efi - Hack/fix
/etc/grub.d/10_linuxso it would prefer booster initramfs (without this, booster initramfs would be hidden in a submenu) and ro root; if you’re not using booster only or you don’t require ro root on boot, you can skip this:From 9ad850b2b8842bb673313be08f6a5af66cdf12ea Mon Sep 17 00:00:00 2001 From: Guoxin Pu <pugokushin@gmail.com> Date: Thu, 13 Nov 2025 15:52:27 +0800 Subject: [PATCH] use booster as main initramfs and prefer ro --- 10_linux | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/10_linux b/10_linux index e16cea8..3ce3a9d 100755 --- a/10_linux +++ b/10_linux @@ -147,7 +147,7 @@ linux_entry () message="$(gettext_printf "Loading Linux %s ..." ${version})" sed "s/^/$submenu_indentation/" << EOF echo '$(echo "$message" | grub_quote)' - linux ${rel_dirname}/${basename} root=${linux_root_device_thisversion} rw ${args} + linux ${rel_dirname}/${basename} root=${linux_root_device_thisversion} ro ${args} EOF if test -n "${initrd}" ; then # TRANSLATORS: ramdisk isn't identifier. Should be translated. @@ -227,7 +227,7 @@ for linux in ${reverse_sorted_list}; do done initrd_real= - for i in "initrd.img-${version}" "initrd-${version}.img" \ + for i in "booster-${version}.img" "initrd.img-${version}" "initrd-${version}.img" \ "initrd-${alt_version}.img.old" "initrd-${version}.gz" \ "initrd-${alt_version}.gz.old" "initrd-${version}" \ "initramfs-${version}.img" "initramfs-${alt_version}.img.old" \ @@ -304,28 +304,6 @@ for linux in ${reverse_sorted_list}; do linux_entry "${OS}" "${version}" advanced \ "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" - if test -e "${dirname}/initramfs-${version}-fallback.img" ; then - initrd="${initrd_early} initramfs-${version}-fallback.img" - - if test -n "${initrd}" ; then - gettext_printf "Found fallback initrd image(s) in %s:%s\n" "${dirname}" "${initrd_extra} ${initrd}" >&2 - fi - - linux_entry "${OS}" "${version}" fallback \ - "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" - fi - - if test -e "${dirname}/booster-${version}.img" ; then - initrd="${initrd_early} booster-${version}.img" - - if test -n "${initrd}" ; then - gettext_printf "Found booster initrd image(s) in %s:%s\n" "${dirname}" "${initrd_extra} ${initrd}" >&2 - fi - - linux_entry "${OS}" "${version}" booster \ - "${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}" - fi - if [ "x${GRUB_DISABLE_RECOVERY}" != "xtrue" ]; then linux_entry "${OS}" "${version}" recovery \ "${GRUB_CMDLINE_LINUX_RECOVERY} ${GRUB_CMDLINE_LINUX}" -- 2.51.2 - Generate
grub.cfgfor Arch Linux ALARM as how you do it on a normal installation:mkdir /boot/grub grub-mkconfig -o /boot/grub/grub.cfgNote this would not be the first config Grub loads, but rather just a “to-be-included” config.
- Add a menuentry in outer Grub config:
menuentry 'Arch Linux ARM (aarch64)' { configfile ($root)/alarm-aarch64/$suffix }
- Install as removable EFI, note we also specify
- Finalize the installation, umount everything and poweroff
The drive should now work on an aarch64 VM (pure UEFI), but not on real hardware (U-boot faking UEFI), due to missing DTB. Verify it on VM first before trying on real hardware. (Remeber to disable Secure Boot first)
The Grub menu shall look like the following in VM:
GNU GRUB version 2:2.14rc1-2
┌────────────────────────────────────────────────────────────────────────────┐
│*Arch Linux (x86_64) │
│ Debian (x86_64) │
│ Arch Linux ARM (aarch64) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Use the ▲ and ▼ keys to select which entry is highlighted.
Press enter to boot the selected OS, `e' to edit the commands
before booting or `c' for a command-line.
And after selecting Arch Linux ARM and pressing Enter it’s the same old Arch Grub menu (well ALARM didn’t bother to modify the look) as always.
Now, for U-Boot to correctly work, U-Boot itself needs to load the specific DTB for device. We don’t want Grub to load DTB from root AFTER U-Boot loads Grub, as the device must be fully functional at the time Grub wants to open the Btrfs root, and that’s too late.
In modern-day U-Boot, there would be these built-in variables:
efi_dtb_prefixes=/ /dtb/ /dtb/current/-> Same across buildsfdtfile=amlogic/meson-sm1-bananapi-m5.dtb-> Unique for each boardscan_dev_for_efi=setenv efi_fdtfile ${fdtfile}; for prefix in ${efi_dtb_prefixes}; do if test -e ${devtype} ${devnum}:${distro_bootpart} ${prefix}${efi_fdtfile}; then run load_efi_dtb; fi;done;run boot_efi_bootmgr;if test -e ${devtype} ${devnum}:${distro_bootpart} efi/boot/bootaa64.efi; then echo Found EFI removable media binary efi/boot/bootaa64.efi; run boot_efi_binary; echo EFI LOAD FAILED: continuing...; fi; setenv efi_fdtfile-> Same across builds
So take my BPI-M5 for example, the place for the DTB could be:
ESP/amlogic/meson-sm1-bananapi-m5.dtbESP/dtb/amlogic/meson-sm1-bananapi-m5.dtbESP/dtb/current/amlogic/meson-sm1-bananapi-m5.dtb
I’ll pick the second one
For this, we needs to copy some DTBs to “ESP”, usually in U-Boot efi_dtb_prefixes=/ /dtb/ /dtb/current/, so let’s pick /dtb/ so they can be loaded by U-Boot as early as possible
cp -rva /mnt/root/boot/dtbs/linux-aarch64-7ji /mnt/root/efi/dtb
And booting on real hardware should now be OK. I’ve tested this on my BananaPi BPi-M5, OrangePi 5, Orange Pi 5 Plus and they all work seamlessly.
Note: as you may have tried and realized, even if you did not place the DTB, boards could still boot, but in those cases it is the U-Boot’s built-in DTB that’s used, and for newer kernels this could bring some problems.
Note also: by doing this we’ve locked the DTB to the one provided by linux-aarch64-7ji kernel, which is a stable-as-new-as-possible kernel, not only for Arch Linux ARM, but also for the latter Debian installation, using new DTBs on old kernels generally wouldn’t bring much trouble, unlike other way around.
Installing Debian aarch64
Do this in Arch Linux ARM aarch64, just similar to how we installed Debian x86_64 from Arch Linux x86_64. Just remember that still we would not want Grub the boot manager installed here, but rather Debian should only install grub2-common to auto-update its sub gurb.cfg
The final Grub menu shall look like this:
GNU GRUB version 2:2.14rc1-2
┌────────────────────────────────────────────────────────────────────────────┐
│*Arch Linux (x86_64) │
│ Debian (x86_64) │
│ Arch Linux ARM (aarch64) │
│ Debian (aarch64) │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Use the ▲ and ▼ keys to select which entry is highlighted.
Press enter to boot the selected OS, `e' to edit the commands
before booting or `c' for a command-line.
After all installation
Now it’s good time to do modification, to e.g. decorate the outer menu, reorder the menu, add more entries, etc.
Also note that I did not set timeout, for that booting default x86_64 on aarch64 is plainly wrong. This could be improved by either one of the following ways:
- Use seperate –boot-directory e.g.
/efi/x86_64and/efi/aarch64and maintain sepearate outer grub.cfg for each architecture - Use
$grub_platformvariable andcpuid(x86_64) /fdtdump(aarch64) command to determine the current platform, but this is not a guaranteed hit
For simplicity I would keep my current configu without default entry and timout logic.