I finally caved and decided to try Arch after a disappointing experience with KDE Neon.
I spent a good part of the week working on this in a VM. It installs a stock, headless Arch onto a fairly complex storage configuration, mixing mdadm, luks and lvm.
Highlights are:
- Noninteractive except for setting a password for your user at the end
- Encrypted boot, root, home and swap in separate luks containers
- Mirrored boot (will boot from either drive in the boot mirror with no intervention) - yes it is bios, but…
- ESP partition is there for future-proofness
- Script can be re-run over itself in case you want to make a change and run it again. It blows away everything on the disks at the beginning and ignores things like “we found a partition/crypt/whatever are you sure you want to blah blah.”
Anyway, this is the first time I’ve done a lot of these things manually, so I am looking for any willing, critical eyes. Very likely I have overlooked something or at the very least done something sub-optimally.
Below is outdated, please reference the gist
Here is how the storage is laid out (vda1 and vdb1 are biosboot):
NAME FSTYPE FSVER LABEL MOUNTPOINT
vda
├─vda1
├─vda2 vfat FAT32 ESP
├─vda3 linux_raid_member 1.0 adm3:boot_mirror
│ └─md127 crypto_LUKS 1
│ └─boot_luks ext4 1.0 boot /mnt/boot
├─vda4 linux_raid_member 1.2 adm3:swap_mirror
│ └─md126
└─vda5 linux_raid_member 1.2 adm3:root_mirror
└─md125 crypto_LUKS 2
└─root_luks LVM2_member LVM2 001
├─root_vg-root_lv ext4 1.0 root /mnt
├─root_vg-var_lv ext4 1.0 var /mnt/var
├─root_vg-var_log_lv ext4 1.0 var_log /mnt/var/log
└─root_vg-var_log_audit_lv ext4 1.0 var_log_audit /mnt/var/log/audit
vdb
├─vdb1
├─vdb2 vfat FAT32 ESP
├─vdb3 linux_raid_member 1.0 adm3:boot_mirror
│ └─md127 crypto_LUKS 1
│ └─boot_luks ext4 1.0 boot /mnt/boot
├─vdb4 linux_raid_member 1.2 adm3:swap_mirror
│ └─md126
└─vdb5 linux_raid_member 1.2 adm3:root_mirror
└─md125 crypto_LUKS 2
└─root_luks LVM2_member LVM2 001
├─root_vg-root_lv ext4 1.0 root /mnt
├─root_vg-var_lv ext4 1.0 var /mnt/var
├─root_vg-var_log_lv ext4 1.0 var_log /mnt/var/log
└─root_vg-var_log_audit_lv ext4 1.0 var_log_audit /mnt/var/log/audit
vdc
└─vdc1 crypto_LUKS 2
└─home_luks LVM2_member LVM2 001
└─home_vg-home_lv ext4 1.0 home /mnt/home
Here is the script. I’ve divided into sections to make it easier to look at here.
Preliminary
#!/usr/bin/env zsh
# WARNING: THIS SCRIPT WILL AGGRESSIVELY DESTROY ALL DATA ON ALL DRIVES
# ON THIS SYSTEM!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
# This script assumes 3 drives (vda vdb and vdc) are present and the
# first 2 are identical in size. The majority of the operating system
# is installed onto a mirror of vda and vdb. Home is installed on vdc.
########################################################################
########################################################################
# PRELIMINARY ##########################################################
########################################################################
set -euxo pipefail
setopt +o nomatch
trap -- 'echo "FAIL: ${pipestatus[@]}"' "ERR"
# Check reverse dns for hostname
declare hostname="$( dig -x "$( hostname -i )" +noall +answer |
awk '/\.$/ { print substr($NF, 1, length($NF)-1) }' )"
# Otherwise set manually
: ${hostname:=adm3}
# User
declare adm_user='o0-o'
# NTP
timedatectl set-ntp true
# Example: a mirror for esp (for future use), boot, swap, root and a
# dedicated drive for home
declare -a boot_swap_root_mirror=( '/dev/vda' '/dev/vdb' )
declare home='/dev/vdc'
Reset Storage
########################################################################
# RESET STORAGE ########################################################
########################################################################
# Unmount everything but live environment
declare notypes='nooverlay,noproc,nosysfs,nodevtmpfs,notmpfs,noiso9660'
declare notypes="${notypes},nodevpts,nocgroup2"
umount --force --recursive '/mnt' || :
umount --force --all \
--types "${notypes}" || :
# Turn off swap
swapoff --all
# Deactivate all LVM logical volumes
lvchange --activate 'n' \
-- \
$( lvs --noheadings --rows --options 'lv_path' ) || :
# Forcefully remove all LVM physical volumes
pvremove --yes --force --force \
-- \
$( pvs --noheadings --rows --options 'pv_name' ) || :
# Close all LUKS containers
lsblk --noheadings \
--list \
--output NAME,TYPE |
tac |
grep "crypt$" |
while read -r crypt; do
cryptsetup close "${crypt% *}"
done
# Remove all md devices
for md in '/dev/md'?*; do
umount --lazy "${md}" || :
echo idle > "/sys/block/${md##*/}/md/sync_action" || :
echo none > "/sys/block/${md##*/}/md/resync_start" || :
mdadm --stop "${md}" || :
mdadm --remove "${md}" || :
done
lsblk --noheadings \
--list \
--output NAME |
tac |
while read -r dev; do
mdadm --misc --force --zero-superblock "/dev/${dev}" || :
done
# Clear partitions
for drive in "${boot_swap_root_mirror[@]}" "${home}"; do
sgdisk --zap-all "${drive}"
done
# Show cleared out storage
set +x; echo; echo; echo; lsblk; ls '/dev/md'?* 2>/dev/null || :; echo; echo
echo; sleep 3; set -x
Partition
########################################################################
# PARTITION ############################################################
########################################################################
# Create partitions for boot mirror and root mirror, create biosboot and
# esp partitions to keep future options open
for drive in "${boot_swap_root_mirror[@]}"; do
sgdisk --zap-all \
--new '1:0:+1M' \
--typecode '1:ef02' \
--change-name '1:biosboot' \
--partition-guid '1:21686148-6449-6E6F-744E-656564454649' \
--new '2:0:+550M' \
--typecode '2:ef00' \
--change-name '2:ESP' \
--new '3:0:+4G' \
--typecode '3:fd00' \
--change-name '3:boot_mirror_part' \
--new '4:0:+4G' \
--typecode '4:fd00' \
--change-name '4:swap_mirror_part' \
--new '5:0:-100M' \
--typecode '5:fd00' \
--change-name '5:root_mirror_part' \
"${drive}"
done
# Create partition for home (luks)
sgdisk --zap-all \
--new '1:0:-100M' \
--typecode '1:8309' \
--change-name '1:home_part' \
"${home}"
RAID
########################################################################
# RAID #################################################################
########################################################################
yes |
mdadm --create \
--force \
--level '1' \
--metadata '1.0' \
--bitmap 'internal' \
--homehost "${hostname}" \
--raid-devices "${#boot_swap_root_mirror[@]}" \
'/dev/md/boot_mirror' \
"${boot_swap_root_mirror[@]/%/3}" ||
[ "${pipestatus[2]}" = '0' ]
# yes will always cause pipefail
yes |
mdadm --create \
--force \
--level '1' \
--metadata '1.2' \
--bitmap 'internal' \
--homehost "${hostname}" \
--raid-devices "${#boot_swap_root_mirror[@]}" \
'/dev/md/swap_mirror' \
"${boot_swap_root_mirror[@]/%/4}" ||
[ "${pipestatus[2]}" = '0' ]
shred --zero --size '20MiB' '/dev/md/swap_mirror'
yes |
mdadm --create \
--force \
--level '1' \
--metadata '1.2' \
--bitmap 'internal' \
--homehost "${hostname}" \
--raid-devices "${#boot_swap_root_mirror[@]}" \
'/dev/md/root_mirror' \
"${boot_swap_root_mirror[@]/%/5}" ||
[ "${pipestatus[2]}" = '0' ]
Encryption
########################################################################
# ENCRYPTION ###########################################################
########################################################################
# Prep
for dev in '/dev/md/boot_mirror' '/dev/md/root_mirror' "${home}"; do
yes 'YES' |
cryptsetup open --type 'plain' \
--key-file '/dev/random' \
"${dev}" \
'container' ||
[ "${pipestatus[2]}" = '0' ]
# Uncomment when you do it for real
# dd if='/dev/zero' \
# of='/dev/mapper/container' \
# bs='1M' \
# status='progress' || : # Exit 1 expected
cryptsetup close 'container'
done
# Key file
dd if='/dev/urandom' \
of='luks.key' \
bs='512' \
count='1'
chmod '600' 'luks.key'
# Boot
yes 'YES' |
cryptsetup luksFormat --type 'luks1' \
--key-file 'luks.key' \
'/dev/md/boot_mirror' ||
[ "${pipestatus[2]}" = '0' ]
cryptsetup open --key-file 'luks.key' \
'/dev/md/boot_mirror' \
'boot_luks'
# Root
yes 'YES' |
cryptsetup luksFormat --key-file 'luks.key' \
'/dev/md/root_mirror' ||
[ "${pipestatus[2]}" = '0' ]
cryptsetup open --key-file 'luks.key' \
'/dev/md/root_mirror' \
'root_luks'
# Home
yes 'YES' |
cryptsetup luksFormat --key-file 'luks.key' \
"${home}1" ||
[ "${pipestatus[2]}" = '0' ]
cryptsetup open --key-file 'luks.key' \
"${home}1" \
'home_luks'
LVM
########################################################################
# LVM ##################################################################
########################################################################
# Root
pvcreate --yes \
--force --force \
'/dev/mapper/root_luks'
vgcreate 'root_vg' \
'/dev/mapper/root_luks'
lvcreate --size '48G' \
--name 'root_lv' \
'root_vg'
# Var
lvcreate --size '32G' \
--name 'var_lv' \
'root_vg'
# Log
lvcreate --size '8G' \
--name 'var_log_lv' \
'root_vg'
# Audit log
lvcreate --size '8G' \
--name 'var_log_audit_lv' \
'root_vg'
# Home
pvcreate --yes \
--force --force \
'/dev/mapper/home_luks'
vgcreate 'home_vg' \
'/dev/mapper/home_luks'
lvcreate --extents '67%FREE' \
--name 'home_lv' \
'home_vg'
File Systems
########################################################################
# FILE SYSTEMS #########################################################
########################################################################
# ESP
for drive in "${boot_swap_root_mirror[@]}"; do
mkfs.vfat -F '32' \
-n 'ESP' \
"${drive}"1
done
# Boot
mkfs.ext4 -FF -L 'boot' \
'/dev/mapper/boot_luks'
# Root
mkfs.ext4 -FF -L 'root' \
'/dev/root_vg/root_lv'
# Var
mkfs.ext4 -FF -L 'var' \
'/dev/root_vg/var_lv'
# Log
mkfs.ext4 -FF -L 'var_log' \
'/dev/root_vg/var_log_lv'
# Audit log
mkfs.ext4 -FF -L 'var_log_audit' \
'/dev/root_vg/var_log_audit_lv'
# Home
mkfs.ext4 -FF -L 'home' \
'/dev/home_vg/home_lv'
Mount and Install OS
########################################################################
# MOUNT AND INSTALL OS #################################################
########################################################################
mount '/dev/root_vg/root_lv' '/mnt'
mkdir '/mnt/var'
mount '/dev/root_vg/var_lv' '/mnt/var'
mkdir '/mnt/var/log'
mount '/dev/root_vg/var_log_lv' '/mnt/var/log'
mkdir '/mnt/var/log/audit'
mount '/dev/root_vg/var_log_audit_lv' '/mnt/var/log/audit'
mkdir '/mnt/home'
mount '/dev/home_vg/home_lv' '/mnt/home'
mkdir '/mnt/boot'
mount '/dev/mapper/boot_luks' '/mnt/boot'
# Show formatted and mounted storage
set +x; echo; echo; echo; lsblk -f; echo; echo; echo; sleep 3; set -x
# Install the base OS
pacstrap '/mnt' base base-devel \
linux linux-firmware intel-ucode \
grub mkinitcpio \
networkmanager \
mdadm lvm2 \
zsh vim git \
openssh
Configure Storage
########################################################################
# STORAGE ##############################################################
########################################################################
# Configure mdadm
mdadm --detail \
--scan >> '/mnt/etc/mdadm.conf'
# Configure luks
# Transfer keys to chroot
cp --archive 'luks.key' '/mnt/etc/'
# Add swap and home to crypttab
printf '%s\t%s\t%s\t%s\n' \
'swap' \
"$( find -L '/dev/disk' \
-samefile '/dev/md/swap_mirror' |
head --lines '1' )" \
'/dev/urandom' \
'swap,cipher=aes-xts-plain64,size=256' \
'home_luks' \
"UUID=$( blkid --match-tag 'UUID' --output 'value' "${home}1" )" \
'/etc/luks.key' \
'luks,discard' >> '/mnt/etc/crypttab'
# Add boot and root to initramfs
printf '%s\t%s\t%s\t%s\n' \
'boot_luks' \
"UUID=$( blkid --match-tag 'UUID' \
--output 'value' \
'/dev/md/boot_mirror' )" \
'/etc/luks.key' \
'luks,discard' \
'root_luks' \
"UUID=$( blkid --match-tag 'UUID' \
--output 'value' \
'/dev/md/root_mirror' )" \
'/etc/luks.key' \
'luks,discard' >> '/mnt/etc/crypttab.initramfs'
# Configure fstab
genfstab '/mnt' >> '/mnt/etc/fstab'
# Swap is re-encrypted each boot via crypttab
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
'/dev/mapper/swap' \
'none' \
'swap' \
'defaults' \
'0' \
'0' >> '/mnt/etc/fstab'
Misc
########################################################################
# MISC CONFIG ##########################################################
########################################################################
# Time
arch-chroot '/mnt' \
hwclock --systohc
arch-chroot '/mnt' \
ln --symbolic \
--force \
'/usr/share/zoneinfo/America/New_York' \
'/etc/localtime'
# Locale
sed --in-place \
--expression '/#en_US.UTF-8/ s/^#//' \
'/mnt/etc/locale.gen'
printf 'LANG=%s.%s' \
'en_US' \
'UTF-8' > '/mnt/etc/locale.conf'
printf 'KEYMAP=%s' \
'us' > '/mnt/etc/vconsole.conf'
arch-chroot '/mnt' locale-gen
# Hostname
printf '%s' "${hostname}" > '/mnt/etc/hostname'
printf '%s\t%s' \
'127.0.1.1' \
"${hostname}" >> '/mnt/etc/hosts'
Services
########################################################################
# SERVICES #############################################################
########################################################################
# Will expand more here in the future
arch-chroot '/mnt' systemctl enable sshd
Bootloader and Init
########################################################################
# BOOTLOADER AND INIT ##################################################
########################################################################
# mkinitcpio
declare files='/etc/luks.key'
declare hooks='base systemd autodetect keyboard sd-vconsole modconf block'
declare hooks="${hooks} mdadm_udev sd-encrypt sd-lvm2 filesystems fsck"
sed --in-place \
--expression '/^MODULES/ s/(.*)$/(ext4)/' \
--expression '/^FILES/ s|(.*)$|('"${files}"')|' \
--expression '/^HOOKS/ s/(.*)$/('"${hooks}"')/' \
--expression '/^#COMPRESSION="zstd"/ a\
COMPRESSION="cat"' \
'/mnt/etc/mkinitcpio.conf'
arch-chroot '/mnt' \
mkinitcpio --allpresets
# Grub
declare gcl="${gcl-}debug"
declare gcl="${gcl-} rw"
declare gcl="${gcl-} rd.luks.name=$( blkid --match-tag 'UUID' \
--output 'value' \
'/dev/md/boot_mirror' )=cryptdev"
declare gcl="${gcl-} rd.luks.options=discard"
declare gcl="${gcl-} root=UUID=$( blkid --match-tag 'UUID' \
--output 'value' \
'/dev/md/root_mirror' )"
sed --in-place \
--expression '/GRUB_ENABLE_CRYPTODISK=/ s/^#//' \
--expression '/GRUB_CMDLINE_LINUX_DEFAULT=/ s|""$|"'"${gcl}"'"|' \
'/mnt/etc/default/grub'
arch-chroot '/mnt' \
grub-install --target=i386-pc '/dev/vda'
arch-chroot '/mnt' \
grub-install --target=i386-pc '/dev/vdb'
arch-chroot '/mnt' \
grub-mkconfig --output '/boot/grub/grub.cfg'
User
########################################################################
# USER #################################################################
########################################################################
# Sudo
printf '%s' '%wheel ALL=(ALL) ALL' > '/mnt/etc/sudoers.d/wheel'
# Interactive password entry
set +x
while [ ! "${password-1}" = "${password_confirm-2}" ]; do
printf '%s:' "Create a password for user ${adm_user}"
read -s password
printf '\n%s:' 'Retype the password'
read -s password_confirm
print '\n'
done
unset password_confirm
# Create admin user
arch-chroot '/mnt' \
sudo useradd --create-home \
--user-group \
--groups 'wheel' \
--shell '/usr/bin/zsh' \
"${adm_user}"
yes "${password}" |
arch-chroot '/mnt' \
passwd "${adm_user}" ||
[ "${pipestatus[2]}" = '0' ]
# Add password to luks
yes "${password}" |
arch-chroot '/mnt' \
cryptsetup luksAddKey --key-file '/etc/luks.key' \
'/dev/md/boot_mirror' ||
[ "${pipestatus[2]}" = '0' ]
yes "${password}" |
arch-chroot '/mnt' \
cryptsetup luksAddKey --key-file '/etc/luks.key' \
'/dev/md/root_mirror' ||
[ "${pipestatus[2]}" = '0' ]
yes "${password}" |
arch-chroot '/mnt' \
cryptsetup luksAddKey --key-file '/etc/luks.key' \
'/dev/vdc1' ||
[ "${pipestatus[2]}" = '0' ]
unset password
reboot now
And here’s the whole thing as a gist: