Prepare the System#

  • Data Backup: the whole disk will be erased.
  • UEFI Mode: set the boot mode to UEFI in the firmware settings and disable CSM/Legacy.
  • Secure Boot off for the install: the Arch ISO is not signed. It can be turned back on afterwards, see the sbctl section.

Prepare the USB Drive#

  • Install Ventoy and copy the ISO from archlinux.org/download onto the drive.
  • Or write the ISO directly: dd if=archlinux.iso of=/dev/sdX bs=4M status=progress oflag=sync.

Disk Partition Structure#

Partition Size Type (fdisk) Contents
ESP 1 GiB uefi FAT32, mounted at /boot
LUKS the rest linux LUKS2 holding LVM: vg0/root, vg0/home

Device names used in this document:

Device In this Doc Examples
Disk Device /dev/<your-disk> /dev/sda, /dev/nvme0n1
EFI Partition /dev/<your-disk-efi> /dev/sda1, /dev/nvme0n1p1
LUKS Partition /dev/<your-disk-luks> /dev/sda2, /dev/nvme0n1p2

Why /boot is not encrypted: systemd-boot cannot read LUKS at all, and GRUB only handles LUKS1 or LUKS2 with PBKDF2 - it cannot open a volume using the default argon2id. What protects an unencrypted /boot from tampering is Secure Boot.


Install Arch Linux#

  1. Boot from the USB drive and make sure the system is in UEFI mode:
cat /sys/firmware/efi/fw_platform_size

It should print 64. If the file does not exist, you booted in BIOS mode.

  1. Connect to the internet. Wired works out of the box, for Wi-Fi use iwctl:
iwctl
[iwd]# station wlan0 connect "YOUR_SSID"

Check with ping archlinux.org. The clock syncs on its own, see timedatectl.

  1. Partition the disk.
fdisk /dev/<your-disk>

Create a new GPT table (this erases the entire disk):

Command (m for help): g

The ESP:

Command (m for help): n
Partition number: <Press Enter>
First sector: <Press Enter>
Last sector, +/-sectors or +/-size{K,M,G,T,P}: +1G

Command (m for help): t
Partition type or alias (type L to list all): uefi

The LUKS partition, using the rest of the disk:

Command (m for help): n
Partition number: <Press Enter>
First sector: <Press Enter>
Last sector, +/-sectors or +/-size{K,M,G,T,P}: <Press Enter>

Command (m for help): t
Partition number: 2
Partition type or alias (type L to list all): linux

Print the table (p), check it, then write it (w).

  1. Format the ESP.
mkfs.fat -F 32 /dev/<your-disk-efi>
  1. Create the LUKS2 container and open it. On an SSD, enable TRIM right away: --persistent stores the flag in the LUKS header, so nothing extra is needed at boot.
cryptsetup luksFormat /dev/<your-disk-luks>
cryptsetup open --allow-discards --persistent /dev/<your-disk-luks> cryptlvm

Check:

cryptsetup luksDump /dev/<your-disk-luks> | grep Flags
Flags:          allow-discards

The trade-off of TRIM on an encrypted disk (free blocks become visible) is described in dm-crypt/Specialties. Skip the flags on an HDD.

  1. Create the LVM volumes.

You can skip creating the home volume and use a single root volume for the system.

pvcreate /dev/mapper/cryptlvm
vgcreate vg0 /dev/mapper/cryptlvm

lvcreate -L 100G vg0 -n root
lvcreate -l 100%FREE vg0 -n home
lvreduce -L -256M vg0/home

The 256 MiB left free in the volume group is what e2scrub needs for its snapshot. There is no swap volume, swap goes to zram. If you need hibernation, add lvcreate -L <RAM size> vg0 -n swap before home.

  1. Format and mount.
mkfs.ext4 /dev/vg0/root
mkfs.ext4 /dev/vg0/home

mount /dev/vg0/root /mnt
mount --mkdir -o fmask=0077,dmask=0077 /dev/<your-disk-efi> /mnt/boot
mount --mkdir /dev/vg0/home /mnt/home

