Added a section on firmware Setup Mode before booting the Arch ISO, emphasizing its importance for a smoother installation process. Updated the install checklist to include confirmation of Setup Mode.
73 KiB
Arch Linux: systemd-boot + LUKS2 + TPM2 + Secure Boot
A guide for setting up Arch Linux with full disk encryption, TPM2 auto-unlock,
and Secure Boot — using systemd-boot and Unified Kernel Images instead of
GRUB. This avoids GRUB's Argon2id/PBKDF2 compatibility questions and the
still-unmerged GRUB LUKS2 systemd-tpm2 token bridge, since systemd-boot
never touches LUKS at all — disk decryption happens entirely in the
initramfs via sd-encrypt.
Scope: this guide covers the boot/encryption chain specifically — it is not a full Arch installation walkthrough. It assumes you'll handle most of the usual remaining install steps (locale, hostname) separately, per the standard Arch installation guide. Network manager installation and basic user setup are the two exceptions — see the notes in step 3, since skipping either leaves you with a system you effectively can't use or update after the first reboot.
One networking gotcha worth knowing before you start: unlike most of
the deferred post-install tasks, missing network config isn't just an
inconvenience — after the first reboot you'll have a base system with no
network manager installed at all, meaning you can't even run pacman -Syu
to catch up on anything else without first getting online. linux-firmware
(already in step 3's pacstrap) covers Wi-Fi firmware and the kernel's
iwlwifi driver will load, so the hardware itself will be detected — but
nothing will automatically connect. Step 3 below includes networkmanager
in pacstrap and enables it in the chroot for exactly this reason; if
you'd rather handle networking differently, that's the point in the guide
to change it.
Architecture overview
flowchart TD
subgraph LIVE["Live Environment (Arch ISO)"]
A0["Step 0: Connectivity check<br/>iwctl / wired DHCP"]
A1["Step 1: Partition ESP & root"]
A2["Step 2: LUKS2 format (Argon2id) & open"]
A2_FS{"Filesystem choice"}
A2_EXT["ext4<br/>(simple, direct mount)"]
A2_BTRFS["Btrfs<br/>(create @, @home, @var_log)"]
A0 --> A1 --> A2 --> A2_FS
A2_FS -->|ext4| A2_EXT
A2_FS -->|Btrfs| A2_BTRFS
end
A2_EXT --> B1
A2_BTRFS --> B1
subgraph BOOTSTRAP["Chroot System Bootstrap"]
B1["Step 3: pacstrap & genfstab"]
CHROOT_IN["arch-chroot /mnt"]
B2["Step 3: enable NetworkManager,<br/>set root passwd, add sudo user"]
B1 --> CHROOT_IN --> B2
end
subgraph BOOTCHAIN["systemd-boot + UKI (inside chroot)"]
C1["Step 4: bootctl install"]
C2["Step 5: mkinitcpio HOOKS<br/>sd-encrypt, sd-vconsole<br/>preset → default_uki target"]
C3["Step 6: /etc/kernel/cmdline<br/>rd.luks.name + root=<br/>(+ rootflags=subvol=@ if Btrfs)"]
C4["Step 7–8: mkinitcpio -P<br/>UKI auto-discovered as Type #2"]
B2 --> C1 --> C2 --> C3 --> C4
end
subgraph SEAL["Hardware Sealing & Recovery (inside chroot)"]
D1["Step 9: sbctl keys<br/>sign UKI + both boot binaries<br/>back up keys offline"]
D2["Step 10: TPM2 enrolment<br/>systemd-cryptenroll --tpm2-pcrs=7"]
D3["Step 11: recovery key +<br/>memorable passphrase<br/>(tested before reboot)"]
C4 --> D1 --> D2 --> D3
end
CHROOT_OUT["exit & umount -R /mnt"]
D3 --> CHROOT_OUT --> E["Step 12: reboot"]
E -.firmware/PCR change.-> R["Appendix: recovery<br/>wipe-slot + re-enrol,<br/>or restore sbctl keys"]
This mirrors the numbered steps below — the diagram is an overview, not a replacement for reading the actual commands in each section, particularly around the filesystem branch in step 2 and the cmdline branch in step 6.
Before you begin: firmware Setup Mode
This isn't step 0 by accident — it genuinely belongs before you boot
the Arch ISO at all, not tucked away next to the sbctl commands in
step 9 where it's used. Setup Mode is pure firmware/NVRAM state,
completely independent of what's booted from disk, so there's no
technical requirement to do it at any particular point in the install.
But doing it now, before the live ISO is even running, means one reboot
covers both "enter firmware setup and clear Secure Boot keys" and "boot
into the Arch ISO" in a single pass — rather than getting most of the
way through the install and then needing to interrupt it, reboot into
firmware, come back, and re-mount/re-chroot everything to pick up where
you left off. Nothing breaks if you leave this until step 9 instead —
it just costs you an avoidable detour.
Reboot into firmware setup now (systemctl reboot --firmware-setup if
you're already in some Linux environment, or the usual F2/Del/Esc/F10/F12
key at power-on otherwise — exact key is model-dependent) and find the
Secure Boot submenu. Setup Mode is normally reached by clearing or
deleting the existing Secure Boot keys — worded as "Clear Secure Boot
Keys," "Delete PK," or "Reset to Setup Mode" depending on vendor. Once
you've done that, boot straight into the Arch ISO from the same firmware
session if your firmware allows it, or exit and power back on with the
install media ready.
The relationship between this and the "Secure Boot" toggle itself
genuinely varies by vendor — full detail, including known ASUS/Gigabyte
quirks, is in step 9 where the enrolment actually happens. The one thing
worth confirming now, before going any further: once you're booted into
the live ISO, sbctl status should report Setup Mode: Enabled. If it
doesn't, go back into firmware before starting step 0 — every step from
here through step 8 works the same either way, but step 9 will fail
outright if this wasn't done.
0. Connect to the internet (live ISO)
Everything from here through pacstrap needs network access from the
live environment itself — this is separate from networkmanager, which
gets installed onto the new system for use after reboot, not for the
install process.
If you're using a custom desktop-enabled ISO (e.g. one with XFCE and
NetworkManager already built in) rather than the stock Arch ISO, connect
via the desktop's network applet instead of iwctl below, then skip
straight to the pacman sync section.
Wired: usually just works via DHCP automatically. Confirm with:
ping -c3 archlinux.org
Wi-Fi: the live ISO ships iwd already, accessible via iwctl:
iwctl
Inside the iwctl prompt:
device list
station wlan0 scan
station wlan0 get-networks
station wlan0 connect "Your-SSID"
(substitute your actual device name if it's not wlan0, and your
network's SSID). Enter your Wi-Fi password when prompted, then exit:
exit
Confirm connectivity:
ping -c3 archlinux.org
Sync the package database and mirrorlist
The live ISO's pacman database and mirrorlist are only as current as
whenever the ISO was built — for a custom ISO that's been sitting around
for a while, this is worth doing before pacstrap rather than
discovering a dead mirror partway through:
reflector --latest 10 --sort rate --save /etc/pacman.d/mirrorlist
pacman -Syy
reflector regenerates the mirrorlist with current, fast mirrors —
usually the actual fix when pacstrap hangs or fails partway through, not
just a sync against an already-dead mirror. -Syy (double-y) forces a
full re-download of the databases rather than trusting the ISO's possibly
stale local copy, which matters more the older the ISO is. If your ISO
is fresh (built in the last week or two), a plain pacman -Sy on its own
is probably enough, and reflector may not be strictly necessary — but
neither costs much to run regardless.
Optional: SSH in from another machine
Once you're online, the rest of the install can be done over SSH from a different computer rather than typing everything at the laptop's own keyboard — genuinely worth it for a long, multi-step install like this one, since copy-pasting commands from this guide is much easier on a full keyboard/screen elsewhere than on the install target itself.
Set a root password on the live environment (this is separate from, and unrelated to, the root password you'll set on the actual installed system in step 3):
passwd
Start SSH on the live ISO — it's already installed, just not running by default:
systemctl start sshd
Find this machine's IP address:
ip addr
Look for the address on your active interface (wlan0 for Wi-Fi, or the
Ethernet interface name if wired) — typically something like
192.168.x.x.
From another machine on the same network:
ssh root@<the-IP-you-found>
Accept the host key prompt on first connection, enter the password you just set, and you're in — everything from partitioning onward can now be run from this SSH session instead of the laptop's own console. This is a convenience for the live-ISO stage only; it has no bearing on the installed system's own SSH configuration, which isn't covered by this guide.
Don't proceed to partitioning until the network connectivity check above
succeeds — pacstrap, genfstab, and everything else through step 3
depends on it. The SSH step above is optional; skip straight to step 1 if
you'd rather work directly at the laptop's console.
1. Partitioning (during install)
- EFI System Partition — FAT32, ~1GiB, mounted at
/efi(systemd-boot convention; not/boot/efi) - Root — one LUKS2 partition, containing Btrfs or ext4
If you're dual-booting and already have an ESP from an existing Windows
or other Linux install, you don't need a fresh one — reuse it. Confirm
its partition type is EFI System
(C12A7328-F81F-11D2-BA4B-00A0C93EC93B in GPT, shown by tools like
fdisk/gdisk as type ef00), and mount the existing partition at
/efi rather than creating a new one. Arch's UKI and systemd-boot
files coexist fine alongside an existing OS's boot files on the same ESP
— nothing in this guide touches or removes what's already there.
Creating a new layout
If you're reusing an existing ESP as above, skip the ESP-creation steps below, but you still need to identify the disk and create the root partition — continue reading, just stop before formatting anything as FAT32.
Identify your target disk first — get this wrong and you'll partition the wrong drive:
lsblk
Look for the disk by size and existing layout, not just by name — device
names (/dev/sda, /dev/nvme0n1) can shift between boots, especially
with multiple drives attached. The rest of this section uses
/dev/sdX as a placeholder; substitute your actual device (e.g.
/dev/nvme0n1, in which case partitions are /dev/nvme0n1p1,
/dev/nvme0n1p2 rather than /dev/sda1, /dev/sda2 — note the p
before the partition number on NVMe devices, easy to miss when
copy-pasting).
If you'd rather not track that p-or-no-p distinction by hand through
every command below, set variables once and use those instead:
DISK="/dev/nvme0n1" # adjust to your actual device
if [[ "$DISK" == *nvme* ]]; then
ESP="${DISK}p1"; ROOT="${DISK}p2"
else
ESP="${DISK}1"; ROOT="${DISK}2"
fi
With that set, every /dev/sdX1//dev/sdX2 below becomes $ESP/$ROOT
— substitute as you prefer; the rest of this guide uses the literal
/dev/sdX1//dev/sdXY form for readability.
Wipe the disk before partitioning it, if you're doing a fresh, single-OS install. Skip this entirely if you're in the dual-boot reuse case above — this destroys everything on the disk, including whatever you're trying to keep. On a disk you're fully repartitioning, stale filesystem signatures, old LUKS headers, or a leftover GPT backup header (GPT keeps a copy at the very end of the disk, which a partial wipe can miss) can confuse partitioning tools or cause UUID collisions later — worth clearing properly now rather than debugging it after the fact:
wipefs -a /dev/sdX
sgdisk --zap-all /dev/sdX
wipefs -a clears recognised filesystem, RAID, and LUKS signatures
across the disk. sgdisk --zap-all goes further and destroys both the
primary and backup GPT structures specifically — the backup header is
easy for a partial wipe to miss since it lives at the last sector, not
the first. Together these leave a genuinely blank disk for the next
step. If you're reusing a drive that held sensitive data under a
different filesystem and want to go further than this, blkdiscard
(SSDs, near-instant) or shred/dd if=/dev/urandom (any disk, but slow
— hours, not minutes) are the tools for an actual secure erase; that's a
separate, optional concern from the ordinary wipe above and not
something this guide covers in detail.
If this is a fresh disk with no existing partition table, create a new GPT table first — this is destructive, so double-check the device name before running it. Skip this if you're adding a root partition to a disk that already has a GPT table (e.g. the dual-boot case, where the existing ESP's disk already has one):
parted /dev/sdX -- mklabel gpt
Create the ESP — 1GiB, with the esp flag (which also implies
boot, so no separate flag is needed). Sizing it at 1GiB rather than a
tighter 512MiB is deliberately generous: each UKI bundles the kernel,
initramfs, and microcode into one binary, and between a default +
fallback UKI and a second kernel if you ever add one (e.g. linux-lts),
a 512MiB ESP can genuinely run short of room at update time — 1GiB gives
enough headroom that you won't need to revisit this later. If you're
reusing an existing ESP, skip this step and the format step below it
— go straight to "Create the root partition":
parted /dev/sdX -- mkpart ESP fat32 1MiB 1025MiB
parted /dev/sdX -- set 1 esp on
Create the root partition in the remaining space — 100% takes
whatever's left on the disk:
parted /dev/sdX -- mkpart cryptroot 1025MiB 100%
By default parted assigns this the generic Linux filesystem type
(GUID 0FC63DAF-8483-4772-8E79-3D69D8477DE4, shown as 8300 in
fdisk/gdisk). There's also a dedicated Linux LUKS type
(CA7D7CCB-63ED-4C53-861C-1742536059CC, 8309) that some tools use for
GPT auto-discovery of encrypted volumes — but this guide doesn't rely on
that mechanism (root is found via the explicit rd.luks.name=/root=
kernel parameters in step 6, not partition-type auto-detection), so the
plain Linux filesystem type from the command above is fine as-is. If
you'd rather set it explicitly anyway for clarity when inspecting the
disk later:
parted /dev/sdX -- type 2 CA7D7CCB-63ED-4C53-861C-1742536059CC
Confirm the result before continuing:
lsblk /dev/sdX
parted /dev/sdX -- print
You should see two partitions: the ESP (/dev/sdX1) and what will
become your LUKS container (/dev/sdX2 — or, if you're reusing an
existing ESP, whatever number parted assigned the new root partition
on that disk). Format the ESP now — this is the one partition in
this layout formatted directly, since LUKS and the filesystem inside it
are handled together in the next step. Skip this if you're reusing an
existing ESP — it's already formatted, and reformatting it would wipe
whatever's already on it (including the other OS's boot files):
mkfs.fat -F32 /dev/sdX1
Don't format /dev/sdX2 — leave it as a raw, unformatted partition.
cryptsetup luksFormat in step 2 writes the LUKS2 header directly onto
it; formatting it with a filesystem first would just get overwritten.
2. Encrypt and open the root partition
cryptsetup luksFormat --type luks2 --pbkdf argon2id \
--pbkdf-memory 8388608 /dev/sdXY
cryptsetup open /dev/sdXY cryptroot
/dev/sdXY here is the root partition from step 1 (/dev/sdX2 in the
example above) — the raw, unformatted one, not the ESP. luksFormat
writes the LUKS2 header onto it directly; there's no separate
"create a partition for LUKS" step beyond the plain partition already
created above, since LUKS doesn't need its own partition type or
pre-formatting to work — it operates directly on the block device.
--pbkdf-memory 8388608 sets the Argon2id memory cost to 8GiB, well above
cryptsetup's 1GiB default and RFC 9106's 2GiB "default for all
environments" recommendation. This is a deliberate choice here, not a
generally-recommended value — it makes sense specifically because this
machine has 40GB of RAM to spare and this drive isn't intended to move to
lower-RAM hardware. On a machine with less memory headroom, or a drive
that might get unlocked elsewhere, RFC 9106's 2GiB figure
(--pbkdf-memory 2097152) is the better-grounded default. Test the unlock
time once after formatting:
time cryptsetup open /dev/sdXY cryptroot
Argon2id works natively here — no PBKDF2 workaround needed, since nothing
in this chain touches GRUB. --iter-time is also tunable if you want to
adjust the time cost independently, though the memory cost above is doing
most of the real hardening work. Note that defaults and available flags
do shift between cryptsetup versions.
Filesystem choice: ext4 or Btrfs
Both work fine with this boot chain — sd-encrypt and systemd-boot
don't care which one sits inside the LUKS volume. The tradeoff is between
simplicity and rollback capability, not compatibility.
ext4 — simpler, zero maintenance, predictable free-space reporting. Good default if you don't specifically want snapshots.
mkfs.ext4 /dev/mapper/cryptroot
mount /dev/mapper/cryptroot /mnt
mkdir -p /mnt/efi
mount /dev/sdXZ /mnt/efi # your ESP partition
Btrfs — worth it mainly for snapshots: on a rolling-release distro
like Arch, being able to snapshot before pacman -Syu (or before
re-signing UKIs via sbctl) and roll back a broken update in seconds is a
genuine safety net this guide's threat model doesn't otherwise cover.
Trade-off is a bit more maintenance (periodic scrub/balance) and slightly
less predictable df output due to copy-on-write allocation. This guide
uses Btrfs without compression — compress=zstd is a reasonable addition
later if you want it, but it's a separate decision from the subvolume
layout below, so it's left out here to keep the filesystem setup and the
compression choice independent.
mkfs.btrfs /dev/mapper/cryptroot
mount /dev/mapper/cryptroot /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@var_log
btrfs subvolume create /mnt/@swap
umount /mnt
mount -o noatime,subvol=@ /dev/mapper/cryptroot /mnt
mkdir -p /mnt/{efi,home,var/log,swap}
mount -o noatime,subvol=@home /dev/mapper/cryptroot /mnt/home
mount -o noatime,subvol=@var_log /dev/mapper/cryptroot /mnt/var/log
mount -o noatime,subvol=@swap /dev/mapper/cryptroot /mnt/swap
mount /dev/sdXZ /mnt/efi # your ESP partition
@swap is created here alongside the others regardless of whether you
actually end up using a swapfile — it costs nothing to have an empty
subvolume, and creating it now (as a proper sibling of @, while /mnt
is still mounted at the top level) avoids a more awkward remount dance
later if you try to create it after switching to subvol=@.
@var_log as its own subvolume means logs survive if you ever roll @
back to an earlier snapshot — otherwise a rollback would silently revert
your logs along with everything else, which defeats half the point of
having them. If you go this route, snapper (or a similar tool) for
managing the actual snapshot lifecycle is the natural next step, but it's
outside this guide's scope — the subvolume layout above just lays the
groundwork for it.
The rest of this guide assumes root is mounted at /mnt and the ESP at
/mnt/efi regardless of which filesystem you picked — the remaining steps
don't otherwise differ between ext4 and Btrfs.
Swap: a swapfile instead of a separate partition
A swapfile inside your already-encrypted root is simpler than a separate
swap partition — it's encrypted automatically as part of the LUKS volume,
with no second cryptsetup entry and no extra unlock prompt, and no
disk space locked into a fixed-size partition ahead of time. This also
means you don't need a third partition on the disk at all — just the ESP
and one LUKS2 partition for everything else.
ext4:
fallocate -l 8G /mnt/swapfile
chmod 600 /mnt/swapfile
mkswap /mnt/swapfile
swapon /mnt/swapfile
Btrfs — the @swap subvolume was already created and mounted at
/mnt/swap in the block above (regardless of whether you use it — no
harm if it stays empty). btrfs filesystem mkswapfile handles the
copy-on-write exclusion, permissions, and preallocation in one step —
manually chaining truncate/chattr +C/fallocate/chmod/mkswap is
the older way and no longer necessary since btrfs-progs 6.1:
btrfs filesystem mkswapfile --size 8G /mnt/swap/swapfile
swapon /mnt/swap/swapfile
Adjust the 8G size to whatever suits your RAM and whether you want
hibernation support (see below).
genfstab will pick up the swapfile automatically only if it already
exists before you run genfstab in step 3. If you create the swapfile
after that point instead, add the line to /etc/fstab by hand:
/swapfile none swap defaults 0 0
(adjust the path to /swap/swapfile if you used the Btrfs layout above).
Hibernation: if you want to hibernate, it has to go through the
swapfile, not zram — hibernating to zram swap isn't supported, even
with a backing device configured. A swapfile-based hibernate needs a
resume= kernel parameter pointing at the swapfile's physical offset,
which is genuinely more involved to set up correctly with a swapfile
than with a dedicated swap partition — outside this guide's scope. If
hibernation matters to you, it's worth deciding that before committing to
the swapfile approach, since a plain swap partition is the simpler path
for that specific use case.
A swapfile and zram aren't mutually exclusive — zram's compressed
in-RAM swap is used first by default (higher priority number wins,
not lower), with the disk-backed swapfile as overflow if RAM pressure
exceeds what zram can absorb. Setup for zram is in step 3, since it's
installed via pacstrap and configured inside the chroot.
3. Install the base system as normal, then chroot in
pacstrap /mnt base base-devel linux linux-headers linux-firmware mkinitcpio \
sbctl btrfs-progs efibootmgr dosfstools intel-ucode networkmanager sudo \
zram-generator \
mc nano vim htop wget iwd iotop-c less man-pages mandoc bc
sudo isn't part of base — it's included here because the wheel/
visudo step further down in this section needs it installed already.
Without it, visudo won't exist to run.
base-devel and linux-headers are worth adding even though this guide
doesn't itself compile anything: base-devel (compilers, make, and the
rest of the standard build toolchain) is required the moment you install
anything from the AUR, and linux-headers is required for any
out-of-tree kernel module (DKMS-based drivers, VirtualBox host modules,
etc.). Neither is needed for the boot chain itself, but going without
them tends to mean an extra pacman -S and possibly a reboot the first
time you actually need either — cheaper to include now.
The rest of that line is general-purpose tooling, not required by the boot/encryption chain, but worth having from first boot rather than needing network access (which won't exist yet if something's wrong) to fetch later:
nano,vim,mc— text editors and a file manager;mcin particular is handy for poking around a fresh systemhtop,iotop-c— process and disk I/O monitoringless,man-pages,mandoc— somanpages actually work;basealone doesn't include documentationwget— command-line downloadsiwd— an alternative Wi-Fi backend towpa_supplicant;NetworkManagercan use either, andiwdalso works standalone viaiwctlif you ever need to debug connectivity without NetworkManager in the picturebc— basic command-line arithmetic, occasionally useful in scripts
networkmanager is included here on the assumption you're on Wi-Fi (as on
a laptop like the XPS) — drop it if you're wired and happy configuring
systemd-networkd yourself instead. Enable it once you're in the chroot,
further down in this step.
efibootmgr and dosfstools aren't strictly required by this chain, but
you'll want both available for troubleshooting boot entries and working
with the FAT-formatted ESP from a running system.
btrfs-progs is worth having in the initramfs — not for fsck (Btrfs
doesn't use a boot-time fsck pass; fsck.btrfs is intentionally a no-op,
since Btrfs does its consistency checking in-kernel at mount time), but
for btrfs device scan if you ever use a multi-device Btrfs array, and
for btrfs check/repair tools being available in an emergency initramfs
shell. (Skip this package if you formatted root as ext4 instead, where
fsck genuinely does matter and the fsck hook below is doing real work.)
Generate /etc/fstab before chrooting, so the ESP and any non-root
mounts are correctly wired up:
genfstab -U /mnt >> /mnt/etc/fstab
Root itself is located via the rd.luks.name= and root= kernel
parameters set in step 6, not via fstab — but the ESP mount and anything
else still needs a correct fstab entry, and skipping this step is a
common cause of a system dropping to an emergency shell on first boot.
Now chroot in:
arch-chroot /mnt
If you included networkmanager above, enable it now while you're
already here:
systemctl enable NetworkManager
zram
zram-generator doesn't need a systemctl enable — it's a systemd
generator, meaning it creates the swap unit automatically at boot based
on a config file, rather than being a service you enable directly.
Create that config now:
cat > /etc/systemd/zram-generator.conf << 'EOF'
[zram0]
zram-size = min(ram / 2, 8192)
# 8192 here means 8192 MB (~8GiB) — bare numbers in this config are
# megabytes by definition, not bytes; this matches zram-generator's own
# documented default of min(ram / 2, 4096)
compression-algorithm = zstd
swap-priority = 100
fs-type = swap
EOF
zram-size = min(ram / 2, 8192) sizes the compressed swap device at half
your RAM, capped at 8GiB — reasonable defaults; adjust to taste. If you'd
rather not deal with the min() expression at all, a plain fixed size
works too — zram-size = 8192 gives a flat 8GiB regardless of how much
RAM the machine has, no expression needed.
swap-priority = 100 ensures zram is preferred over the disk-backed
swapfile from step 2, which should have a lower priority set in its
fstab entry (or none, which defaults lower) so zram gets used first
and the swapfile only kicks in as overflow. No further action needed here
— the generator picks this config up automatically on next boot.
Root password and a sudo-capable user
The base install has no usable login yet — set the root password and create a normal user while you're still in the chroot, rather than discovering there's no way in after the first reboot:
passwd
That sets root's password (running passwd with no username, while
still root inside the chroot, targets root itself).
useradd -m -G wheel -s /bin/bash george
passwd george
Adjust the username as you like. -m creates a home directory, -G wheel
adds the user to the wheel group, -s /bin/bash sets the shell.
Being in wheel isn't enough on its own — Arch ships sudo but doesn't
enable the wheel group for it by default. Edit the sudoers file with
visudo, never a direct text editor, since it validates syntax before
saving and a broken sudoers file can lock you out of sudo entirely:
EDITOR=nano visudo
Find and uncomment this line:
%wheel ALL=(ALL:ALL) ALL
Save and exit. Your user can now sudo after the next login.
4. Install and enable systemd-boot
bootctl install
This installs directly to the ESP — no grub-mkconfig, no grub.cfg
scripting.
Ongoing maintenance, worth knowing now even though it's a later
concern: bootctl install is a one-time setup command — it doesn't
keep the ESP's copy of systemd-boot in sync with future systemd package
upgrades. That's bootctl update's job, and left to its defaults it
won't run automatically — not because systemd lacks the mechanism, but
because of Arch's package-management philosophy specifically. systemd
ships a stock unit for exactly this, systemd-boot-update.service
(ExecStart=bootctl --variables=no --graceful update, WantedBy=sysinit.target,
meaning it'd run at every boot and no-op harmlessly when there's nothing
to update) — but Arch, unlike some distros, never auto-enables services
on package install, so it sits there disabled until you turn it on
yourself:
systemctl enable systemd-boot-update.service
Recent systemd (261.2-1 and later) also does a version of this
automatically: when the package upgrades and an installed systemd-boot
is detected, it marks systemd-boot-update.service for a triggered run,
and a separate pacman hook (35-systemd-enqueue-marked.hook) carries
that out via systemctl enqueue-marked. Worth knowing this exists, but
it doesn't replace the enable step above — enabling the service
directly is the deterministic behaviour this guide relies on throughout,
and it's what makes the service run at every boot regardless of which
package-upgrade path triggered the update.
This matters because of the boot-manager signing approach in step 9:
sbctl signs the source binary in /usr/lib/systemd/boot/efi/, and
it's systemd-boot-update.service (or the auto-enqueue hook above) that
actually pushes that signed copy onto the ESP. Without this service
enabled, the ESP copy simply never gets refreshed at all — old, but
still correctly signed, rather than new and unsigned. Either way, the
service needs to be running for updates to actually reach the ESP.
5. Build a Unified Kernel Image (UKI)
Arch's mkinitcpio can output UKIs natively, but the two config files
involved do different jobs: /etc/mkinitcpio.conf sets which hooks
run, while /etc/mkinitcpio.d/linux.preset sets the output target —
including whether a UKI gets built at all.
In /etc/mkinitcpio.conf, use the sd-encrypt hook (not the legacy
encrypt hook). sd-vconsole and keyboard do different jobs and are
both worth keeping: sd-vconsole sets the console keymap/font,
keyboard ensures keyboard driver modules are actually loaded into the
initramfs — the latter matters most for USB keyboards, which need their
controller driver available before you can type a LUKS passphrase at an
early boot prompt. No real cost to including both:
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)
If your root is Btrfs, the fsck hook here is harmless but does nothing —
Btrfs has no boot-time fsck pass, so this hook is only doing real work on
ext4. Feel free to drop it from HOOKS on a pure-Btrfs setup; leaving it
in costs nothing either way.
sd-vconsole reads /etc/vconsole.conf for your keymap and console font.
If you're on a non-US keyboard layout, set this now — otherwise your LUKS
passphrase prompt (and everything else pre-userspace) will default to a US
layout:
echo "KEYMAP=uk" > /etc/vconsole.conf # adjust to your layout
Then edit your kernel's preset file to point output at a UKI on the ESP
instead of a standard .img. The filename matches your kernel package —
linux.preset for the standard kernel, linux-lts.preset for LTS, and so
on. Check what you actually have:
ls /etc/mkinitcpio.d/
Then edit the relevant preset (/etc/mkinitcpio.d/linux.preset in the
standard case) with a text editor, e.g.:
nano /etc/mkinitcpio.d/linux.preset
You're looking for two existing lines and replacing each with a _uki
equivalent — nothing here needs to be invented, just found and edited.
Before (what's already in the file):
default_image="/boot/initramfs-linux.img"
fallback_image="/boot/initramfs-linux-fallback.img"
After (comment out the old lines, add the new ones directly below):
# default_image="/boot/initramfs-linux.img"
default_uki="/efi/EFI/Linux/arch-linux.efi"
# fallback_image="/boot/initramfs-linux-fallback.img"
fallback_uki="/efi/EFI/Linux/arch-linux-fallback.efi"
If your preset has no fallback_image line at all, skip the fallback
half — not every install uses one.
Confirm the old _image lines are actually commented out, not just
edited nearby them. If a default_image/fallback_image line is
left active alongside the new _uki line, mkinitcpio builds both
targets rather than one replacing the other — not something that breaks
boot, but it silently doubles your build time and wastes ESP space on
images you're not using. Check after editing:
grep -E '^(default|fallback)_image=' /etc/mkinitcpio.d/linux.preset
No output means both are correctly commented out. If either line appears, go back and comment it out.
Check for a fallback_options="-S autodetect" line elsewhere in the
same file, and don't remove it if present — Arch's auto-generated
presets ship with this by default, and it's easy to overlook since it
sits apart from the two lines you're editing above. It matters more
than it looks: -S autodetect tells mkinitcpio to skip the
autodetect hook specifically for the fallback build, so the fallback
UKI carries a broad, untrimmed set of drivers rather than the
this-machine-only set autodetect produces for the default UKI. Without
it, your "fallback" is really just a second copy of the same image —
no more use than the default if the reason you needed a fallback was a
driver autodetect guessed wrong about. If your preset genuinely
doesn't have this line (unusual, but possible on a heavily hand-edited
config), add it back in on its own line near the other fallback_*
entries.
mkinitcpio will not create the destination directory for you — if
/efi/EFI/Linux/ doesn't exist yet, UKI generation fails silently. Create
it before your first build:
mkdir -p /efi/EFI/Linux
CPU microcode
With a traditional (non-UKI) setup, microcode used to be loaded via a
separate initrd image the bootloader concatenated in ahead of the main
one. That method is deprecated — current mkinitcpio (v38+) handles this
through the microcode hook already present in the HOOKS line above,
which bundles the microcode directly into the generated image (the
unified .efi binary, in this UKI setup) at build time. The package
being installed (amd-ucode or intel-ucode, per step 3) is what
actually supplies the microcode data; the hook is what pulls it in.
Placement of microcode relative to autodetect changes what gets
included, not just when: autodetect trims the initramfs down to only
what this specific machine needs — modules, and, if it precedes
microcode, only the current CPU's vendor microcode rather than every
vendor's. This guide places microcode after autodetect (as shown
above) deliberately, since you're building this UKI for one specific,
fixed machine, not a portable or rescue image that might run on
different hardware — there's no benefit to carrying both AMD and Intel
microcode blobs when only one will ever be used here.
If you ever do want the broader set (multi-hardware use, or you're
adapting this guide's HOOKS for a rescue/live image rather than a
fixed install), move microcode ahead of autodetect instead, or drop
autodetect entirely. You can confirm which vendor's microcode actually
made it into the built image with:
lsinitcpio --early /efi/EFI/Linux/arch-linux.efi | grep microcode
6. Kernel command line for sd-encrypt
Create /etc/kernel/cmdline. This differs depending on which filesystem
you picked in step 2 — if you're on Btrfs with the subvolume layout
above, the kernel needs to be told which subvolume is root, or it'll mount
the top-level volume (which contains nothing but the @/@home/
@var_log directories) instead of your actual system, landing you in an
emergency shell.
ext4:
rd.luks.name=<LUKS-UUID>=cryptroot root=/dev/mapper/cryptroot rw
Btrfs (with the @ subvolume layout above):
rd.luks.name=<LUKS-UUID>=cryptroot root=/dev/mapper/cryptroot rw rootflags=subvol=@
Use whichever line matches your step 2 choice — don't use the Btrfs line on an ext4 install. An unrecognised mount option isn't reliably ignored by every filesystem driver; treat the two as genuinely different commands rather than one that happens to work for both.
Replace <LUKS-UUID> with the output of:
cryptsetup luksUUID /dev/sdXY
blkid -s UUID -o value /dev/sdXY returns the same value and works too —
blkid recognises the LUKS2 header and reports its UUID field directly.
Either command is fine; cryptsetup luksUUID is just more explicit about
what it's returning.
This has to be the LUKS container's own UUID, not the partition's GPT
PARTUUID — two different values that are easy to mix up if you're
copying from a lsblk -f/gdisk -l listing with several UUID-looking
columns on screen at once. Both commands above are already scoped to
return the LUKS UUID specifically, so as long as you copy their output
verbatim rather than picking a UUID by eye from a broader listing,
you're using the right one.
mkinitcpio reads /etc/kernel/cmdline when building a UKI and embeds it
into the .efi binary — that embedding is what makes the command line
tamper-evident under Secure Boot, since altering it breaks the signature.
If your preset overrides this with its own CMDLINE= or similar, that
takes precedence, so check the preset if the parameters don't appear to
take effect. If you run multiple kernels (e.g. linux and linux-lts),
each has its own preset and each needs its UKI target configured.
Neither cmdline example above includes a resume= parameter — that's
deliberate, not an oversight. Hibernation needs one, pointing at the
swapfile's physical offset, but setting that up correctly is outside this
guide's scope (see the hibernation note in step 2). If you want
hibernation, work that out before your first reboot, since it changes
this file.
7. Rebuild the UKI
mkinitcpio -P
Confirm it actually built something before moving on — mkinitcpio
failing partway through (a missing hook, a typo in the preset) doesn't
always make itself obvious:
ls -l /efi/EFI/Linux/
You should see arch-linux.efi (and arch-linux-fallback.efi, if you
kept the fallback preset section), both with a non-trivial size — a UKI
bundling a kernel, microcode, and initramfs is typically tens of
megabytes, not a handful of bytes. A missing file or an oddly small one
means the build didn't complete; check the mkinitcpio -P output above
for errors before continuing.
8. Boot entry auto-discovery
Unlike GRUB2-BLS or manually-placed .conf entries, a UKI placed directly
in /efi/EFI/Linux/ is auto-discovered by systemd-boot as a Type #2
entry — no manual boot entry file is needed.
Just confirm /efi/loader/loader.conf exists:
cat /efi/loader/loader.conf
A minimal version is fine:
default @saved
timeout 3
console-mode max
systemd-boot will populate the boot menu dynamically from
/efi/EFI/Linux/*.efi.
9. Secure Boot — enrol your own keys
Arch has no vendor shim in Microsoft's trust chain (unlike Fedora), so this step is mandatory, not optional, if you want Secure Boot at all.
sbctl is the tool used throughout this guide, but worth knowing it's not
the only path: bootctl itself gained native key enrolment
(--secure-boot-auto-enroll=yes, with --certificate=/--private-key=)
in systemd 257, which populates the ESP's PK/KEK/db databases directly at
install time without a separate tool. This guide sticks with sbctl
throughout since it's more thoroughly exercised elsewhere in these steps
(status checks, signing, key backup) — not a suggestion to switch
mid-guide, just worth knowing the alternative exists if you're building
your own setup from scratch later.
By this point you should already be in Setup Mode — covered in
"Before you begin" at the top of this guide, which is where this
belongs if you haven't done it yet, since it means one less reboot
mid-install. If you skipped ahead: sbctl status should report Setup Mode: Enabled before you run anything below; if it doesn't, go back
and do that first — everything here will fail against a firmware still
holding an enrolled PK.
The relationship between Setup Mode and the "Secure Boot" toggle
itself genuinely varies by firmware vendor — there isn't one universal
rule here, so don't assume your board behaves like someone else's
writeup. On some hardware, Secure Boot needs to already be toggled on
before you can even reach the key-clearing option. On others, clearing
the keys to enter Setup Mode also flips Secure Boot itself off, and it
only comes back once you've enrolled new keys — sometimes automatically,
sometimes only after you go back into firmware and toggle it on by
hand. ASUS and Gigabyte boards in particular are known to require that
manual re-enable step after enrolment, not before. If your firmware has
an "OS Type" setting, look for something like "Windows UEFI Mode" or
"Custom" rather than "Other OS" — on at least some ASUS boards, "Other
OS" silently disables Secure Boot regardless of anything else you've
set. When your specific firmware's menus don't match any of this
exactly, sbctl status after each change is the reliable way to check
what state you're actually in, rather than trusting the menu wording.
sbctl status # confirm you're in Setup Mode
sbctl create-keys
sbctl enroll-keys -m # -m includes Microsoft's certs — useful for dual-boot
# and firmware update compatibility
sbctl sign -s /efi/EFI/Linux/arch-linux.efi
sbctl sign -s -o /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed \
/usr/lib/systemd/boot/efi/systemd-bootx64.efi
bootctl update
sbctl verify
Don't consider this step done until sbctl status shows both Setup Mode: Disabled and Secure Boot: Enabled together. Given the
vendor variance above, it's entirely possible for enrolment to succeed
(Setup Mode: Disabled) while Secure Boot itself still reads as
Disabled — meaning nothing is actually being enforced yet, silently.
If you see that combination, go back into firmware and enable Secure
Boot explicitly before moving on; don't assume enrolling keys was
sufficient on its own.
The -m flag isn't just for dual-boot — some hardware needs it regardless.
Certain option ROMs (some Nvidia GPUs, some OEM storage controllers) are
themselves signed under Microsoft's Third-Party UEFI CA, not by you. On a
purely single-boot Arch system with such hardware, omitting -m can mean
a black screen or storage controller failure at boot, since the firmware
would refuse to run that unsigned-by-your-keys component before Linux even
starts. Keep -m unless you have a specific reason not to.
Worth being clear-eyed about the tradeoff, though: -m doesn't just add
compatibility, it also expands what your firmware will run. Enrolling
Microsoft's certificates means the firmware now trusts any binary
signed under Microsoft's Third-Party UEFI CA, not only the specific
things you actually needed it for — a larger trusted set than just your
own key, in exchange for hardware that would otherwise refuse to boot.
For most people this is a straightforward, worthwhile trade given the
alternative is broken hardware; worth knowing it's a trade at all, not
a free compatibility switch.
There is an alternative to -m for the same Option ROM problem —
enroll-keys --tpm-eventlog enrols checksums read from the TPM's own
eventlog instead of trusting Microsoft's CA wholesale. sbctl's own man
page marks this feature explicitly experimental, so this guide sticks
with -m throughout; worth knowing the alternative exists if you'd
rather avoid enrolling a third-party CA at all and are comfortable with
an experimental feature to do it.
Why the boot manager is signed at its source path, not on the ESP —
this is not a stylistic choice. An earlier version of this guide (and
plenty of guides still in circulation) signed
/efi/EFI/systemd/systemd-bootx64.efi and /efi/EFI/BOOT/BOOTX64.EFI
directly on the ESP, relying on sbctl's -s-flag pacman hook to
re-sign them after future updates. That approach has a real, confirmed
gap: the ArchWiki's own Secure Boot page states it plainly — "if you
use systemd-boot and systemd-boot-update.service, the boot loader is
only updated after a reboot, and the sbctl pacman hook will therefore
not sign the new file." The pacman hook fires immediately during the
systemd package's own upgrade, re-signing whatever is currently on
the ESP — but systemd-boot-update.service only copies the new boot
manager binary onto the ESP later, at the next boot, after the hook has
already run. The result: a routine systemd update can silently leave
you with an unsigned boot manager on the ESP, with nothing further
triggering a re-sign, discovered only when the firmware refuses to boot
it.
The fix, per the ArchWiki and confirmed independently by other current
guides using this exact stack: sign the source binary in
/usr/lib/systemd/boot/efi/, using -o to produce a .signed sibling
file rather than overwriting anything on the ESP. bootctl has native
support for this convention — both bootctl install and bootctl update look for a <name>.efi.signed file next to the plain .efi
file and copy that instead, to both ESP destinations
(/efi/EFI/systemd/systemd-bootx64.efi and /efi/EFI/BOOT/BOOTX64.EFI)
in the same pass — one signed source file covers both, no separate
signing step needed for the fallback copy. Since the systemd package
upgrade that replaces the source binary happens synchronously within
the same pacman transaction the -s hook fires in, the hook can catch
and re-sign the fresh source file at the right moment — unlike the
ESP copy, which is genuinely only touched later.
The bootctl update command above is what actually pushes this freshly
signed .signed sibling onto the ESP for the first time, replacing the
unsigned binary bootctl install put there back in step 4. sbctl verify afterwards confirms both ESP copies now report as signed — don't
skip this check; it's the only way to know the .signed convention
actually took effect on your system rather than assuming it did.
The -s flag registers the UKI and the source boot-manager path so
sbctl auto-resigns them via a pacman hook on future kernel and
systemd updates — you shouldn't need to re-run sbctl sign manually
after routine updates. That said, this is exactly the kind of
"shouldn't need to" that's worth spot-checking rather than assuming
forever: run sbctl verify after any systemd package upgrade
specifically, at least the first few times, until you've seen it hold
up across a real update on your own machine.
If sbctl can't find your ESP — an error like "failed to find EFI
system partition" from sbctl verify or similar means its automatic
detection (which queries lsblk under the hood) didn't work for your
disk layout. Override it explicitly rather than troubleshooting the
detection itself:
export ESP_PATH=/efi
Set this in the same shell before re-running the sbctl commands above.
This guide's ESP is consistently at /efi throughout, so that's the
value to use here regardless of what sbctl failed to detect.
Reinstalling on a machine that's already had keys enrolled
The commands above assume a genuinely fresh machine — firmware still in Setup Mode, no Platform Key set. If you're reinstalling Arch on hardware that's already been through this guide once before, that assumption doesn't hold, and it's worth understanding why before you hit it.
The Platform Key lives in the motherboard's firmware NVRAM, not on
your disk. Wiping and repartitioning the drive has no effect on it —
it survives a full reinstall untouched. So on a second install, sbctl status will report you're no longer in Setup Mode, and sbctl enroll-keys -m will fail with:
Your system is not in Setup Mode! Please reboot your machine and reset
secure boot keys before attempting to enroll the keys.
This isn't a bug — enrolling a new PK when one is already set
normally requires either Setup Mode, or a signature from the
currently enrolled PK's own private key. That key lived on the disk
you just wiped, so sbctl has no way to authorise the change from
inside the new install.
Three ways forward, depending on whether you kept the old backup:
1. No backup of the old keys (or you don't want to reuse them) —
clear the firmware's Secure Boot state and start over. Enter firmware
setup (not the OS) and look for an option along the lines of "Clear
Secure Boot Keys," "Delete PK," or "Reset to Setup Mode" — wording
varies by vendor, but the option is standard on essentially all UEFI
implementations. This returns the firmware to Setup Mode, at which
point sbctl status confirms it and the enrolment block above runs
exactly as it would on genuinely new hardware — generating and
enrolling a completely new PK/KEK/db. That's fine here, since the OS
underneath is fresh too. This is the only option available if you
don't have the old key backup — nothing below works without it.
2. You backed up the old keys and want to keep using them —
restore the backed-up key directory to the path the new install's
sbctl expects, then sign the new UKI and boot binaries with that
same, already-enrolled key material instead of generating new keys:
sbctl setup --print-config | grep keydir # confirm the expected key path
cp -a /path/to/your/backup/sbctl /var/lib/sbctl # adjust destination to
# match what was reported
sbctl sign -s /efi/EFI/Linux/arch-linux.efi
sbctl sign -s -o /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed \
/usr/lib/systemd/boot/efi/systemd-bootx64.efi
bootctl update
sbctl verify # confirm everything signed correctly
No create-keys or enroll-keys here at all — the firmware already
trusts this key material from the previous install, so there's nothing
to re-enrol, only signing to redo. This is quicker than path 1 and means
one less firmware round-trip, and it's the same principle as the
"firmware refuses to boot your UKI at all" case in the recovery
appendix, just applied proactively at install time rather than after a
failure.
3. You backed up the old keys but want fresh ones anyway — restore the backup as in path 2, but stop before signing anything, then clear the PK and generate new keys instead:
sbctl setup --print-config | grep keydir # confirm the expected key path
cp -a /path/to/your/backup/sbctl /var/lib/sbctl # adjust destination to
# match what was reported
sudo sbctl reset # clears the enrolled PK
sbctl status # confirm Setup Mode is back on
sbctl create-keys
sbctl enroll-keys -m
sbctl sign -s /efi/EFI/Linux/arch-linux.efi
sbctl sign -s -o /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed \
/usr/lib/systemd/boot/efi/systemd-bootx64.efi
bootctl update
sbctl verify
reset clears the enrolled PK, but — like enroll-keys — this is
itself an authenticated firmware operation: it needs to sign the
clear-request with the current PK's private key, which is exactly
what the restored backup provides in the cp -a step above. With the
PK cleared, sbctl status reports Setup Mode again, and
create-keys/enroll-keys proceed as normal, generating an entirely
new hierarchy — all without ever rebooting into firmware setup. Skip
this path if you don't have the old backup; reset has nothing to sign
with otherwise, and you're back to path 1.
Which path makes sense depends on whether you value the convenience of reusing trusted keys across reinstalls (path 2), want a clean break each time but without the firmware-menu detour (path 3, backup permitting), or simply don't have the backup to work with (path 1, the only option left in that case).
Back up your Secure Boot keys
sbctl stores its generated keys under /var/lib/sbctl/ in current
versions (older versions used /usr/share/secureboot/ — sbctl even
ships a sbctl setup --migrate command specifically to move an install
from the old path to the new one, which tells you which direction is
current). Don't take either path on faith and don't check sbctl status for it — despite sounding like the obvious command, status's
output doesn't include a key path at all, only Installed, Owner GUID, Setup Mode, Secure Boot, and Vendor Keys. The command that
actually reports it is:
sbctl setup --print-config | grep keydir
Then copy the directory it reports to external media — don't hardcode the path, since it varies by version:
# Example, if the above reports keydir: /var/lib/sbctl/keys:
cp -a /var/lib/sbctl /root/sbctl-keys-backup
# then move /root/sbctl-keys-backup to offline storage
Verify the backup is readable from another machine before you rely on it — an unverified backup of key material is worth roughly nothing.
Restrict access to the copy before it leaves this machine — these are private keys, not just configuration:
chmod 700 /root/sbctl-keys-backup
find /root/sbctl-keys-backup -type f -name '*.key' -exec chmod 600 {} +
find here rather than a ** glob deliberately — recursive globbing
like keys/**/*.key only expands the way you'd expect if globstar is
enabled (shopt -s globstar), which isn't bash's default and isn't
something you can assume is set in every shell you might run this from.
find doesn't depend on any shell option at all.
And once it's off this machine, keep it there: don't leave the backup on a USB drive that stays permanently plugged into this laptop, or on any other storage this same machine can read unattended. The entire point of the offline copy is that compromising this machine shouldn't also compromise the keys that vouch for its boot chain — a "backup" that's always reachable from the live system doesn't give you that.
This matters because of the firmware-reset scenario described in step 10: if a major UEFI update wipes your enrolled keys, having the originals means re-enrolling them rather than regenerating everything and re-signing from scratch. Treat them like any other private key material — offline, not on the encrypted disk they protect.
Verify the auto-signing hook actually works
This test matters more than it might look — unlike the boot manager
case above, the UKI doesn't have a structural gap: sbctl's pacman
hook is literally named ZZ-sbctl.hook, which forces it to run last
among pacman's PostTransaction hooks. mkinitcpio's own kernel-update
hook — the one that actually rebuilds the UKI — runs earlier in the
same transaction, so by the time sbctl fires, the freshly-built UKI is
already sitting at its final ESP path, and gets signed correctly, all
within one pacman -Syu run. That's fundamentally different from the
boot-manager problem, where the refresh happens outside any pacman
transaction entirely, at the next boot — no hook ordering can fix a gap
that isn't a hook-ordering problem in the first place.
This does mean the ordering trick has a theoretical failure mode of its
own: any custom hook you (or a package you install later) add under
/etc/pacman.d/hooks/ that sorts after ZZ- alphabetically — a name
starting ZZZ- or similar — and that touches the UKI would run after
sbctl has already signed it, invalidating the signature. Not something
you're likely to hit by accident, but worth knowing if you ever add your
own hooks: check ls /etc/pacman.d/hooks/ /usr/share/libalpm/hooks/
sorted, and keep anything that modifies boot files ordered before
ZZ-sbctl.hook, not after.
That said, "structurally sound" isn't the same as "guaranteed" — there are scattered older reports of the UKI ending up unsigned after an update for less-well-understood reasons, so don't take it purely on faith. Test it once now, while you can still fix it easily:
mkinitcpio -P # rebuild the UKI
sbctl verify # confirm the freshly rebuilt UKI is signed
If sbctl verify reports the new UKI as unsigned, the hook didn't run.
Confirm the hook file itself is present:
ls /usr/share/libalpm/hooks | grep -i sbctl
If nothing shows up, sbctl isn't registering pacman hooks on this
install — reinstalling the package is the next step. If the hook file
is present but sbctl verify still fails, the likely cause is having
used plain sbctl sign instead of sbctl sign -s in step 9 — only -s
registers a file for auto-resigning; without it, the hook has nothing to
act on and your next kernel update will silently leave you with an
unbootable Secure Boot system.
10. TPM2 enrolment
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/sdXY
This gives fully unattended boot — convenient, but see the PIN section
below before you settle on this as your final setup: PCR-only binding
doesn't protect against a stolen laptop that's simply switched on, only
against a stolen disk. Most people reading this guide on a laptop should
use the --tpm2-with-pin=yes variant a few paragraphs down instead.
Note the device argument is the LUKS container partition (/dev/sdXY,
the same one you ran luksFormat against), not the opened mapper device
(/dev/mapper/cryptroot). Enrolment writes a token into the LUKS2 header,
which lives on the container.
This is the same command regardless of distro or bootloader — sd-encrypt
in the initramfs reads the systemd-tpm2 LUKS2 token natively. No GRUB-side
bridging, no manual key sealing.
Confirm the enrolment actually landed
Don't take the command's silent success on faith — check the LUKS2 header directly:
cryptsetup luksDump /dev/sdXY
The output has two sections that matter here, and they're not the same
thing: Keyslots lists the actual key material slots (numbered,
e.g. 0, 1) — this is what a passphrase or the TPM's sealed key
occupies. Tokens lists metadata objects, and this is where you're
looking for an entry of type: systemd-tpm2, which references back to
the keyslot it unlocks. A healthy TPM2 enrolment has both: a keyslot
holding the sealed key, and a token pointing at it. If you see a
systemd-tpm2 token with no corresponding keyslot (or vice versa), the
enrolment is broken or partial — don't rely on it; wipe and re-enrol
rather than assuming it'll sort itself out at boot.
This same check is worth repeating any time you wipe and re-enrol later
(see the recovery appendix) — --wipe-slot=tpm2 and a fresh enrolment
are two separate operations, and if one of them gets interrupted
(power loss, Ctrl-C, a crashed session) you can end up with an orphaned
token or keyslot rather than a clean pair. luksDump before and after
is the way to actually confirm that didn't happen, rather than assuming
it from the command's exit status alone.
PCR selection — what you're actually choosing
| PCRs | Protects against | Tradeoff |
|---|---|---|
7 only |
Secure Boot chain tampering | Survives most firmware/UEFI updates without re-enrolment |
0+7 |
+ low-level firmware settings | More brittle — breaks on more firmware updates |
0+2+7 |
+ option ROM tampering | Most brittle — highest chance of unexpected lockout |
Start with --tpm2-pcrs=7. Only add more PCRs if you specifically need to
detect the additional tampering vectors, and only after you understand
you'll be re-enrolling more often.
Caution: a routine firmware update changing PCR 7's value is recoverable — re-enrol and move on. A major UEFI/firmware update that resets the Secure Boot key database to factory defaults is a different, more serious failure: it wipes your custom-enrolled
sbctlkeys entirely, and no LUKS recovery key will fix that, since the problem is the boot chain no longer trusting your signed binaries at all. This is a real, if uncommon, risk inherent to self-signed Secure Boot setups — not something this guide (or any guide) can fully protect against. Check release notes before applying major firmware updates, and keep a record of yoursbctlkey material backed up separately.
Add a PIN — closing the "stolen while off" gap
PCR-only TPM2 binding has a real blind spot worth understanding before you rely on it: it protects against a stolen disk (pulled drive, booted elsewhere), but not against a stolen laptop that's simply switched on. PCR 7 doesn't change just because the machine changed hands — the TPM sees the same firmware state it always has, releases the key automatically, and whoever turned it on lands straight at the login screen, past the one layer (LUKS) that was supposed to stop them.
If theft of the whole device is part of your threat model — which, for a laptop, it should be — require a PIN in addition to the PCR match:
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --tpm2-with-pin=yes /dev/sdXY
You'll be prompted to set the PIN interactively. Despite the name, this
isn't digits-only — systemd-cryptenroll's own documentation notes any
character can be used, so this is really a short passphrase rather than
a numeric PIN in the traditional sense. A short, unique value is the
right choice here (see the lockout note below for why), and "unique"
matters specifically: systemd-cryptenroll doesn't verify the TPM
measurement before asking for the PIN, so a compromised or spoofed
pre-boot environment could be phishing for it before any check has
happened — don't reuse this value anywhere else.
With this enrolled, boot now needs the PIN before the TPM will release the key at all — a stolen, powered-off laptop can't be unlocked just by turning it on. You lose fully-unattended boot, but for a laptop that leaves the house, that's a small cost for closing a real gap.
If you're adding this after already enrolling TPM2 without a PIN, wipe the existing TPM slot first, then re-enrol with the flag — you can't add a PIN to an already-enrolled slot in place:
systemd-cryptenroll --wipe-slot=tpm2 /dev/sdXY
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 --tpm2-with-pin=yes /dev/sdXY
On lockout: the TPM enforces its own dictionary-attack protection against repeated wrong PINs, separate from anything LUKS does. Enough wrong attempts trips a cooldown — enforced by the TPM chip itself, on a schedule set by its firmware, not by systemd — during which it won't process further authentication attempts. This protection isn't scoped just to your LUKS PIN slot; it's a property of the TPM as a whole, so tripping it can affect other TPM-backed operations on the machine too until it clears. This is a genuine reason to prefer a short, easy-to-type PIN over a long alphanumeric one: you're far less likely to fat-finger a short PIN into a lockout than a long one, and the recovery key/passphrase from step 11 are your fallback if you do get locked out or simply forget it — the TPM lockout doesn't touch those other slots.
11. Add a recovery key (do this before you reboot)
TPM2/PCR-bound unlocking can and does break — most commonly after a routine firmware update changes a bound PCR's value. Without a fallback, this means a full reinstall. Generate one now:
systemd-cryptenroll --recovery-key /dev/sdXY
This prints a high-entropy recovery passphrase. Write it down or save it somewhere off this machine — a locked-out disk with no recovery key and no other working slot is not recoverable.
Add a memorable passphrase too
The recovery key is long and random by design — useful, but not something you'll have memorised or necessarily have on you. A third slot with a passphrase you can actually type is worth having:
cryptsetup luksAddKey /dev/sdXY
You now have three independent unlock paths: TPM2 (automatic), the recovery key (offline, high-entropy), and a passphrase (memorable). Losing any one of them isn't fatal.
Test the fallbacks before you rely on them
Confirm each slot actually works before you trust the setup — --test-passphrase
verifies a slot without opening the device:
cryptsetup open --test-passphrase /dev/sdXY # try the recovery key
cryptsetup open --test-passphrase /dev/sdXY # try the passphrase
A silent exit (return code 0) means the slot matched. Doing this now is considerably cheaper than discovering a typo in your saved recovery key after a firmware update has already locked you out.
12. Finish and reboot
exit
umount -R /mnt
reboot
Remove the install media and confirm the system boots and unlocks automatically via TPM2. Once booted, verify the boot chain directly:
bootctl status
Check for Secure Boot: enabled and TPM2 Support: yes in the output,
and confirm the File: line under "Current Boot Loader" points at
/EFI/systemd/systemd-bootx64.efi — that's the actual binary you signed
and enrolled in step 9, not some other bootloader you didn't expect.
What this avoids, compared to a GRUB-based setup
- No Argon2id vs. PBKDF2 compatibility question —
sd-encryptnever had the limitation GRUB'scryptomountdid - No GRUB SRK/NV-index key-sealing dance
- No dependency on GRUB's unmerged LUKS2
systemd-tpm2token bridge (submitted by Yann Diorcet, still unlanded as of this writing) - UKI is the native model here, not a retrofit
What you give up
- No legacy BIOS boot fallback (UEFI only)
- No GRUB-level exotic filesystem/RAID/LVM traversal — anything below the UKI is the kernel's problem, not the bootloader's (irrelevant for a single LUKS2 partition like this one)
- PCR brittleness is still a real, live risk — this is an
sd-encrypt/systemd-cryptenrollcharacteristic, not something either bootloader fixes. The recovery key in step 11 is not optional in practice.
Appendix: install checklist
A condensed version of the above, for working through at the terminal.
- Firmware Setup Mode entered before booting the live ISO —
confirm with
sbctl statusonce booted; avoids an interrupted mid-install reboot later - Connected to the internet from the live ISO (
iwctlfor Wi-Fi, or wired DHCP), confirmed withping archlinux.org - (Optional)
passwd+systemctl start sshdon the live ISO, SSH'd in from another machine for the rest of the install - Target disk identified correctly with
lsblkbefore touchingparted - Disk wiped (
wipefs -a+sgdisk --zap-all) — fresh single-OS install only, skipped entirely for the dual-boot ESP-reuse case - GPT table created (
parted /dev/sdX -- mklabel gpt) if this is a fresh disk — skipped if reusing an existing ESP - ESP created (~1GiB,
espflag set) and formatted FAT32 (mkfs.fat -F32); root partition created in the remaining space, left unformatted - Root partition LUKS2-formatted with
--pbkdf argon2id - Root mounted at
/mnt, ESP mounted at/mnt/efi - (Optional) swapfile created before
genfstabso it's picked up automatically —btrfs filesystem mkswapfilein the pre-created@swapsubvolume if Btrfs,fallocate+mkswapif ext4 pacstrapincludes microcode (amd-ucode/intel-ucode),sbctl,btrfs-progs(if Btrfs),efibootmgr,dosfstools,networkmanager,sudo,base-devel,linux-headers,zram-generator, plus general tooling (editors,htop,man-pages, etc.)genfstab -U /mnt >> /mnt/etc/fstabrun beforearch-chrootsystemctl enable NetworkManagerrun inside the chroot/etc/systemd/zram-generator.confcreated (nosystemctl enableneeded — it's a generator, picked up automatically at boot)- root password set (
passwd) and a sudo-capable user created (useradd -m -G wheel -s /bin/bash <name>, thenpasswd <name>) %wheel ALL=(ALL:ALL) ALLuncommented viavisudo(never edited directly)bootctl installsystemctl enable systemd-boot-update.service— the stock unit exists but ships disabled on Arch; without it,sbctl's hook re-signs a systemd-boot binary that never actually gets updatedHOOKShassystemd,block,sd-encryptin that order;sd-vconsoleandkeyboardboth present; no legacyencrypthook/etc/vconsole.confset if not on a US keyboard layout- Correct
/etc/mkinitcpio.d/*.presetedited:default_ukiandfallback_ukiset,default_image/fallback_imagecommented out, existingfallback_options="-S autodetect"line left in place mkdir -p /efi/EFI/Linuxbefore first build/etc/kernel/cmdlinecontainsrd.luks.name=<UUID>=cryptroot root=/dev/mapper/cryptroot rw— plusrootflags=subvol=@if you're on Btrfsmkinitcpio -Prun; UKI confirmed present in/efi/EFI/Linux//efi/loader/loader.confexistssbctl statusre-confirmsSetup Mode: Enabledbefore continuing (should already be true from the very first checklist item above)sbctl create-keys,sbctl enroll-keys -m— if this is a reinstall on hardware that's had keys enrolled before,enroll-keyswill fail until you either clear the firmware's Secure Boot state or restore an old key backup (see "Reinstalling on a machine that's already had keys enrolled" above)efibootmgr -vruns cleanly and lists a boot entry for this install — if it errors out, NVRAM isn't accessible from the OS (rare, but worth catching here rather than after reboot)- UKI signed directly; boot manager signed at its source path
(
/usr/lib/systemd/boot/efi/systemd-bootx64.efi) with a.signedsibling, not signed directly on the ESP — signing the ESP copy instead is a confirmed gap (ArchWiki), sincesbctl's pacman hook can't catchsystemd-boot-update.service's later, decoupled ESP refresh bootctl updaterun afterwards to push the signed.signedsibling onto both ESP destinations;sbctl verifyconfirms both report as signedsbctl statusshowsSetup Mode: DisabledandSecure Boot: Enabledtogether — if Secure Boot still reads Disabled, go back into firmware and enable it explicitly before continuing- sbctl keys copied to offline media, and backup verified readable elsewhere
- Auto-signing hook tested:
mkinitcpio -Pthensbctl verify systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/sdXY— or, on a laptop, the--tpm2-with-pin=yesvariant instead, to close the "stolen while powered off" gap (LUKS container, not the mapper device)cryptsetup luksDump /dev/sdXYchecked: matchingsystemd-tpm2entry inTokensand a corresponding slot inKeyslotssystemd-cryptenroll --recovery-key /dev/sdXY; recovery key stored offlinecryptsetup luksAddKey /dev/sdXYfor a memorable passphrase- Both fallbacks tested with
cryptsetup open --test-passphrasebefore rebooting - After reboot:
bootctl statusshowsSecure Boot: enabled,TPM2 Support: yes, and the correctsystemd-bootx64.efipath - Remaining post-install tasks done (locale, hostname)
Appendix: recovery after a firmware change
Two distinct failure modes, with different fixes. Work out which one you're in before doing anything.
Symptom: TPM2 auto-unlock stopped, but you get a passphrase prompt
A bound PCR changed — most commonly PCR 7 after a firmware update. The boot chain is intact; only the TPM binding is stale.
Unlock with your recovery key or passphrase, boot normally, then re-enrol:
systemd-cryptenroll --wipe-slot=tpm2 /dev/sdXY
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/sdXY
# add --tpm2-with-pin=yes on the end here too, if you enrolled with a PIN originally
--wipe-slot=tpm2 removes the stale TPM keyslot specifically, leaving
your recovery key and passphrase slots untouched. Confirm with
cryptsetup luksDump /dev/sdXY before and after — check that the old
systemd-tpm2 token is gone from Tokens after the wipe, and that the
new enrolment has produced a matching pair in Keyslots and Tokens
afterwards (see step 10's enrolment-verification section for what that
should look like). An interrupted wipe or re-enrol can leave an orphaned
token or keyslot behind — this check is how you'd actually catch that,
rather than discovering it the next time you reboot.
Symptom: firmware refuses to boot your UKI at all
The Secure Boot key database was reset to factory defaults — your enrolled keys are gone, so the firmware no longer trusts anything you signed. A LUKS recovery key doesn't help here; the disk isn't the problem.
- Disable Secure Boot in firmware setup to get booting again, or boot from Arch install media and chroot in.
- Restore your backed-up sbctl keys to the path
sbctl setup --print-config | grep keydirreports, or — if the backup is lost —sbctl create-keysto generate new ones. - Put the firmware back into Setup Mode, then:
sbctl enroll-keys -m
sbctl sign -s /efi/EFI/Linux/arch-linux.efi
sbctl sign -s -o /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed \
/usr/lib/systemd/boot/efi/systemd-bootx64.efi
bootctl update
sbctl verify
- Enable Secure Boot and reboot.
If you generated new keys rather than restoring the originals, PCR 7 has changed as a result — so you'll also need the TPM re-enrolment from the first case above.