Skip to content

Instantly share code, notes, and snippets.

@cstockton
Last active January 27, 2023 08:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cstockton/0037a57aa1dab3658a3b1a2c8a3fefe7 to your computer and use it in GitHub Desktop.
Save cstockton/0037a57aa1dab3658a3b1a2c8a3fefe7 to your computer and use it in GitHub Desktop.

Arch Linux Install Guide

WARNING

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.

Notes

I install arch from a second machine via ssh usually so I can have a better terminal and copy / paste commands.

Hardware

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

Optional - Setup SSH to install from a support machine

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

Install Steps

Option 1 - Modify existing installation

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

Option 2 - Clean installation

# 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

Setup from within chroot

# 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

Enable services

systemctl enable \
    systemd-networkd \
    systemd-resolved \
    wpa_supplicant@wlp71s0.service

Networking

# 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

Custom mkinitcpio

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

Packages



# 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 yay (aur repo helper)

# 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 helper scripts (run as user)

# 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/*

Backup disk (powered by usb-key)

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

Makefile for USB-key

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment