Install the system
The disk is partitioned, formatted and mounted under /mnt, and there
is nothing on it. This step puts a complete Arch system there, makes
it able to boot itself, and makes sure you can log in when it does.
setup install
================ Review =================
Host: myhost
Detected: x86_64 PC, UEFI
Packages: base btrfs-progs cryptsetup sudo openssh iwd vim git pacutils expect snapper snap-pac linux linux-firmware intel-ucode
+ packages.txt: just tmux
Boot: systemd-boot
Units: systemd-networkd systemd-resolved iwd sshd snapper-cleanup.timer
Accounts: admin (1000, wheel, passwordless sudo), user
SSH keys: 3, from /root/arch/authorized_keys
Host repo: /root/arch -> /mnt/home/admin/arch
Maintenance: aconfmgr 96ab414 (admin diff/apply/save/upgrade), snapper + snap-pac
=========================================
? Install? (Y/n):
Everything on this screen was decided either by the hardware or by
the host repo. If any of it is not what you want, answer n, change
the host repo, and run it again.
Three lines read differently on a Pi 5, because they are the three things chosen by the hardware:
Detected: Raspberry Pi 5 (aarch64, bcm2712)
Packages: ... snapper snap-pac linux-rpi-16k firmware-raspberrypi linux-firmware
Boot: Pi firmware (cmdline.txt)
linux-rpi-16k and firmware-raspberrypi replace linux and the
intel-ucode / amd-ucode microcode, and there is no bootloader to
install. Everything else – the subvolumes, the accounts, the units,
the host repo, the maintenance tools – is the same on both.
After you confirm, it works through eleven stages. Apart from the passwords near the end, it asks nothing more.
1. Packages
setup refreshes the live system’s pacman keyring, then installs the
base set and everything in packages.txt into /mnt with pacstrap
– pacman pointed at a different root. There is no image and no magic:
when it finishes, /mnt is an ordinary directory tree that happens to
be a Linux system.
The base set is chosen from the hardware:
| Package | Why it is there |
|---|---|
base |
The minimum system: glibc, bash, coreutils, pacman, systemd |
linux / linux-rpi-16k |
A kernel, which base does not include; picked by uname -m, and on aarch64 only for a bcm2712 board |
linux-firmware |
Firmware blobs for WiFi, GPUs and other devices |
firmware-raspberrypi |
Pi only: VideoCore firmware and the files its bootloader reads |
intel-ucode / amd-ucode |
x86_64 only, by CPU vendor: microcode errata fixes, loaded at boot |
btrfs-progs |
The root filesystem is btrfs and cannot be checked without it |
cryptsetup |
Always: a couple of megabytes, and forgetting it on an encrypted disk is a far worse afternoon |
sudo |
Administration from admin |
openssh |
The whole point of a headless machine |
iwd |
WiFi |
vim |
An editor that exists inside the installed system |
less |
git’s pager. Only an optional dependency of git, so without it git log in the host repo fails outright rather than merely not paging |
git |
The host repo is a git repository |
pacutils, expect |
What aconfmgr needs to compare the machine with the repo |
snapper, snap-pac |
Snapshots of /, and the pacman hooks that take them |
The keyring refresh matters more than it looks. Every package is checked against developer signing keys, and the copy of those keys on the stick is as old as the image; a stick that has been in a drawer for six months will eventually meet a package signed by a key it has never heard of. The resulting error names a signature rather than a stale keyring, which sends people down the wrong path.
pacstrap uses the live system’s mirror list, which by default starts
with Arch’s own geo-routed CDN – a fine first choice anywhere. If you
would rather draw from a measured-fastest mirror near you, run
reflector --country US --protocol https --age 12 --sort rate --save /etc/pacman.d/mirrorlist before setup install (x86_64 only;
reflector is not on the Pi image and knows nothing about Arch Linux
ARM’s mirrors).
Early BOOMSTICK Pi images shipped without Arch Linux ARM’s signing
keys, and on one of those every ALARM package – including the
kernel – fails as unknown trust. setup checks before it starts,
and stops with the fix: rebuild the stick, or sign the key locally
with pacman-key --lsign-key 68B3537F39A313B3E574D06777193F152BDBE6A6.
On such a stick it also runs pacstrap without -K, because -K
gives the target a fresh keyring and would leave that hand-made trust
behind.
On a re-run, when the base system is already there, this stage only installs what is missing.
2. fstab
The new system needs to know how to mount its own filesystems at boot,
and right now that knowledge exists only in the live system’s mount
table, which is what genfstab -U reads. -U writes UUIDs rather
than device paths, because /dev/sda is assigned in whatever order
the kernel finds disks, while a UUID belongs to the filesystem and
never moves.
setup writes the file rather than appending to it, so running it
twice cannot duplicate an entry, and it stops if the swapfile is
missing from the result.
3. Time zone, locale, keymap and hostname
/etc/localtime becomes a symlink into /usr/share/zoneinfo, and the
hardware clock is set from the system clock. Your locale is uncommented
in /etc/locale.gen and generated, and written to /etc/locale.conf
as the default. The keymap goes into /etc/vconsole.conf and the
hostname into /etc/hostname. There is no need for a 127.0.1.1
line in /etc/hosts: systemd’s myhostname resolver answers for the
local hostname already.
A Pi 5 has a real-time clock, but it only keeps time across a power
cut if you have fitted the optional battery. Without one it boots
with a meaningless date and waits for NTP to correct it, which is
harmless as long as the network comes up. The same is true of the
live system, and a wrong clock makes pacman’s signature checks and
TLS fail in confusing ways – so if setup install fails on
signatures on a Pi, check timedatectl first.
4. initramfs
To mount the root filesystem the kernel needs drivers – for the disk
controller, for btrfs, maybe for unlocking LUKS – and those drivers
live on the root filesystem it cannot mount yet. The initramfs is a
second, tiny filesystem loaded into memory alongside the kernel with
just enough in it to find the real root, mount it and hand over.
Arch builds it with mkinitcpio.
The default HOOKS are already right for an unencrypted btrfs root,
including the microcode hook that folds CPU microcode into the
image (which is why the boot entry below has no separate
intel-ucode.img line, unlike older guides). On an encrypted disk
setup adds sd-encrypt after block. It has to be sd-encrypt,
not plain encrypt: the default initramfs is systemd-based, and the
older hook builds without complaint into an image that cannot unlock
the disk.
mkinitcpio -P then rebuilds every image. Warnings about missing
firmware for hardware you do not have are normal.
5. Bootloader
A machine can only start itself if three things line up: the firmware finds something to run, that something loads a kernel and the initramfs, and the kernel is told where its root filesystem is. The last part is where installs usually fail, and they fail the same way: the machine powers on, prints a little, and drops to an emergency shell.
On an x86_64 PC, setup runs bootctl install, which copies
systemd-boot onto the ESP and registers it with the firmware, then
writes its menu and one entry:
default arch.conf timeout 3 console-mode keep editor no
title Arch Linux linux /vmlinuz-linux initrd /initramfs-linux.img options root=UUID=e1445380-51a2-449d-ad82-8ac3286cbfb6 rootflags=subvol=@ rw
editor no matters more than it looks: with the editor enabled,
anyone at the keyboard can append init=/bin/sh to the kernel command
line and get a root shell without a password.
On a Raspberry Pi 5 there is no bootloader to install. The Pi’s
firmware reads config.txt and cmdline.txt from the FAT partition,
loads the kernel and initramfs itself, and jumps straight in.
config.txt already loads the initramfs, so setup leaves it alone
and rewrites only the kernel command line, which ships pointing at an
SD card:
root=UUID=e1445380-51a2-449d-ad82-8ac3286cbfb6 rootflags=subvol=@ rw rootwait console=serial0,115200 console=tty1 fsck.repair=yes
The firmware reads only the first line of that file, so if you edit
it by hand, make sure your editor has not wrapped it. rootwait tells
the kernel to wait for a USB or NVMe disk to appear rather than give
up. Both files are pacman backup files of the kernel package, so a
kernel update keeps your version and writes its own alongside as
cmdline.txt.pacnew – which is how new firmware options arrive, and
why .pacnew files are worth checking after kernel updates.
Either way, the part that is specific to this book’s layout is
rootflags=subvol=@. Without it the kernel mounts the top of the
btrfs filesystem, finds the subvolumes sitting there as directories,
and fails to boot. The UUID is read from the filesystem setup disk
made, never typed. On an encrypted disk, the root becomes
rd.luks.name=<LUKS UUID>=cryptroot root=/dev/mapper/cryptroot –
the UUID of the LUKS container, which is a different UUID from the
btrfs filesystem inside it, and the easiest thing to get wrong when
doing this by hand.
The x86_64 half of this stage boots in BOOMSTICK’s test VM, encrypted
or not: ENCRYPT=yes just test-vm installs onto LUKS, answers the
initramfs passphrase prompt on the serial console, and logs in
afterwards. The Raspberry Pi half – cmdline.txt, and a passphrase
prompt with no bootloader behind it – has not yet booted a machine.
6. The host repo, applied
The files under the host repo’s etc/ are installed at the same paths
under /mnt/etc, owned by root. git does not keep file modes, so
setup sets them by place: 0440 for sudoers drop-ins, which sudo
insists on before it will read them, and 0644 for everything else.
Files under the repo’s bin/ – admin – go to /usr/local/bin,
mode 0755. Then every unit in units.txt is enabled.
Enabling works inside the chroot because it only creates symlinks on
disk. Nothing starts until the machine reboots: the systemd you
can see from the chroot belongs to the live system, not the target.
The network this sets up by default is systemd-networkd for
addresses, systemd-resolved for DNS and iwd for WiFi – the same
combination the live system is running. The two .network files
match by name prefix (en*, wl*) rather than exact name, so they
work on any machine without checking what the kernel called the
hardware, and a USB ethernet adapter plugged in later just works.
The sshd drop-in is three lines, and all three matter.
PasswordAuthentication no alone is not enough: with PAM,
keyboard-interactive is a second route to the same password prompt,
so it is switched off explicitly, along with root login. Arch’s
sshd_config includes /etc/ssh/sshd_config.d/*.conf, so the drop-in
overrides the defaults without touching the shipped file, and package
updates never conflict with it.
7. The live system’s side
Two things can only be done from outside the chroot, so setup does
them from the live system against /mnt:
/etc/resolv.confbecomes a symlink to resolved’s stub. Inside the chroot that file is the live system’s own, bind-mounted over the target’s so that package downloads can resolve names, and replacing it there fails.- The live system’s iwd network files – the WiFi passphrases the
stick joined with – are copied into
/mnt/var/lib/iwd. This is the single most commonly forgotten step of a headless install: the machine boots, the disk is perfect, and it never joins the network because nothing told it the passphrase.
8. Accounts
admin and user are created, if they do not exist yet, with
admin in wheel. visudo -c then checks that the whole sudo
configuration parses. That check is not optional: a malformed sudoers
file does not degrade gracefully – sudo refuses to run at all –
and on a machine where sudo is the only route to root, that is a
lockout you can only fix from the console.
admin’s ~/.ssh/authorized_keys is installed from the host repo,
owned by admin, mode 0600, in a 0700 directory. Those modes are
not decoration: sshd silently refuses an authorized_keys file that
other users could write to, and from the client’s side you are just
asked for a password that will never be accepted.
The daily user is given the same keys, so the first thing you do on the
new machine can be logging in as yourself rather than as its
administrator. The two are managed differently from then on:
admin’s are installed from the repo on every run, because the repo is
what says who may administer this machine, while the daily user’s are
seeded once and left alone afterwards – a key added there by hand is
theirs, and a re-run will not revert it.
9. Passwords
The only prompts after the review: a password for admin and one for
the daily user, each typed twice. On a re-run, an account that already
has one is skipped.
root is not among them. It is locked instead – no password at all,
so there is none to guess, leak, or forget to rotate. Nothing needs it:
admin is in wheel, administration happens through sudo, and sshd
refuses root whatever its password is. setup check expects
root=L.
systemd’s emergency and rescue targets ask for the root password, and
sulogin will not open a shell for a locked account – it says so and
reboots. A machine that cannot mount its root filesystem therefore
cannot be rescued from its own keyboard.
This book makes that trade on purpose, because the stick is the
recovery tool and a better one: boot it, then setup host --from and
setup disk --mount as in Prepare the disk, and arch-chroot /mnt
gives you a root shell inside the broken system – over SSH, which a
keyboard attached to a machine in another room does not offer.
If you would rather have the console, sudo passwd root on the running
machine sets one. setup check accepts a root password that exists; it
objects only to an account with none.
10. aconfmgr
aconfmgr is what admin runs. It is in neither Arch’s nor Arch Linux
ARM’s repositories – only the AUR has it, and building AUR packages
would mean base-devel on every machine – but it is a bash program
with no compiled parts, so setup clones it from GitHub at a fixed
commit into /usr/local/lib/aconfmgr, and links it where its own
launcher looks (/usr/lib/aconfmgr, /usr/local/bin/aconfmgr).
Pinning the commit means every machine runs the version this book was
checked against; a newer one is a deliberate change to setup.
11. The host repo moves in
setup writes aconfmgr/10-base.sh, the base packages it picked for
this hardware, so aconfmgr knows them as wanted. A commit records the
install in the host repo, which is then copied to
/mnt/home/admin/arch and handed to admin. From the first boot,
the description of the machine lives on the machine.
setup install finishes by running the check.