fmask/dmask stop bootctl from complaining that random-seed is world readable. genfstab carries these options into fstab.

  1. Install the base system. Pick intel-ucode or amd-ucode for your CPU.
pacstrap -K /mnt base linux linux-firmware lvm2 intel-ucode \
         networkmanager sudo vim man-db git openssh
  1. Generate fstab and enter the new system.
genfstab -U /mnt >> /mnt/etc/fstab
arch-chroot /mnt
  1. Time zone and hardware clock.
ln -sf /usr/share/zoneinfo/Europe/Kyiv /etc/localtime
hwclock --systohc
  1. Locale. Uncomment en_US.UTF-8 UTF-8 in /etc/locale.gen, then:
locale-gen
echo LANG=en_US.UTF-8 > /etc/locale.conf
  1. Console keymap. The sd-vconsole hook reads this file, and mkinitcpio warns without it:
echo KEYMAP=us > /etc/vconsole.conf
  1. Hostname.
echo yourhostname > /etc/hostname
  1. Create a user.
useradd -m -G wheel -s /bin/bash yourusername
passwd yourusername
EDITOR=vim visudo
# ---> Uncomment "%wheel ALL=(ALL:ALL) ALL"

Leaving the root password unset keeps root login locked, everything goes through sudo.

  1. Configure the initramfs. In /etc/mkinitcpio.conf:
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt lvm2 filesystems fsck)

Order matters: keyboard and sd-vconsole come before sd-encrypt, otherwise the password prompt may have no keyboard or the wrong layout. sd-encrypt comes before lvm2, because LVM lives inside LUKS.

mkinitcpio -P
  1. Find the UUID of the LUKS partition. It has to be the partition (TYPE="crypto_LUKS"), not /dev/mapper/cryptlvm and not /dev/vg0/root:
blkid -s UUID -o value /dev/<your-disk-luks>

Below it is called <LUKS-UUID>. To avoid copying it by hand, the examples substitute it with $(blkid ...). 17. Install a boot loader - one of the two options below.

Option A: systemd-boot (no GRUB)#

  1. Install the boot loader to the ESP:
bootctl install

bootctl adds the NVRAM entry and puts it first in the boot order.

  1. /boot/loader/loader.conf:
default  arch.conf
timeout  3
console-mode max
editor   no

editor no prevents editing the kernel command line from the menu, otherwise anyone at the keyboard can append init=/bin/sh.

  1. The Arch entry:
cat > /boot/loader/entries/arch.conf <<EOF
title   Arch Linux
linux   /vmlinuz-linux
initrd  /initramfs-linux.img
options rd.luks.name=$(blkid -s UUID -o value /dev/<your-disk-luks>)=cryptlvm root=/dev/vg0/root rw
EOF

rw is required here: GRUB adds it on its own, systemd-boot does not. No initrd /intel-ucode.img line, the microcode is already inside the initramfs thanks to the microcode hook.

  1. A fallback entry, if mkinitcpio built a fallback image (ls /boot/initramfs-linux-fallback.img):
sed -e 's/^title.*/title   Arch Linux (fallback)/' \
    -e 's/initramfs-linux.img/initramfs-linux-fallback.img/' \
    /boot/loader/entries/arch.conf > /boot/loader/entries/arch-fallback.conf
  1. Update systemd-boot itself automatically after a systemd upgrade:
systemctl enable systemd-boot-update.service
  1. Check:
bootctl list

Option B: GRUB#

  1. Install GRUB to the ESP:
pacman -S grub efibootmgr
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
  1. Set the kernel command line in /etc/default/grub:
GRUB_CMDLINE_LINUX="rd.luks.name=<LUKS-UUID>=cryptlvm root=/dev/vg0/root"

Or in one command:

sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"rd.luks.name=$(blkid -s UUID -o value /dev/<your-disk-luks>)=cryptlvm root=/dev/vg0/root\"|" /etc/default/grub

GRUB_ENABLE_CRYPTODISK is not needed: /boot is not encrypted, GRUB decrypts nothing, the initramfs does.

  1. Generate the configuration:
grub-mkconfig -o /boot/grub/grub.cfg

Rerun grub-mkconfig after every change to /etc/default/grub.


Finish the Installation#

  1. Network and TRIM:
systemctl enable NetworkManager
systemctl enable fstrim.timer
  1. Swap on zram:
pacman -S zram-generator
cat > /etc/systemd/zram-generator.conf <<EOF
[zram0]
zram-size = min(ram / 2, 8192)
compression-algorithm = zstd
EOF

After a reboot swapon --show should list /dev/zram0.

  1. Exit, unmount and reboot:
exit
umount -R /mnt
cryptsetup close cryptlvm
reboot
  1. systemd asks for the cryptlvm passphrase at boot, then the usual login. Check connectivity: ping archlinux.org.
  2. A desktop, for example GNOME:
sudo pacman -S gnome gdm
sudo systemctl enable gdm

Or KDE Plasma: sudo pacman -S plasma-meta sddm and sudo systemctl enable sddm.


IMPORTANT#

After the first boot, make the LUKS header backup from the Backup LUKS Header section and keep it off this disk.


Notes#

Backup LUKS Header#

A damaged header means all data is lost, and the passphrase will not help. Make a copy and keep it somewhere other than this disk:

sudo cryptsetup luksHeaderBackup /dev/<your-disk-luks> \
     --header-backup-file luks-header-backup-$(date -I).img

To restore:

sudo cryptsetup luksHeaderRestore /dev/<your-disk-luks> \
     --header-backup-file /path/to/luks-header-backup.img

The backup contains the key slots, so an old passphrase later removed from the disk still opens the data through this backup. Store it accordingly.

Entering Your Password Only Once (GNOME)#

sd-encrypt puts the entered LUKS passphrase into the kernel keyring, and GDM can pick it up from there to unlock the GNOME Keyring. This needs:

  1. The LUKS passphrase to match the user password (which protects the keyring by default).
  2. Automatic Login enabled in Settings - Users.

After that the password is typed once, at the disk unlock prompt. More info in this Reddit post.

UKI Instead of Separate vmlinuz and initramfs (systemd-boot)#

A Unified Kernel Image is the kernel, initramfs, microcode and kernel command line in one EFI file. systemd-boot picks such files up from /boot/EFI/Linux/ by itself, with no loader/entries. The main reason is Secure Boot: one file gets signed, the command line along with it, and it cannot be swapped.

  1. Kernel command line in /etc/kernel/cmdline:
echo "rd.luks.name=$(blkid -s UUID -o value /dev/<your-disk-luks>)=cryptlvm root=/dev/vg0/root rw" > /etc/kernel/cmdline
  1. In /etc/mkinitcpio.d/linux.preset comment out default_image and fallback_image, uncomment default_uki and fallback_uki, and point them at /boot/EFI/Linux/:
default_uki="/boot/EFI/Linux/arch-linux.efi"
fallback_uki="/boot/EFI/Linux/arch-linux-fallback.efi"
  1. Build, then remove the old entries and images:
mkdir -p /boot/EFI/Linux
mkinitcpio -P
rm /boot/loader/entries/arch*.conf /boot/initramfs-linux*.img
  1. In loader.conf change default arch.conf to default arch-linux.efi.

Secure Boot with sbctl#

Do this once the system already boots. Put Secure Boot into Setup Mode in the firmware (clear the platform keys), then:

sudo pacman -S sbctl
sbctl status                     # should show Setup Mode: Enabled
sudo sbctl create-keys
sudo sbctl enroll-keys -m        # -m keeps the Microsoft keys, GPU option ROMs may need them

Sign everything the firmware loads:

sudo sbctl verify                # lists unsigned files
sudo sbctl sign -s /boot/EFI/systemd/systemd-bootx64.efi
sudo sbctl sign -s /boot/EFI/BOOT/BOOTX64.EFI
sudo sbctl sign -s /boot/EFI/Linux/arch-linux.efi     # with UKI
# without UKI: sudo sbctl sign -s /boot/vmlinuz-linux

-s records the file, and the sbctl pacman hook re-signs it after every upgrade. Then enable Secure Boot in the firmware.

