Install Arch Linux with full disk encryption
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
sbctlsection.
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#
- 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.
- 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.
- 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).
- Format the ESP.
mkfs.fat -F 32 /dev/<your-disk-efi>
- Create the LUKS2 container and open it. On an SSD, enable TRIM right away:
--persistentstores 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.
- 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.
- 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.
- Install the base system. Pick
intel-ucodeoramd-ucodefor your CPU.
pacstrap -K /mnt base linux linux-firmware lvm2 intel-ucode \
networkmanager sudo vim man-db git openssh
- Generate fstab and enter the new system.
genfstab -U /mnt >> /mnt/etc/fstab
arch-chroot /mnt
- Time zone and hardware clock.
ln -sf /usr/share/zoneinfo/Europe/Kyiv /etc/localtime
hwclock --systohc
- Locale. Uncomment
en_US.UTF-8 UTF-8in/etc/locale.gen, then:
locale-gen
echo LANG=en_US.UTF-8 > /etc/locale.conf
- Console keymap. The
sd-vconsolehook reads this file, andmkinitcpiowarns without it:
echo KEYMAP=us > /etc/vconsole.conf
- Hostname.
echo yourhostname > /etc/hostname
- 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.
- 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
- Find the UUID of the LUKS partition. It has to be the partition (
TYPE="crypto_LUKS"), not/dev/mapper/cryptlvmand 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)#
- Install the boot loader to the ESP:
bootctl install
bootctl adds the NVRAM entry and puts it first in the boot order.
/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.
- 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.
- A fallback entry, if
mkinitcpiobuilt 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
- Update systemd-boot itself automatically after a
systemdupgrade:
systemctl enable systemd-boot-update.service
- Check:
bootctl list
Option B: GRUB#
- Install GRUB to the ESP:
pacman -S grub efibootmgr
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
- 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.
- Generate the configuration:
grub-mkconfig -o /boot/grub/grub.cfg
Rerun grub-mkconfig after every change to /etc/default/grub.
Finish the Installation#
- Network and TRIM:
systemctl enable NetworkManager
systemctl enable fstrim.timer
- 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.
- Exit, unmount and reboot:
exit
umount -R /mnt
cryptsetup close cryptlvm
reboot
- systemd asks for the
cryptlvmpassphrase at boot, then the usual login. Check connectivity:ping archlinux.org. - 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:
- The LUKS passphrase to match the user password (which protects the keyring by default).
- 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.
- 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
- In
/etc/mkinitcpio.d/linux.presetcomment outdefault_imageandfallback_image, uncommentdefault_ukiandfallback_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"
- 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
- In
loader.confchangedefault arch.conftodefault 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#
- https://wiki.archlinux.org/title/Installation_guide
- https://wiki.archlinux.org/title/Dm-crypt/Encrypting_an_entire_system#LVM_on_LUKS
- https://wiki.archlinux.org/title/Dm-crypt/System_configuration#Using_systemd-cryptsetup-generator
- https://wiki.archlinux.org/title/Systemd-boot
- https://wiki.archlinux.org/title/GRUB
- https://wiki.archlinux.org/title/Unified_kernel_image
- https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot#sbctl
- https://wiki.archlinux.org/title/Systemd-cryptenroll
- https://wiki.archlinux.org/title/Zram