Do not blindly paste commands from this guide, you're responsible for any destruction it causes.
This is a guide for installing a minimal arch linux from scratch.
This guide uses full disk encryption with a detached luks header so if you were to lose your laptop it's impossible for an attacker to brute force one of your luks slots. This works with any generic USB storage device.
The main threat I'm trying to mitigate is someone stealing my laptop from my backpack while I'm out and about. I can rest easy if my laptop is ever stolen knowing that it's nothing but random data on both the backup disk and the root partition.
It's important that you never keep the USB key in your laptop and always shutdown the machine when traveling, hibernation leaves your machine vulnerable. I keep a tiny little 16GB samsung USB flash drive on my key chain, which I always keep my keys on me. Leaving my bulky keys hanging out the side of my laptop is a nuisance in the off chance I forget to remove it after unlocking the device.
I install arch from a second machine via ssh usually so I can have a better terminal and copy / paste commands.
os:
/dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_XXXXXXXXXXXXXXXX
backup:
/dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_YYYYYYYYYYY
eth: enp70s0
wifi: wlp71s0
From the Live CD console set a root password get your IP address:
# WIFI
iwctl stations wlan0 connect aaa_iot_guest50
# DNS
echo 'DNS=1.1.1.1 9.9.9.10 8.8.8.8 2606:4700:4700::1111 2620:fe::10 2001:4860:4860::8888' \
>> /etc/systemd/resolved.conf
# Restart resolvd
systemctl restart systemd-resolved
# Set root passwd & start sshd
passwd
systemctl start sshd
ip addr # ip for ssh
From your support machine:
export ARCHLINUX_TARGET_IP=192.168.X.X
# Cause you'll probably mess up at least once :)
ssh-keygen -f "/home/$USER/.ssh/known_hosts" -R $ARCHLINUX_TARGET_IP
# Copy your id key
ssh-copy-id -i /home/$USER/.ssh/id_pub $USER@$ARCHLINUX_TARGET_IP
This is how you get into a shell for tweaking the install from the Live CD after installation:
cryptsetup \
open \
/dev/disk/by-partlabel/os_luks \
os_luks_crypt
until test -e /dev/os/swap; do sleep 1; done
swapon /dev/os/swap
until test -e /dev/os/root; do sleep 1; done
mount /dev/os/root /mnt
until test -e /dev/disk/by-partlabel/os_boot; do sleep 1; done
mount /dev/disk/by-partlabel/os_boot /mnt/boot
until test -e /dev/disk/by-partlabel/os_efi; do sleep 1; done
mount /dev/disk/by-partlabel/os_efi /mnt/boot/efi
arch-chroot /mnt
# Basic init
timedatectl set-ntp true
# Primary Disk - clean
wipefs --all /dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_XXXXXXXXXXXXXXXX
dd if=/dev/zero of=/dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_XXXXXXXXXXXXXXXX status=progress bs=1M count=4096 && sync
WARNING Obviously you need to make your own sfdisk template. You can get the basic template of an unpartioned disk with the command below. You only need to tune the end sector for the "os_luks" partition to match your disk size.
sfdisk -d /dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_0_1TB_XXXX_SERIAL_XXXX
# Primary Disk - partition
sfdisk /dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_XXXXXXXXXXXXXXXX <<SFDISK-CONFIG
label: gpt
unit: sectors
first-lba: 2048
last-lba: 1953525134
sector-size: 512
name=os_efi, start=2048, size=1048576, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
name=os_boot, start=1050624, size=1048576, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4
name=os_luks, start=2099200, size=1951425935, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4
SFDISK-CONFIG
# Primary Disk - Format
mkfs.vfat -F32 /dev/disk/by-partlabel/os_efi
mkfs.ext4 /dev/disk/by-partlabel/os_boot
dd if=/dev/urandom of=/dev/disk/by-partlabel/os_luks status=progress bs=1M count=1024 && sync
# Primary Disk - Setup root os container using detached header from usb-key
mkdir -p /root/usb-key
mount /dev/disk/by-partlabel/usb-key-crypt /root/usb-key
cryptsetup \
--header /root/usb-key/header.img \
open \
/dev/disk/by-partlabel/os_luks \
os_luks_crypt
umount /root/usb-key
# Wipe fs
wipefs --all /dev/mapper/os_luks_crypt
# Setup lvm
pvcreate /dev/mapper/os_luks_crypt
vgcreate os /dev/mapper/os_luks_crypt
lvcreate -L 32G os -n swap
lvcreate -l 100%FREE os -n root
mkfs.ext4 /dev/os/root
# Setup swap
mkswap /dev/os/swap
swapon /dev/os/swap
# Setup chroot
mount /dev/os/root /mnt
mkdir -p /mnt/boot
mount /dev/disk/by-partlabel/os_boot /mnt/boot
mkdir -p /mnt/boot/efi
mount /dev/disk/by-partlabel/os_efi /mnt/boot/efi
# Pacstrap
pacstrap /mnt \
base \
base-devel \
dkms \
linux-headers \
grub \
grub-efi-x86_64 efibootmgr \
lvm2 \
linux \
linux-firmware \
man-db \
man-pages \
net-tools \
vim \
wget \
curl \
git \
rsync \
wpa_supplicant \
ttf-dejavu ttf-liberation \
openssh
# Generate fstab
genfstab -U /mnt >> /mnt/etc/fstab
cat /mnt/etc/fstab
# MAKE SURE YOU REMEMBERED TO GENERATE FSTAB!!!!
### YOU ALWAYS FORGET CAUSE PACSTRAP TAKES A WHILE AND YOU WALK AWAY
arch-chroot /mnt
# Set root pssword
passwd
# Add a user
useradd -m -s /bin/bash cstockton
passwd cstockton
echo 'cstockton ALL=(ALL) ALL' > /etc/sudoers.d/00-cstockton
chmod 400 /etc/sudoers.d/00-cstockton
# Timezone
ln -sf /usr/share/zoneinfo/US/Arizona /etc/localtime
hwclock --systohc --utc
# Locale
sed -i 's/^#en_US\.UTF-8 UTF-8/en_US.UTF-8 UTF-8/g' /etc/locale.gen
echo 'LANG=en_US.UTF-8' >> /etc/locale.conf
echo 'LANGUAGE=en_US' >> /etc/locale.conf
echo 'LC_ALL=C' >> /etc/locale.conf
locale-gen
# insufferable visual mode, one of the dumbest threads I've ever seen:
# https://github.com/vim/vim/issues/2042
#
echo 'set mouse-=a' >> /etc/vimrc
echo 'let skip_defaults_vim=1' >> /etc/vimrc
ln -s /bin/vim /bin/vi
# No, i dont want shitty C services running on my machine
sed -i 's/^#LLMNR=yes/LLMNR=no/g' /etc/systemd/resolved.conf
# Because systemd-resolved listens on address 127.0.0.53
ln -rsf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl enable \
systemd-networkd \
systemd-resolved \
wpa_supplicant@wlp71s0.service
# Hostname
echo cvv > /etc/hostname
cat >/etc/hosts <<'ETCHOSTS';
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
127.0.0.1 localhost cvv example.com
ETCHOSTS
# bond dev
cat >/etc/systemd/network/20-bond0.netdev <<'MULTILINE_STRING';
[NetDev]
Name=bond0
Kind=bond
[Bond]
Mode=active-backup
PrimaryReselectPolicy=always
MIIMonitorSec=1s
MULTILINE_STRING
# bond network
cat >/etc/systemd/network/20-bond0.network <<'MULTILINE_STRING';
[Match]
Name=bond0
[Network]
DHCP=ipv4
MULTILINE_STRING
# ethernet network
cat >/etc/systemd/network/20-bond0-enp70s0.network <<'MULTILINE_STRING';
[Match]
Name=enp70s0
[Network]
Bond=bond0
PrimarySlave=true
MULTILINE_STRING
# wifi network
cat >/etc/systemd/network/20-bond0-wlp71s0.network <<'MULTILINE_STRING';
[Match]
Name=wlp71s0
[Network]
Bond=bond0
MULTILINE_STRING
# wifi config
#
## !!!!!!!!!!!!!!!!!!!!!! WARNING CONTAINS PASSWORD !!!!!!!!!!!!!!!!!!!!!!
#
cat >/etc/wpa_supplicant/wpa_supplicant-wlp71s0.conf <<'MULTILINE_STRING';
ctrl_interface=/run/wpa_supplicant
update_config=1
# AP scanning
ap_scan=1
# ISO/IEC alpha2 country code in which the device is operating
country=US
# network section generated by wpa_passphrase
network={
ssid="aaa_iot_guest50"
psk="....."
priority=1
}
MULTILINE_STRING
This is for my custom usb-key:
###############################################################
## Manual VI - so we don't break new modules in future we just
## add the two we need: lvm2 and custom-usb-key
####
# Configure mkinitcpio with modules needed for the initrd image
vi +/^HOOKS= /etc/mkinitcpio.conf
## Comment out the existing hooks
# HOOKS=(base udev autodetect modconf block filesystems keyboard fsck)
# Add: lvm2 custom-usb-key
##
# HOOKS=(base udev autodetect modconf block lvm2 custom-usb-key filesystems keyboard fsck)
## End manual VI
###############################################################
cp /usr/lib/initcpio/install/encrypt /etc/initcpio/install/custom-usb-key
cat >/etc/initcpio/hooks/custom-usb-key <<'MULTILINE_STRING';
#!/usr/bin/ash
# Expects one of two disk labels to be inserted:
#
# /dev/disk/by-label/usb-key-plain OR
# /dev/disk/by-label/usb-key-crypt
#
# Both will be mounted at /usb-key, expected to contain
# the same exact header file:
#
# /usb-key/header.img
#
# Optionally the usb-key-plain may also contain a valid key:
#
# /usb-key/header.key
#
run_hook() {
# Config
local _crypt_dev=/dev/disk/by-partlabel/os_luks
local _crypt_name=os_luks_crypt
modprobe -a -q dm-crypt >/dev/null 2>&1
modprobe loop
# Wait for a valid usb-key device
echo "custom-usb-key :: Waiting for a valid usb-key device..."
local _usb_key_dev
while ! [ -e "${_usb_key_dev}" ]; do
if [ -L '/dev/disk/by-label/usb-key-crypt' ]; then
echo "custom-usb-key :: Found /dev/disk/by-label/usb-key-crypt (password protected)..."
_usb_key_dev='/dev/disk/by-label/usb-key-crypt'
elif [ -L '/dev/disk/by-label/usb-key-plain' ]; then
echo "custom-usb-key :: Found /dev/disk/by-label/usb-key-plain..."
_usb_key_dev='/dev/disk/by-label/usb-key-plain'
fi
sleep 1
done
# Mount the usb-key which contains header and possibly a keyfile (if a usb-key-crypt)
mkdir -p /usb-key
mount "${_usb_key_dev}" /usb-key
# Try to open with key-file if it exists
local _crypt_open_state=0
if [ -f /usb-key/header.key ]; then
if eval cryptsetup --key-file /usb-key/header.key --header /usb-key/header.img open ${_crypt_dev} ${_crypt_name}; then
_crypt_open_state=1
else
echo "custom-usb-key :: Key file was not accepted, falling back to passphrase..."
fi
fi
# Fallback to passphrase
if [ ${_crypt_open_state} -eq 0 ]; then
while ! eval cryptsetup --header /usb-key/header.img open ${_crypt_dev} ${_crypt_name}; do
sleep 2;
done
fi
# Done with setup, unmount the key file
umount /usb-key
rmdir /usb-key
echo "custom-usb-key :: It is now safe to remove the USB key..."
}
MULTILINE_STRING
# Regenerate initrd image
mkinitcpio -p linux
# Grub install, no longer need to specify other args
# grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ArchLinux
grub-install
grub-mkconfig -o /boot/grub/grub.cfg
# I like to double check I have a new efi entry
efibootmgr
# Update
pacman -Syu
pacman -Fy
# Create pkgs.txt file
cat >pkgs.txt<<'MULTILINE_STRING';
# Window manager
openbox
obconf
# File manager
pcmanfm
# GPU
nvidia
nvidia-utils
# LXDE Core Apps
lxappearance
lxde-common
lxde-icon-theme
lxdm
lxhotkey
lxinput
lxlauncher
lxpanel
lxrandr
lxsession
lxterminal
# Sound
alsa-utils
pulseaudio
pavucontrol
# Media player
mpv
# Browsers
firefox
chromium
# Code editor
vscode
# Terminal
terminator
# Containers / Virtualization
docker
qemu
qemu-arch-extra
virtualbox
virtualbox-host-dkms
# Utils - Xorg
xsel
xclip
xdotool
# Utils - General
strace
iotop
lsof
inotify-tools
usbutils
inetutils
tcpdump
# Utils -Dev
pyenv
# Utils - FS
btrfs-progs
nfs-utils
hdparm
e2fsprogs
# Fido2 / HSM
libfido2
opensc
MULTILINE_STRING
# Install packages
pacman -S $(cat pkgs.txt | grep -v '^#' | sort | uniq)
# Install aur repo helper
pacman -S --needed git base-devel
git clone https://aur.archlinux.org/yay.git
cd yay
makepkg -si
# Remove the system wide Go it installs
pacman -R go
# Install AUR packages
yay -S \
keepassxc
# Create pkgs.txt file
cat >pkgs.txt<<'MULTILINE_STRING';
# Apps
keepassxc
MULTILINE_STRING
# Install aur repo helper
pacman -S --needed git base-devel
git clone https://aur.archlinux.org/yay.git
cd yay
makepkg -si
# Remove the system wide Go it installs
pacman -R go
# usb-key mount script
mkdir -p /home/cstockton/bin
cat >/home/cstockton/bin/fs-usb-key-mount<<'USB_KEY_MOUNT_SCRIPT';
#!/bin/bash
test "$#" -ne 3 || {
echo "usage: <device> <mountpoint>";
exit 1;
}
test -f /root/usb-key/header.img || \
mount -o ro /dev/disk/by-partlabel/usb-key-crypt /root/usb-key
_name="$(basename $1)"
_crypt="${_name}_crypt"
cryptsetup \
--header /root/usb-key/header.img \
open \
"$1" "$_crypt"
mkdir -p "$2"
mount "/dev/mapper/$_crypt" "$2"
test -f /root/usb-key/header.img && \
umount /root/usb-key
USB_KEY_MOUNT_SCRIPT
# usb-key umount script
cat >/home/cstockton/bin/fs-usb-key-umount<<'USB_KEY_MOUNT_SCRIPT';
#!/bin/bash
test "$#" -ne 3 || {
echo "usage: <device> <mountpoint>";
exit 1;
}
_name="$(basename $1)"
_crypt="${_name}_crypt"
umount "$2"
cryptsetup luksClose "$_crypt"
test -f /root/usb-key/header.img && \
umount /root/usb-key
USB_KEY_MOUNT_SCRIPT
# Backup mount script
cat >/home/cstockton/bin/fs-backup-mount<<'USB_KEY_MOUNT_SCRIPT';
#!/bin/bash
/home/cstockton/bin/fs-usb-key-mount \
/dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_XXXXX \
/backup
USB_KEY_MOUNT_SCRIPT
# Backup umount script
cat >/home/cstockton/bin/fs-backup-umount<<'USB_KEY_MOUNT_SCRIPT';
#!/bin/bash
/home/cstockton/bin/fs-usb-key-umount \
/dev/disk/by-id/nvme-Sabrent_Rocket_4.0_1TB_XXXXX \
/backup
USB_KEY_MOUNT_SCRIPT
# Backup script
cat >/home/cstockton/bin/fs-backup-sync<<'USB_KEY_MOUNT_SCRIPT';
#!/bin/bash
rsync --delete -aAXHv --exclude={"/cws/***","/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/mnt/*","/media/*","/lost+found"} / /backup/live/
USB_KEY_MOUNT_SCRIPT
# misc useful scripts
cat >/home/cstockton/bin/clip-echo<<'BIN_SCRIPT';
#!/bin/bash
_data="$(xsel -b -o "$@")"
test ! -z "${_data}" || {
printf >&2 "[error] %s: clipboard is empty\n" "$0"
exit 1
}
echo "${_data}"
BIN_SCRIPT
cat >/home/cstockton/bin/clip-echo-wait<<'BIN_SCRIPT';
#!/bin/bash
while true; do
_data="$(xsel -c -b -o)"
test ! -z "${_data}" || {
sleep 0.5
continue
}
printf >&2 "[info] %s: clipboard value '%s'\n" "$0" "$_data"
echo "${_data}"
break;
done
BIN_SCRIPT
chmod 700 /home/cstockton/bin/*
Provisioning (WIPES EXISTING DATA)
# Insert usb-key
mkdir -p /root/usb-key
mount /dev/disk/by-partlabel/usb-key-crypt /root/usb-key
cryptsetup \
--header /root/usb-key/header.img \
open \
/dev/mapper/nvme-Sabrent_Rocket_4.0_1TB_YYYYYYYYYYY \
os_backup_crypt
umount /root/usb-key
# Format
mkfs.ext4 /dev/mapper/os_backup_crypt -L os_backup
Mounting
# Insert usb-key
mkdir -p /root/usb-key
mount /dev/disk/by-partlabel/usb-key-crypt /root/usb-key
cryptsetup \
--header /root/usb-key/header.img \
open \
/dev/mapper/nvme-Sabrent_Rocket_4.0_1TB_YYYYYYYYYYY \
os_backup_crypt
umount /root/usb-key
# Format
mkfs.ext4 /dev/mapper/os_backup_crypt -L os_backup
targets := \
header.img \
usb-key-plain.img \
usb-key-crypt.img
wait_for_file = \
until test -e '$(1)'; do \
echo 'waiting for file "$(1)"...' && sleep 1; \
done
.PHONY: all
all: $(targets)
dummy.img:
dd if=/dev/zero of=$(@) bs=1M count=1
header.key:
head -c 4096 /dev/urandom > $(@)
header.img: header.key | dummy.img
dd if=/dev/zero of=$(@) bs=16M count=1
sudo cryptsetup \
--header $(@) \
--key-file $(^) \
luksFormat $(|)
@echo '================================='
@echo '== This is the password for the "crypt" usb-key'
sudo cryptsetup \
--key-file $(^) \
--iter-time=10000 \
luksAddKey $(@)
cryptsetup luksDump $(@)
.PRECIOUS: loop-%.img
usb-key-%.img:
make fs-$(*) fs-format-$(*) fs-deinit-$(*)
mv loop-$(*).img $(@)
sha256sum $(@)
fs-%: \
/dev/disk/by-label/usb-key-% | loop-%.dev
mkdir -p fs-$(*)
sudo mount '$(^)' fs-$(*)
.PHONY: fs-format-crypt
fs-format-crypt: | fs-crypt/header.img
sha256sum header.img $(|)
.PHONY: fs-format-plain
fs-format-plain: | fs-plain/header.img fs-plain/header.key
sha256sum header.img header.key $(|)
fs-deinit-%: \
| /dev/disk/by-label/usb-key-% loop-%.dev
find fs-$(*) | sort
sudo umount fs-$(*)
rmdir fs-$(*)
sudo losetup -d '$(realpath loop-$(*).dev)'
fs-%/header.img: header.img | fs-%
sudo cp $(^) $(@)
fs-%/header.key: header.key | fs-%
sudo cp $(^) $(@)
/dev/disk/by-label/usb-key-%: \
/dev/disk/by-partlabel/usb-key-% | loop-%.dev
sudo mkfs.ext4 '$(^)' -L usb-key-$(*)
$(call wait_for_file,$(@))
/dev/disk/by-partlabel/usb-key-%: \
conf/sfdisk-%.conf | loop-%.dev
cat '$(^)' | sudo sfdisk '$(realpath $(|))'
$(call wait_for_file,$(@))
loop-%.dev: loop-%.img
ln -s $$(sudo losetup --show -fP $(^)) $(@)
loop-%.img:
dd if=/dev/urandom of=$(@) bs=1M count=64 status=progress
.PHONY: clean
clean:
rm -f dummy.img
.PHONY: clean-all
clean-all: clean
rm -f $(targets) header.key
sudo umount fs-plain || true
sudo umount fs-crypt || true
rmdir fs-plain fs-crypt || true
rm -f loop-plain.img loop-crypt.img
sudo losetup -d \
$$(losetup -ln -O NAME,BACK-FILE \
| grep '$(shell pwd)/loop' \
| awk '{print $$1}') >/dev/null 2>&1 \
|| true
.PHONY: list
list:
@lsblk -do name,tran,size,type,mountpoint | grep ' usb ' || \
for x in /sys/block/*; do \
v=$$(udevadm info -q property $$x); \
echo '$$v' | grep -q '^ID_BUS=usb' || continue; \
echo '$$v' | grep '^DEVLINKS=' | sed 's|DEVLINKS=||g' \
| xargs printf '%s\n' | grep '^/dev/disk/by-id/'; \
done