Arch Linux Fresh Install: Bare Metal to Productive Desktop

Complete runbook for installing Arch Linux on a new machine — from BIOS settings to a fully productive Hyprland desktop with all tools, repos, and secrets. Each phase is self-contained and can be re-run independently.

1. Phase 0: Pre-Installation

1.1. Find Latest Arch ISO

curl -s https://archlinux.org/download/ | grep -oP 'archlinux-\d{4}\.\d{2}\.\d{2}-x86_64\.iso' | head -1

1.2. Download ISO + Signature

curl -LO https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso
curl -LO https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso.sig

1.3. Verify Signature

gpg --keyserver-options auto-key-retrieve --verify archlinux-x86_64.iso.sig archlinux-x86_64.iso

1.4. Identify USB Device

# Plug in USB — identify the DEVICE (sda), NOT a partition (sda1)
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT

1.5. Write USB

Ventoy is AUR — not in main repos:

# Install from AUR
yay -S ventoy
# Or manually
git clone https://aur.archlinux.org/ventoy.git /tmp/ventoy
cd /tmp/ventoy && makepkg -si
# Write Ventoy bootloader to USB (WIPES the drive)
sudo ventoy -i /dev/sdX
# Copy ISO to Ventoy data partition
sudo mount -o uid=$(id -u),gid=$(id -g) /dev/sdX1 /mnt
cp archlinux-x86_64.iso /mnt/
sudo umount /mnt

1.5.2. Option B: Direct dd (single ISO, destroys USB)

# TRIPLE CHECK lsblk — wrong device = wiped disk
lsblk
sudo dd bs=4M if=archlinux-x86_64.iso of=/dev/sdX status=progress oflag=sync

1.6. BIOS/UEFI Settings

Do this BEFORE booting the USB. Every vendor hides these differently.

Setting Value

Secure Boot

Disabled (re-enable after install with signed bootloader if desired)

Boot Mode

UEFI only (disable Legacy/CSM)

Boot Order

USB first, then NVMe

Intel VMX / AMD-V

Enabled (for KVM/libvirt later)

TPM

Enabled (optional, for measured boot)

1.7. Boot the USB

  1. Power off, insert USB

  2. Power on, press boot menu key (F12 / F2 / Del — vendor-specific)

  3. Select USB UEFI entry

  4. At Arch menu, select Arch Linux install medium

1.8. Verify UEFI Mode

# This directory must exist — if not, you're in Legacy mode
ls /sys/firmware/efi/efivars

1.9. Set Console Font (HiDPI laptops)

# Tiny text on 4K screen? Fix it
setfont ter-132b

1.10. Connect to Network

1.10.1. Wired (usually automatic)

ip link
ping -c 3 archlinux.org

1.10.2. WiFi with iwctl (iPSK networks)

If the WiFi network uses iPSK authentication (e.g., DOMUS-IoT), the device MAC must be registered in the iPSK identity group BEFORE connecting. ISE validates the MAC via ODBC against the iPSK Manager.

  1. Get the wireless MAC from the live environment:

    ip -o link show | awk '/ether/ {print $2, $(NF-2)}'
  2. Add the MAC to the iPSK Manager (from another device):

    • Open iPSK Manager web UI

    • Add MAC to the appropriate identity group (e.g., DOMUS-IoT)

    • ISE picks it up automatically via secure ODBC

iwctl
# Inside iwctl shell:
device list
station wlan0 scan
station wlan0 get-networks
station wlan0 connect "YourSSID"
# Enter iPSK password when prompted
exit
ping -c 3 archlinux.org

1.11. Enable SSH (Remote Install from Razer)

This lets you SSH in from modestus-razer and run the entire install remotely — copy/paste from the rendered runbook, no squinting at unscaled HiDPI.

These 3 commands are typed on the T16g console. Everything after this is from the Razer.

# Set root password for the live environment
passwd
# Start SSH daemon
systemctl start sshd
# Get the T16g's IP address
ip -4 -o addr show | awk '$2!="lo" {print $2, $4}'

1.11.1. From the Razer

# SSH into the T16g live environment (replace IP)
ssh root@<T16G-IP>
Example
ssh root@10.50.10.42

After reboot (end of Phase 3), the live SSH session dies. The installed system will get a new IP via DHCP. SSH is enabled in Phase 4 (systemctl enable --now sshd) — reconnect after first boot using the new IP:

# Find T16g on the network after reboot
# (from Razer — adjust subnet)
nmap -sn 10.50.10.0/24 | grep -B2 "Lenovo\|ThinkPad"

Or check your DHCP server / router for the new lease.

1.12. Sync Clock

timedatectl set-ntp true
timedatectl status

1.13. Update Mirrors

reflector --country US --age 12 --protocol https --sort rate --save /etc/pacman.d/mirrorlist

2. Phase 1: Disk Setup

2.1. Identify Target Disk

lsblk
fdisk -l

NVMe drives: /dev/nvme0n1 (partitions: p1, p2, …​)
SATA drives: /dev/sda (partitions: 1, 2, …​)

2.2. Partition Layout (UEFI + Dual LUKS + Btrfs)

Production layout — four partitions with separate LUKS volumes for root and home:

Partition Size Type Mount

/dev/nvme0n1p1

512M

EFI System (ef00)

/boot/efi (vfat) — systemd-boot reads bootloader, entries, AND kernels from here

/dev/nvme0n1p2

2G

Linux filesystem (8300)

/boot (ext4) — OS kernel staging. pacman installs here; hook copies to ESP

/dev/nvme0n1p3

250G

Linux LUKS (8309)

LUKS → cryptroot → btrfs → /

/dev/nvme0n1p4

Remaining

Linux LUKS (8309)

LUKS → crypthome → btrfs → /home

Btrfs subvolumes per LUKS volume:

Volume Subvolumes

cryptroot

@ (root), @snapshots, @var_log

crypthome

@home

Why this layout (not single-LUKS):

  • Separate /boot (ext4) outside LUKS — holds multiple kernels, robust recovery

  • /boot/efi (vfat) holds only the EFI bootloader binary

  • Separate LUKS volumes — wipe/reinstall root without touching /home

  • 250G root cap prevents system bloat

  • @var_log isolated — log explosion won’t fill root

Dual-NVMe machines (e.g., modestus-razer): use the second NVMe as a third LUKS volume (cryptdata) for /data and /var/lib/libvirt/images with subvolumes @data and @vms.

2.3. Wipe and Partition

This destroys ALL data on the target disk. Triple-check lsblk output.

# Wipe partition table and create GPT
sgdisk -Z /dev/nvme0n1
# Create EFI partition (512M)
sgdisk -n 1:0:+512M -t 1:ef00 /dev/nvme0n1
# Create /boot partition (2G — holds main + LTS + fallback initramfs)
sgdisk -n 2:0:+2G -t 2:8300 /dev/nvme0n1
# Create root partition (250G)
sgdisk -n 3:0:+250G -t 3:8309 /dev/nvme0n1
# Create home partition (remaining space)
sgdisk -n 4:0:0 -t 4:8309 /dev/nvme0n1
# Verify — should show 4 partitions
sgdisk -p /dev/nvme0n1

2.4. LUKS Encryption

2.4.1. Root Volume

cryptsetup luksFormat /dev/nvme0n1p3
cryptsetup open /dev/nvme0n1p3 cryptroot

2.4.2. Home Volume

# Use a DIFFERENT passphrase than root, or the same — your call
cryptsetup luksFormat /dev/nvme0n1p4
cryptsetup open /dev/nvme0n1p4 crypthome
# Verify both LUKS containers are open
ls /dev/mapper/crypt*

2.5. Format Filesystems

# EFI — FAT32
mkfs.fat -F32 /dev/nvme0n1p1
# /boot — ext4
mkfs.ext4 /dev/nvme0n1p2
# Root — Btrfs on LUKS (label for fstab readability)
mkfs.btrfs -L archroot /dev/mapper/cryptroot
# Home — Btrfs on LUKS
mkfs.btrfs -L archhome /dev/mapper/crypthome

2.6. Create Btrfs Subvolumes

2.6.1. Root subvolumes (cryptroot)

mount /dev/mapper/cryptroot /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@snapshots
btrfs subvolume create /mnt/@var_log
# Verify — 3 subvolumes
btrfs subvolume list /mnt
umount /mnt

2.6.2. Home subvolumes (crypthome)

mount /dev/mapper/crypthome /mnt
btrfs subvolume create /mnt/@home
# Verify — 1 subvolume
btrfs subvolume list /mnt
umount /mnt

2.7. Mount Everything

Mount order matters — root first, then create mount points, then the rest.

# Mount root subvolume with production options
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=@ /dev/mapper/cryptroot /mnt
# Create mount points
mkdir -p /mnt/{boot/efi,home,.snapshots,var/log}
# Mount remaining root subvolumes
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@snapshots /dev/mapper/cryptroot /mnt/.snapshots
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@var_log /dev/mapper/cryptroot /mnt/var/log
# Mount home from separate LUKS volume
mount -o noatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@home /dev/mapper/crypthome /mnt/home
# Mount /boot (ext4)
mount /dev/nvme0n1p2 /mnt/boot
# Mount EFI
mount /dev/nvme0n1p1 /mnt/boot/efi
# Final verification — all 6 mount points visible
lsblk /dev/nvme0n1
# Detailed mount verification
findmnt -t btrfs,vfat,ext4 --output TARGET,SOURCE,FSTYPE,OPTIONS -n

Mount options explained:

  • noatime — don’t update access timestamps (performance)

  • compress=zstd:3 — zstd compression level 3 (balanced speed/ratio)

  • ssd — SSD-aware allocation (NVMe/SSD only)

  • discard=async — batched TRIM (better than discard sync)

  • space_cache=v2 — improved free space tracking

3. Phase 2: Base System

3.1. Install Base Packages

pacstrap -K /mnt \
    base \
    linux \
    linux-headers \
    linux-lts \
    linux-lts-headers \
    linux-firmware \
    base-devel \
    btrfs-progs \
    git \
    neovim \
    zsh \
    sudo \
    networkmanager \
    openssh \
    intel-ucode \
    iwd \
    wireless_tools \
    zram-generator
  • Use amd-ucode instead of intel-ucode for AMD CPUs.

  • linux-lts provides a fallback kernel — critical when bleeding-edge kernel breaks NVIDIA or WiFi.

  • zsh is included here because the user account (created below) uses /bin/zsh as default shell.

  • openssh is included here so sshd is available on first boot (Phase 4 remote install).

  • zram-generator provides compressed swap in RAM (no swap partition needed).

3.2. Generate fstab

genfstab -U /mnt >> /mnt/etc/fstab
# Verify — should show entries for:
#   /         (cryptroot, subvol=/@)
#   /.snapshots (cryptroot, subvol=/@snapshots)
#   /var/log  (cryptroot, subvol=/@var_log)
#   /home     (crypthome, subvol=/@home)
#   /boot     (ext4)
#   /boot/efi (vfat)
cat /mnt/etc/fstab

Verify every btrfs mount has the full options: noatime,compress=zstd:3,ssd,discard=async,space_cache=v2. If genfstab missed any, edit /mnt/etc/fstab before proceeding.

3.3. Enter Chroot

arch-chroot /mnt

3.4. Configure crypttab (Dual LUKS)

The kernel cmdline handles cryptroot at boot. Additional LUKS volumes need /etc/crypttab so systemd opens them during init.

# Get the UUID of the home LUKS partition (nvme0n1p4, NOT the mapper)
blkid -s UUID -o value /dev/nvme0n1p4
# Create crypttab — replace <HOME-LUKS-UUID> with the UUID above
cat > /etc/crypttab << 'EOF'
# <name>    <device>                          <password>  <options>
crypthome   UUID=<HOME-LUKS-UUID>             none        luks,timeout=90
EOF

none means interactive passphrase prompt at boot. You will get TWO prompts: one for cryptroot (from kernel cmdline), one for crypthome (from crypttab).

Optional — auto-unlock with keyfile (eliminates second prompt):

# Generate a keyfile on the encrypted root
mkdir -p /etc/luks-keys
dd if=/dev/urandom of=/etc/luks-keys/crypthome.key bs=4096 count=1
chmod 600 /etc/luks-keys/crypthome.key

# Add the keyfile to the home LUKS volume
cryptsetup luksAddKey /dev/nvme0n1p4 /etc/luks-keys/crypthome.key

# Update crypttab to use it
# crypthome   UUID=<HOME-LUKS-UUID>   /etc/luks-keys/crypthome.key   luks

The keyfile lives on encrypted root — it’s only accessible after cryptroot is unlocked.

3.5. Time Zone

ln -sf /usr/share/zoneinfo/America/Los_Angeles /etc/localtime
hwclock --systohc

3.6. Locale

sed -i 's/^#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
locale-gen
echo "LANG=en_US.UTF-8" > /etc/locale.conf

3.7. Hostname

# Replace with your hostname (e.g., modestus-t16g, modestus-legion)
echo "modestus-t16g" > /etc/hostname
cat > /etc/hosts << 'EOF'
127.0.0.1   localhost
::1         localhost
127.0.1.1   modestus-t16g.inside.domusdigitalis.dev modestus-t16g
EOF

3.8. Configure zram Swap

cat > /etc/systemd/zram-generator.conf << 'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
EOF

This creates compressed swap in RAM at half your physical memory (32G for 64GB RAM). No swap partition needed. zstd compression typically achieves 2-3x ratio, giving effectively 64-96GB of usable swap space.

The Razer runs 31.1G zram — verify after first boot with zramctl.

3.10. Create User

useradd -m -G wheel -s /bin/zsh evanusmodestus
passwd evanusmodestus
# Enable sudo for wheel group
EDITOR=nvim visudo
# Uncomment: %wheel ALL=(ALL:ALL) ALL

3.11. Enable Services

systemctl enable NetworkManager
systemctl enable systemd-timesyncd

4. Phase 3: Bootloader

4.1. mkinitcpio Configuration

nvim /etc/mkinitcpio.conf

Set the following:

MODULES=()

HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block encrypt btrfs filesystems fsck)

Key choices (matching modestus-razer production):

  • MODULES=() — empty. Modern nvidia (>=560) and nvidia-open work with KMS. No need to force-load nvidia modules in initramfs.

  • kms — early KMS for smooth boot. Previously removed for proprietary nvidia — no longer necessary with current drivers.

  • encrypt — LUKS support. Must come BEFORE btrfs and filesystems.

  • btrfs — btrfs module loaded early. Required for multi-device btrfs, good practice for single-device.

  • No microcode hook — microcode is loaded via the initrd line in the boot entry instead.

mkinitcpio -P

This generates four images (main + fallback for both kernels):

  • /boot/initramfs-linux.img

  • /boot/initramfs-linux-fallback.img

  • /boot/initramfs-linux-lts.img

  • /boot/initramfs-linux-lts-fallback.img

4.2. Install systemd-boot

bootctl --esp-path=/boot/efi --boot-path=/boot install

--esp-path=/boot/efi tells systemd-boot where the EFI System Partition is.
--boot-path=/boot tells it where kernel images live (the ext4 partition).
Boot entries go to /boot/efi/loader/entries/, kernels stay on /boot/.

4.3. Loader Configuration

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

4.4. Get UUIDs

# LUKS partition UUID for cryptdevice= (the RAW partition, not the mapper)
blkid -s UUID -o value /dev/nvme0n1p3

Record this UUID — it goes in every boot entry’s options line.

4.5. Create Boot Entries

4.5.1. Main Entry

cat > /boot/efi/loader/entries/arch.conf << 'EOF'
title   Arch Linux
linux   /vmlinuz-linux
initrd  /intel-ucode.img
initrd  /initramfs-linux.img
options cryptdevice=UUID=<LUKS-UUID>:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ rw nvidia_drm.modeset=1 mem_sleep_default=s2idle
EOF

4.5.2. Fallback Entry

cat > /boot/efi/loader/entries/arch-fallback.conf << 'EOF'
title   Arch Linux (fallback)
linux   /vmlinuz-linux
initrd  /intel-ucode.img
initrd  /initramfs-linux-fallback.img
options cryptdevice=UUID=<LUKS-UUID>:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ rw nvidia_drm.modeset=1 mem_sleep_default=s2idle
EOF

4.5.3. LTS Entry

cat > /boot/efi/loader/entries/arch-lts.conf << 'EOF'
title   Arch Linux LTS
linux   /vmlinuz-linux-lts
initrd  /intel-ucode.img
initrd  /initramfs-linux-lts.img
options cryptdevice=UUID=<LUKS-UUID>:cryptroot root=/dev/mapper/cryptroot rootflags=subvol=@ rw nvidia_drm.modeset=1 mem_sleep_default=s2idle
EOF

Replace <LUKS-UUID> in ALL THREE entries with the UUID from blkid /dev/nvme0n1p3.
Use amd-ucode.img for AMD CPUs.

Kernel parameters explained:

  • cryptdevice=UUID=…​:cryptroot — unlock root LUKS at boot

  • rootflags=subvol=@ — mount the @ btrfs subvolume as root

  • nvidia_drm.modeset=1 — enable NVIDIA DRM kernel modesetting (required for Wayland/Hyprland)

  • mem_sleep_default=s2idle — use s2idle (modern standby) instead of deep sleep. Prevents resume issues on Intel HX + NVIDIA laptops.

4.6. Verify Boot Entries

bootctl list
# Should show 3 entries: Arch Linux, Arch Linux (fallback), Arch Linux LTS
ls /boot/efi/loader/entries/

4.7. Exit Chroot and Reboot

exit
umount -R /mnt
reboot

Remove the USB when the system powers off.

At boot you will see TWO passphrase prompts (unless you configured a keyfile):

  1. cryptroot — from the kernel cmdline cryptdevice= parameter

  2. crypthome — from /etc/crypttab

This is expected behavior for dual-LUKS.

5. Phase 4: First Boot

5.1. Login & Re-establish SSH

Log in as evanusmodestus on the T16g console (not root). You’ll enter the LUKS passphrase(s) during boot, then get a TTY login.

On the T16g console (last commands before going back to Razer):

# Connect to network
ip addr

If wired, DHCP should auto-assign. If WiFi:

nmcli device wifi connect "YourSSID" password "YourPassword"
ping -c 3 archlinux.org
# Enable and start SSH immediately
sudo systemctl enable --now sshd
# Get the new IP (different from live ISO)
ip -4 -o addr show | awk '$2!="lo" {print $2, $4}'

Back on the Razer — reconnect:

ssh evanusmodestus@<NEW-T16G-IP>

From this point forward, everything runs via SSH from the Razer. The T16g console is only needed again if SSH breaks or for LUKS passphrase entry at reboot.

5.2. Full System Update

sudo pacman -Syu

5.3. Install AUR Helper (yay)

git clone https://aur.archlinux.org/yay.git /tmp/yay
cd /tmp/yay && makepkg -si
cd ~

5.4. Enable Multilib (32-bit support)

# Uncomment [multilib] section
sudo nvim /etc/pacman.conf
sudo pacman -Syu

5.5. Install Essential System Packages

sudo pacman -S \
    stow \
    age \
    man-db \
    man-pages \
    htop \
    btop \
    tree \
    wget \
    curl \
    unzip \
    zip \
    p7zip

zsh and openssh are already installed from pacstrap (Phase 2). pacman will skip them if present.

6. Phase 5: Desktop Environment

6.1. GPU Driver

6.1.1. NVIDIA (Lenovo Legion / discrete GPU)

sudo pacman -S nvidia nvidia-utils nvidia-settings lib32-nvidia-utils
# Verify
nvidia-smi

6.1.2. Intel (integrated only)

sudo pacman -S mesa intel-media-driver vulkan-intel

6.2. Hyprland Stack

sudo pacman -S \
    hyprland \
    xdg-desktop-portal-hyprland \
    waybar \
    wofi \
    mako \
    hyprlock \
    hypridle \
    hyprpaper \
    grim \
    slurp \
    wl-clipboard \
    cliphist \
    swappy

6.3. Terminals

sudo pacman -S kitty
yay -S ghostty

6.4. Fonts

sudo pacman -S \
    ttf-jetbrains-mono-nerd \
    ttf-nerd-fonts-symbols \
    ttf-font-awesome \
    noto-fonts \
    noto-fonts-emoji \
    noto-fonts-cjk

6.5. Audio

sudo pacman -S \
    pipewire \
    pipewire-alsa \
    pipewire-pulse \
    pipewire-jack \
    wireplumber \
    pavucontrol
systemctl --user enable --now pipewire pipewire-pulse wireplumber

6.6. Bluetooth

sudo pacman -S bluez bluez-utils blueman
sudo systemctl enable --now bluetooth

6.7. Display Manager (optional)

# SDDM for graphical login, or just use TTY login + Hyprland auto-start
sudo pacman -S sddm
sudo systemctl enable sddm

6.8. File Manager & Utilities

sudo pacman -S \
    thunar \
    thunar-volman \
    gvfs \
    tumbler \
    ffmpegthumbnailer \
    zathura \
    zathura-pdf-mupdf \
    imv \
    polkit-gnome

6.9. Screenshot & Screen Recording

# Already installed grim + slurp above
# For screen recording:
sudo pacman -S wf-recorder

6.10. Theme & Appearance

sudo pacman -S \
    qt5-wayland \
    qt6-wayland \
    gtk3 \
    gtk4 \
    nwg-look
yay -S catppuccin-gtk-theme-mocha catppuccin-cursors-mocha

7. Phase 6: Dotfiles & Stow

7.1. Create Directory Structure

mkdir -p ~/atelier/{_bibliotheca,_projects/personal}

7.2. Clone dots-quantum

git clone git@github.com:EvanusModestus/dots-quantum.git \
    ~/atelier/_projects/personal/dots-quantum

7.3. Install CLI Tools (stow dependencies)

sudo pacman -S \
    fzf \
    fd \
    ripgrep \
    bat \
    eza \
    jq \
    yq \
    lazygit \
    tmux
yay -S oh-my-posh-bin

7.4. Stow Essential Packages (Tier 1)

cd ~/atelier/_projects/personal/dots-quantum
stow -t ~ \
    zsh bash shell git \
    hyprland waybar wofi mako \
    kitty oh-my-posh \
    bin share \
    btop fastfetch fzf fd ripgrep \
    claude

7.5. Verify Stow

# Check symlinks were created
ls -la ~/.zshrc
ls -la ~/.config/hypr/hyprland.conf
ls -la ~/.config/waybar/config.jsonc

7.6. Clone and Setup Neovim

git clone git@github.com:EvanusModestus/domus-nvim.git \
    ~/atelier/_projects/personal/domus-nvim
# Symlink nvim config
ln -sf ~/atelier/_projects/personal/domus-nvim ~/.config/nvim
# Launch nvim — plugins will auto-install on first run
nvim

7.7. Stow Tier 2 Packages

cd ~/atelier/_projects/personal/dots-quantum
stow -t ~ \
    tmux vim lazygit \
    vscodium zathura thunar \
    libvirt ghostty

7.8. Machine-Specific Host Config

# Check if host-specific directory exists
ls ~/atelier/_projects/personal/dots-quantum/hosts/
# Create host config for new machine if needed
# (monitor layout, GPU settings, etc.)
mkdir -p ~/atelier/_projects/personal/dots-quantum/hosts/modestus-legion

8. Phase 7: Secrets & Credentials

This phase requires access to your existing secrets infrastructure. Have ready:

  • age identity file (from secure backup or another machine)

  • YubiKey with GPG subkeys (signing, encryption, authentication)

  • SSH access to Gitea/GitHub/GitLab

  • VPN or network access to Vault cluster (for SSH certs)

  • SSH access to Synology NAS (for Borg backups)

8.1. age Setup

mkdir -p ~/.age/{recipients,identities}
chmod 700 ~/.age/identities
# Copy your age identity from secure backup
# (USB drive, password manager, or SCP from existing machine)
# DO NOT paste identity content here — execute manually
# Verify age can decrypt
age -d -i ~/.age/identities < /dev/null 2>&1 | head -1
# Should show a decryption error, not "no identity file"

8.2. SSH Keys

mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Option A: Restore from backup (SCP from existing machine)
# scp modestus-razer:~/.ssh/id_ed25519* ~/.ssh/

# Option B: Generate new keypair
ssh-keygen -t ed25519 -C "evanusmodestus@modestus-t16g"
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

8.3. Decrypt SSH Config from dots-quantum

age -d -i ~/.age/identities \
    ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config.age \
    > ~/atelier/_projects/personal/dots-quantum/ssh/.ssh/config
cd ~/atelier/_projects/personal/dots-quantum
stow -t ~ ssh
# Verify SSH config is symlinked
ls -la ~/.ssh/config

8.4. YubiKey & GPG

8.4.1. Install packages

sudo pacman -S gnupg pcsc-tools ccid yubikey-manager
sudo systemctl enable --now pcscd

8.4.2. Detect YubiKey

# Insert YubiKey, then:
gpg --card-status
# Should show:
#   Reader ...........: Yubico YubiKey ...
#   Application Type .: OpenPGP
#   Signature key ....: [your key fingerprint]
#   Encryption key ...: [your key fingerprint]
#   Authentication key: [your key fingerprint]

8.4.3. Import GPG public key

# The YubiKey holds private subkeys. You still need the public key.
# Option A: Fetch from keyserver
gpg --keyserver keys.openpgp.org --recv-keys <YOUR-KEY-ID>

# Option B: Import from file
gpg --import /path/to/public-key.gpg

# Option C: Export from existing machine
# (on razer): gpg --export --armor <KEY-ID> > /tmp/pub.gpg
# (on t16g):  gpg --import /tmp/pub.gpg
# Trust the key (ultimate trust for your own key)
gpg --edit-key <KEY-ID>
# At gpg> prompt: trust → 5 (ultimate) → quit

8.4.4. Verify GPG + YubiKey

# List secret keys — should show "ssb>" (stub pointing to card)
gpg -K
# Test decryption (touch YubiKey when prompted)
echo "test" | gpg --encrypt --recipient <KEY-ID> | gpg --decrypt

8.4.5. GPG Agent Troubleshooting

# If YubiKey not detected after replug
gpgconf --kill gpg-agent
gpg --card-status
# Force agent reload
gpg-connect-agent reloadagent /bye
# Check udev rules exist
ls /etc/udev/rules.d/*yubikey* 2>/dev/null || ls /usr/lib/udev/rules.d/*yubi* 2>/dev/null

8.5. gopass Setup & Sync

8.5.1. Clone store

sudo pacman -S gopass
# Clone from Gitea (internal) or GitHub
gopass clone ssh://git@gitea-01.inside.domusdigitalis.dev:2222/evanusmodestus/password-store v3
# Verify — touch YubiKey to decrypt
gopass ls

8.5.2. Sync and verify remotes

# Sync pulls and pushes all mounted stores
gopass sync
# Verify gopass git remotes are configured
gopass git remote -v --store v3
# Verify recipients (your GPG key should be listed)
gopass recipients --store v3
# Test a known entry decrypts correctly
gopass show v3/test/entry 2>/dev/null || echo "Create a test entry to verify"

8.5.3. gopass configuration

# Recommended settings
gopass config core.cliptimeout 45
gopass config core.autosync true
gopass config core.autoclip false
gopass config core.showsafecontent true

8.7. Verify Git Remote Access

ssh -T git@github.com
ssh -T git@gitlab.com
ssh git@gitea-01.inside.domusdigitalis.dev

8.8. Vault SSH Certificates

Certificate-based SSH eliminates authorized_keys management. Vault signs your public key with an 8-hour TTL — hosts trust the CA, not individual keys.

8.8.1. Prerequisites

# Install Vault CLI
sudo pacman -S vault
# Set Vault address (from gopass or environment)
export VAULT_ADDR="https://vault-01.inside.domusdigitalis.dev:8200"

8.8.2. Authenticate to Vault

# Interactive login (uses LDAP, userpass, or token — depends on your auth method)
vault login
# Verify access
vault token lookup

8.8.3. Generate a dedicated keypair for Vault signing

# Separate key for Vault-signed certs (don't mix with static SSH keys)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_vault -C "vault-signed@modestus-t16g"

8.8.4. Sign the key

vault write -field=signed_key ssh/sign/domus-client \
    public_key=@~/.ssh/id_ed25519_vault.pub \
    valid_principals="evanusmodestus,admin,root" \
    > ~/.ssh/id_ed25519_vault-cert.pub
# Inspect the certificate
ssh-keygen -L -f ~/.ssh/id_ed25519_vault-cert.pub
# Should show:
#   Type: ssh-ed25519-cert-v01@openssh.com user certificate
#   Valid: from <now> to <now + 8h>
#   Principals:
#         evanusmodestus
#         admin
#         root

8.8.5. Test certificate-based login

# SSH to a host that trusts the Vault CA
ssh -i ~/.ssh/id_ed25519_vault evanusmodestus@<target-host>

Certificates expire after 8 hours (TTL configured on the Vault role). Re-sign daily:

vault write -field=signed_key ssh/sign/domus-client \
    public_key=@~/.ssh/id_ed25519_vault.pub \
    valid_principals="evanusmodestus,admin,root" \
    > ~/.ssh/id_ed25519_vault-cert.pub

Consider a shell alias or script for this. The key pair is static — only the cert rotates.

8.9. Borg Backup Setup

8.9.1. Install

sudo pacman -S borg nfs-utils

8.9.2. Mount Synology NAS

Borg uses a local NFS mount to the Synology, not SSH transport.

sudo mkdir -p /mnt/synology
sudo mount -t nfs nas-01.inside.domusdigitalis.dev:/volume1/borg_backups /mnt/synology

8.9.3. Load Credentials

# Load BORG_PASSPHRASE from dsource
dsource d000 dev/storage

8.9.4. Verify existing repo

sudo -E BORG_PASSPHRASE="$BORG_PASSPHRASE" borg info /mnt/synology/borg-repo
sudo -E BORG_PASSPHRASE="$BORG_PASSPHRASE" borg list /mnt/synology/borg-repo --last 3

8.9.5. Initialize repo for new machine (if needed)

sudo -E BORG_PASSPHRASE="$BORG_PASSPHRASE" borg init --encryption=repokey /mnt/synology/borg-repo-<HOSTNAME>

8.9.6. Validate backup script

The backup script is stowed from dots-quantum:

ls -la ~/.local/bin/borg-backup-synology.sh
# Check for hardcoded paths that may reference old machine
grep -i "optimus\|dotfiles-optimus" ~/.local/bin/borg-backup-synology.sh

8.9.7. Run first backup

sudo -E BORG_PASSPHRASE="$BORG_PASSPHRASE" ~/.local/bin/borg-backup-synology.sh
# Verify the archive
sudo -E BORG_PASSPHRASE="$BORG_PASSPHRASE" borg list /mnt/synology/borg-repo --last 3

8.9.8. Persistent NFS mount (optional)

# Add to /etc/fstab for auto-mount
echo "nas-01.inside.domusdigitalis.dev:/volume1/borg_backups /mnt/synology nfs defaults,noauto,x-systemd.automount 0 0" | sudo tee -a /etc/fstab

9. Phase 8: Development Environment

9.1. Programming Languages & Runtimes

9.1.1. Python (uv)

sudo pacman -S python
# Install uv (fast Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh

9.1.2. Node.js

sudo pacman -S nodejs npm

9.1.3. Rust

sudo pacman -S rustup
rustup default stable

9.1.4. Go (optional)

sudo pacman -S go

9.2. Documentation Toolchain (Antora)

# Antora runs via npx — no global install needed
# Verify node/npm versions
node --version
npm --version

9.3. Clone Documentation Repos (Tier 2)

cd ~/atelier/_bibliotheca
# Hub
git clone git@github.com:EvanusModestus/domus-docs.git
# Core spokes
for repo in domus-captures domus-infra-ops domus-ise-linux domus-secrets-ops domus-linux-ops domus-antora-ui; do
    git clone git@github.com:EvanusModestus/$repo.git
done
# Verify build
cd ~/atelier/_bibliotheca/domus-docs
make

9.4. Clone Active Project Repos

cd ~/atelier/_projects/personal
for repo in netapi netapi-tui domus-cli ise-automation domus-digitalis evanusmodestus-site; do
    git clone git@github.com:EvanusModestus/$repo.git
done

9.5. Add Multi-Remote Push for All Repos

# For each repo that needs GitLab + Gitea mirrors:
# Example for domus-captures
cd ~/atelier/_bibliotheca/domus-captures
git remote add gitlab git@gitlab.com:EvanusModestus/domus-captures.git
git remote add gitea ssh://git@gitea-01.inside.domusdigitalis.dev:2222/evanusmodestus/domus-captures.git

9.6. Claude Code

# Install Claude Code CLI
npm install -g @anthropic-ai/claude-code
# Verify (dots-quantum/claude already stowed settings + hooks)
claude --version

9.7. Container Tools

sudo pacman -S podman buildah skopeo

9.8. Virtualization (KVM/libvirt)

sudo pacman -S qemu-full libvirt virt-manager dnsmasq
sudo systemctl enable --now libvirtd
sudo usermod -aG libvirt evanusmodestus

9.9. Additional Dev Tools

sudo pacman -S \
    httpie \
    tokei \
    dust \
    duf \
    procs \
    bandwhich \
    hyperfine
yay -S rmapi

10. Phase 9: Verification & Hardening

10.1. System Verification Checklist

Check Command Expected

Boot

System boots without errors

[ ] Pass

LUKS

lsblk shows cryptroot

[ ] Pass

Btrfs subvolumes

btrfs subvolume list /

[ ] 4 subvolumes (@, @home, @var, @snapshots)

Network (wired)

ping -c 3 archlinux.org

[ ] Pass

Network (WiFi)

nmcli device wifi list

[ ] SSIDs visible

GPU

nvidia-smi or lspci | grep VGA

[ ] Driver loaded

Audio

wpctl status

[ ] PipeWire running

Bluetooth

bluetoothctl show

[ ] Powered yes

10.2. Desktop Verification

Check Test Expected

Hyprland launches

Login from TTY or SDDM

[ ] Desktop visible

Waybar

Status bar at top

[ ] Clock, workspaces, tray

Wofi

Super+D

[ ] App launcher opens

Terminal

Super+Enter

[ ] Kitty opens

Notifications

notify-send "test" "hello"

[ ] Mako popup

Screenshots

grim -g "$(slurp)"

[ ] Region capture works

Clipboard

Copy text, wl-paste

[ ] Clipboard works

10.3. Development Verification

Check Command Expected

Neovim

nvim — plugins loaded

[ ] Pass

Git push (GitHub)

ssh -T git@github.com

[ ] Authenticated

Git push (GitLab)

ssh -T git@gitlab.com

[ ] Authenticated

Git push (Gitea)

SSH connection test

[ ] Authenticated

Antora build

cd domus-docs && make

[ ] Site builds

Python

python --version && uv --version

[ ] Both available

Node

node --version && npm --version

[ ] Both available

Rust

rustc --version && cargo --version

[ ] Both available

gopass

gopass ls

[ ] Store accessible

Claude Code

claude --version

[ ] Installed

10.4. Security Hardening

10.4.1. Firewall

sudo pacman -S ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw enable
sudo systemctl enable ufw

10.4.2. Disable Root Login via SSH

sudo sed -i 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart sshd

10.4.3. Enable Automatic Security Updates (optional)

# Consider a pacman hook or timer for unattended upgrades
# For now, manual: sudo pacman -Syu

10.4.4. Verify No Listening Services

ss -tlnp | awk '{print $4}' | sort -u

10.5. Snapshot Clean State

# Take a btrfs snapshot of the clean install
sudo btrfs subvolume snapshot -r / /.snapshots/fresh-install-$(date +%Y%m%d)
# Verify
sudo btrfs subvolume list /.snapshots

This snapshot is your rollback point. If anything goes wrong during Tier 3 setup, you can restore to this known-good state.

11. Hardware-Specific Notes

11.1. ThinkPad T16g Gen 3 (Travel: modestus-t16g)

Component Notes

CPU

Intel Core Ultra 9 275HX (24C) — same as Razer. Use intel-ucode.

GPU

NVIDIA RTX 5090 24GB GDDR7 — same driver as Razer (nvidia, not nvidia-open). nvidia_drm.modeset=1 in boot params.

RAM

64 GB DDR5-5600

Storage

Single 2TB Gen5 NVMe — 4 partitions (EFI, /boot, cryptroot 250G, crypthome ~1.7T)

Display

16" 3.2K Tandem OLED — may need HiDPI scaling in Hyprland (monitor=,preferred,auto,2 or 1.5)

WiFi

Intel AX — works out of box with iwd/NetworkManager

BIOS

F1 for setup, F12 for boot menu (ThinkPad standard). Disable Secure Boot, set UEFI only, enable dGPU mode.

Keyboard

TrackPoint + ThinkPad keyboard. No RGB control needed. Fn keys: check wev for keycodes.

Battery

~99Wh. Install tlp + tlp-rdw. ThinkPad supports charge thresholds natively via tp_smapi or natacpi.

Docking

Thunderbolt 4 — verify external display output via wlr-randr or Hyprland monitor config

# Post-install: check hardware detection
lspci | grep -E "(VGA|Network|Audio)"
# HiDPI scaling (add to hyprland.conf if text is tiny on 3.2K OLED)
# monitor=,preferred,auto,1.5
# ThinkPad battery charge thresholds (via TLP)
sudo pacman -S tlp tlp-rdw
sudo systemctl enable --now tlp
# Edit /etc/tlp.conf:
#   START_CHARGE_THRESH_BAT0=40
#   STOP_CHARGE_THRESH_BAT0=80

11.2. Razer Blade 18 (Primary: modestus-razer)

Component Notes

GPU

NVIDIA RTX — same driver setup as Legion

WiFi

Intel AX — works out of box with iwd

Keyboard

openrazer-meta + polychromatic from AUR for RGB control

Fingerprint

Check fprintd support — Razer sensors vary by model

Battery

razer_charge_limit sysfs for charge cap, or use TLP

11.3. Common to Both Machines

# Brightness control (laptop screens)
sudo pacman -S brightnessctl
# Backlight for keyboard
brightnessctl -d *kbd* set 50%
# Power profiles
sudo pacman -S power-profiles-daemon
sudo systemctl enable --now power-profiles-daemon
# Check current power profile
powerprofilesctl get

12. Appendix: System Reference Commands

Reusable commands for verifying and inspecting any Arch workstation. Run these after install, after changes, or when auditing an existing system.

12.1. Disk & Partitions

# Partition layout with sizes and mount points
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
# Partition layout with UUIDs and labels
lsblk -f
# GPT partition table details
sgdisk -p /dev/nvme0n1
# Disk usage summary for key mount points
df -h / /home /boot /boot/efi

12.2. LUKS Encryption

# List all open LUKS mappings
dmsetup ls --target crypt
# LUKS header details (cipher, key slots, UUID)
sudo cryptsetup luksDump /dev/nvme0n1p3
# Status of an open LUKS container
sudo cryptsetup status cryptroot
# Verify crypttab (additional LUKS volumes opened at boot)
cat /etc/crypttab

12.3. Btrfs

# List subvolumes on root
sudo btrfs subvolume list /
# List subvolumes on home (if separate LUKS)
sudo btrfs subvolume list /home
# Detailed space usage (data, metadata, system)
sudo btrfs filesystem usage /
# Allocation profile (single, DUP, etc.)
sudo btrfs filesystem df /
# Device error stats (corruption detection)
sudo btrfs device stats /
# Scrub status (last integrity check)
sudo btrfs scrub status /
# List snapshots
sudo btrfs subvolume list /.snapshots

12.4. Mounts & fstab

# All btrfs + vfat + ext4 mounts with full options (the master view)
findmnt -t btrfs,vfat,ext4 --output TARGET,SOURCE,FSTYPE,OPTIONS -n
# fstab contents (persistent mounts)
cat /etc/fstab
# Verify all fstab entries are mounted
findmnt --verify

12.5. Boot

# systemd-boot status (ESP path, entries, default)
bootctl status
# List boot entries
bootctl list
# Loader configuration
cat /boot/efi/loader/loader.conf
# All boot entry files
cat /boot/efi/loader/entries/*.conf
# mkinitcpio MODULES and HOOKS
grep -E '^(MODULES|HOOKS)=' /etc/mkinitcpio.conf
# Kernel versions installed
ls /boot/vmlinuz-*
# Running kernel
uname -r

12.6. Swap (zram)

# zram device status (size, compression, algorithm)
zramctl
# All swap devices
swapon --show
# zram-generator config
cat /etc/systemd/zram-generator.conf

12.7. System

# Boot time breakdown
systemd-analyze
# Slowest services at boot
systemd-analyze blame | head -20
# Failed services
systemctl --failed
# Hardware detection (GPU, network, audio)
lspci | grep -E "(VGA|Network|Audio)"
# GPU status (NVIDIA)
nvidia-smi
# NVIDIA driver and kernel module version
nvidia-smi --query-gpu=driver_version --format=csv,noheader
cat /proc/driver/nvidia/version 2>/dev/null | head -1

12.8. Network

# IPv4 addresses (concise)
ip -4 -o addr show | awk '{print $2, $4}'
# Network device status
nmcli device status
# DNS resolver status
resolvectl status
# Listening ports (what services are exposed)
ss -tlnp | awk '{print $4}' | sort -u

12.9. Firewall

# UFW status and rules
sudo ufw status verbose

12.10. Ollama (Post-Install)

# List installed models with sizes
ollama list
# Verify bind mount for model storage
findmnt /var/lib/ollama/.ollama/models
# Ollama service status
systemctl status ollama