Secure Boot also works with GRUB, but with more effort: grub-install has to be run with --modules="tpm" --disable-shim-lock, and neither grub.cfg nor the initramfs gets signed. If Secure Boot matters, that is an argument for systemd-boot with a UKI.

Unlocking with TPM2 or FIDO2#

Requires sd-encrypt. The passphrase stays as a fallback key slot.

TPM2 (the disk unlocks by itself as long as the boot chain is unchanged):

sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/<your-disk-luks>

PCR 7 is the Secure Boot state. Without Secure Boot enabled, TPM unlock protects almost nothing: anyone with physical access boots their own system and the TPM hands over the key just the same. For an extra PIN add --tpm2-with-pin=yes.

A FIDO2 key (YubiKey and similar):

sudo systemd-cryptenroll --fido2-device=auto /dev/<your-disk-luks>

In both cases add rd.luks.options=tpm2-device=auto or rd.luks.options=fido2-device=auto to the kernel command line (the options line of the systemd-boot entry, /etc/kernel/cmdline for a UKI, or GRUB_CMDLINE_LINUX) and rebuild the initramfs or UKI (mkinitcpio -P).

A recovery key you can write down on paper:

sudo systemd-cryptenroll --recovery-key /dev/<your-disk-luks>

Automated Install Script (systemd-boot)#

The steps above as one script: GPT with an ESP and a LUKS2 partition, LVM with root and home, sd-encrypt, systemd-boot with a fallback entry, zram swap, NetworkManager. It erases the target disk.

Usage from the Arch ISO, once the network is up:

# or copy it from a USB drive
curl -LO https://yousysadmin.com/posts/install-arch-full-encrypt/arch-install.sh

vim arch-install.sh # set DISK and the other variables
bash arch-install.sh

It asks for the LUKS passphrase and the user password (each twice), and for the disk name once more before wiping it. The microcode package is picked from /proc/cpuinfo. It does not reboot by itself.

#!/usr/bin/env bash
# Arch Linux install: LVM on LUKS2, sd-encrypt, systemd-boot.
# Run as root from the Arch ISO. ERASES THE TARGET DISK.
set -euo pipefail

DISK=/dev/nvme0n1
HOST_NAME=archlinux
USERNAME=user
TIMEZONE=Europe/Kyiv
LOCALE=en_US.UTF-8
KEYMAP=us
ESP_SIZE=1G
ROOT_SIZE=100G
# yes on an SSD, no on an HDD
DISCARD=yes
PACKAGES=(base linux linux-firmware lvm2 networkmanager sudo vim man-db git openssh zram-generator)
HOOKS="base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt lvm2 filesystems fsck"

die() {
    echo "error: $*" >&2
    exit 1
}

# Prompts go to stderr so the value is the only thing on stdout.
ask_secret() {
    local a b
    while true
    do
        read -rsp "$1: " a
        echo >&2
        read -rsp "$1 (again): " b
        echo >&2
        [[ -n $a && $a == "$b" ]] && break
        echo "Empty or does not match, try again." >&2
    done

    printf '%s' "$a"
}

[[ $EUID -eq 0 ]] || die "run as root"
[[ -d /sys/firmware/efi/efivars ]] || die "not booted in UEFI mode"
[[ -b $DISK ]] || die "$DISK is not a block device"
ping -c 1 -W 3 archlinux.org > /dev/null || die "no network"

# nvme0n1 and mmcblk0 get a p before the partition number, sda does not.
PART=$DISK
[[ $DISK =~ [0-9]$ ]] && PART=${DISK}p
ESP=${PART}1
LUKS=${PART}2

UCODE=amd-ucode
grep -q GenuineIntel /proc/cpuinfo && UCODE=intel-ucode

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS "$DISK"
echo
read -rp "Everything on $DISK will be erased. Type $DISK to continue: " answer
[[ $answer == "$DISK" ]] || die "aborted"

LUKS_PASS=$(ask_secret "LUKS passphrase")
USER_PASS=$(ask_secret "Password for $USERNAME")

echo ">>> Partitioning $DISK"
wipefs -af "$DISK"
sgdisk --zap-all "$DISK"
sgdisk -n "1:0:+$ESP_SIZE" -t 1:ef00 -c 1:EFI \
    -n 2:0:0 -t 2:8309 -c 2:cryptlvm "$DISK"
udevadm settle

mkfs.fat -F 32 -n EFI "$ESP"

echo ">>> Setting up LUKS2"
open_args=()
[[ $DISCARD == yes ]] && open_args=(--allow-discards --persistent)
printf '%s' "$LUKS_PASS" | cryptsetup luksFormat --batch-mode --key-file - "$LUKS"
printf '%s' "$LUKS_PASS" | cryptsetup open "${open_args[@]}" --key-file - "$LUKS" cryptlvm
LUKS_UUID=$(blkid -s UUID -o value "$LUKS")

echo ">>> Setting up LVM"
pvcreate /dev/mapper/cryptlvm
vgcreate vg0 /dev/mapper/cryptlvm
lvcreate -L "$ROOT_SIZE" vg0 -n root
lvcreate -l 100%FREE vg0 -n home
# e2scrub needs 256 MiB free in the volume group for its snapshot
lvreduce --yes -L -256M vg0/home

mkfs.ext4 /dev/vg0/root
mkfs.ext4 /dev/vg0/home

mount /dev/vg0/root /mnt
mount --mkdir -o fmask=0077,dmask=0077 "$ESP" /mnt/boot
mount --mkdir /dev/vg0/home /mnt/home

echo ">>> Installing packages"
pacstrap -K /mnt "${PACKAGES[@]}" "$UCODE"
genfstab -U /mnt >> /mnt/etc/fstab

echo ">>> Configuring the system"
ln -sf "/usr/share/zoneinfo/$TIMEZONE" /mnt/etc/localtime
arch-chroot /mnt hwclock --systohc

sed -i "s/^#\s*\($LOCALE UTF-8\)/\1/" /mnt/etc/locale.gen
arch-chroot /mnt locale-gen
echo "LANG=$LOCALE" > /mnt/etc/locale.conf
echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf
echo "$HOST_NAME" > /mnt/etc/hostname

arch-chroot /mnt useradd -m -G wheel -s /bin/bash "$USERNAME"
printf '%s:%s\n' "$USERNAME" "$USER_PASS" | arch-chroot /mnt chpasswd
echo '%wheel ALL=(ALL:ALL) ALL' > /mnt/etc/sudoers.d/wheel
chmod 440 /mnt/etc/sudoers.d/wheel

cat > /mnt/etc/systemd/zram-generator.conf <<EOF
[zram0]
zram-size = min(ram / 2, 8192)
compression-algorithm = zstd
EOF

echo ">>> Building the initramfs"
sed -i "s/^HOOKS=.*/HOOKS=($HOOKS)/" /mnt/etc/mkinitcpio.conf
arch-chroot /mnt mkinitcpio -P

echo ">>> Installing systemd-boot"
arch-chroot /mnt bootctl install

cat > /mnt/boot/loader/loader.conf <<EOF
default  arch.conf
timeout  3
console-mode max
editor   no
EOF

OPTIONS="rd.luks.name=$LUKS_UUID=cryptlvm root=/dev/vg0/root rw"

cat > /mnt/boot/loader/entries/arch.conf <<EOF
title   Arch Linux
linux   /vmlinuz-linux
initrd  /initramfs-linux.img
options $OPTIONS
EOF

if [[ -f /mnt/boot/initramfs-linux-fallback.img ]]
then
    cat > /mnt/boot/loader/entries/arch-fallback.conf <<EOF
title   Arch Linux (fallback)
linux   /vmlinuz-linux
initrd  /initramfs-linux-fallback.img
options $OPTIONS
EOF
fi

arch-chroot /mnt systemctl enable NetworkManager fstrim.timer systemd-boot-update.service

unset LUKS_PASS USER_PASS

echo ">>> Unmounting"
umount -R /mnt
cryptsetup close cryptlvm

echo
echo "Done. Remove the USB drive and run: reboot"

References#