archlinux/homelab.sh

2317 lines
57 KiB
Bash
Raw Permalink Normal View History

2025-11-22 23:15:27 +00:00
#!/usr/bin/env bash
set -a
set -E
revision=1.0l
# ZFS key
# zfsgpgkey=DDF7DB817396A49B2A2723F7403BD972F75D9D76 # archzfs
zfsgpgkey=D0F2AE55C1BF11A026D155813A658A95B8CFCC51 # myvezfs
# SSH public keys, if not supplied
sshkey=https://git.myvelabs.com/lab/archlinux/raw/branch/master/notes/sshkeys.pub
# Exit function
trap '[ "${?}" -ne 77 ] || exit 77' ERR
function die
{
local reset="$(tput sgr0)"
local red="${reset}$(tput setaf 1)"
local yellow="${reset}$(tput setaf 3)"
cat <<- abort
${red}
Error encountered for the following reason:
${yellow}
${@}
${red}
Script aborted...
${reset}
abort
exit 77
}
# EFI system check
if [ ! -d /sys/firmware/efi/efivars/ ]
then
die 'This script only works with UEFI systems'
fi
# Quit if zfs modules aren't loaded
modprobe zfs || die 'ZFS modules are not loaded'
##
## Declare functions
##
# Prompt function
function reformat
{
case "${1}" in
# Formatting
bold) format+=($(tput bold)) ;;
italic) format+=($(tput sitm)) ;;
uline) format+=($(tput smul)) ;;
blink) format+=($(tput blink)) ;;
dim) format+=($(tput dim)) ;;
# Colours
black) format+=($(tput setaf 0)) ;;
red) format+=($(tput setaf 1)) ;;
green) format+=($(tput setaf 2)) ;;
yellow) format+=($(tput setaf 3)) ;;
blue) format+=($(tput setaf 4)) ;;
magenta) format+=($(tput setaf 5)) ;;
cyan) format+=($(tput setaf 6)) ;;
white) format+=($(tput setaf 7)) ;;
# Read-specific
array) array="-a" ;;
secret) secret="-s" ;;
press) press="-n 1" ;;
# Everything else goes into line
*) line="${@}" ;;
esac
}
# Syntax: ask for var [in format] "statement"
function ask
{
local format variable line secret press array
case "${1}" in
for)
case "${2}" in
in)
die "Syntax error detected (missing variable before $(tput sitm)$(tput smso)in$(tput rmso)$(tput ritm))
Usage: ask for \${variable} in \${format[@]} \"\${line}\""
;;
*)
variable="${2}"
shift 2
;;
esac
;;
*)
die "Syntax error detected (misuse of $(tput sitm)$(tput smso)for$(tput rmso)$(tput ritm))
Usage: ask for \${variable} in \${format[@]} \"\${line}\""
;;
esac
case "${1}" in
in)
shift
for arg in "${@}"
do
reformat ${arg}
done
;;
*) line="${@}" ;;
esac
read ${press} -r -p "$(printf "%s" "${format[@]}" "${line}: " "$(tput sgr0)")" ${secret} ${array} ${variable}
echo
export ${variable}
}
# Syntax: say [as template] [in format] "statement"
function say
{
local format line heading
case "${1}" in
as)
# Templates
case ${2} in
title)
format+=($(tput setaf 6)) # cyan
;;
heading)
format+=($(tput setaf 5) $(tput bold)) # bold magenta
heading=":: "
;;
success)
format+=($(tput setaf 2)) # green
;;
warning)
format+=($(tput setaf 1)) # red
;;
*)
die "Syntax error detected (misuse of $(tput sitm)$(tput smso)as$(tput rmso)$(tput ritm))
Usage: say as \${template} in \${format[@]} \"\${line}\""
;;
esac
shift 2
;;
esac
case "${1}" in
in)
shift
for arg in "${@}"
do
reformat ${arg}
done
;;
*) line="${@}" ;;
esac
printf "%s" "${format[@]}" "${heading}" "${line}" "$(tput sgr0)"
echo
}
function presskeytoresume
{
read -n 1 -s -p "Press any key when ready..."
echo
}
# Repeat a command until it exits with code 0
function repeat
{
until ${@}
do
sleep 3
done
}
# Fetch flags
rm -f sshkeys
while [ ${1} ]
do
case ${1} in
-s | --ssh-key )
if [ "${2}" ]
then
echo "${2}" >>sshkeys
if ! ssh-keygen -l -f sshkeys >/dev/null
then
die 'Invalid SSH public key detected'
fi
shift
fi
;;
-p | --port )
if [ "${2}" ]
then
port=${2}
shift
fi
;;
-c | --cache )
if [ "${2}" ]
then
cacheserver=${2}
shift
fi
;;
-t | --test )
load_defaults=y
;;
-? | -h | --help )
cat <<- help
Parameters:
-s, --ssh-key Add SSH public key (enclosed in quotes)
-p, --port SSH port
-c, --cache Pacman cache server
-t, --test Load troubleshooting defaults
-?, -h, --help This help screen
help
exit 0
;;
* ) die "Unknown flag: ${1}" ;;
esac
shift
done
# Quit if ssh keys are missing
if [ ! -f sshkeys ]
then
curl --fail --silent ${sshkey} -o sshkeys
fi
# Ensure pacman.conf settings are properly set
if [ ! -f /etc/pacman.conf.bkp ]
then
mv /etc/pacman.conf /etc/pacman.conf.bkp
fi
sed -e '/ParallelDownloads/c ParallelDownloads = 10' \
-e '/Color/c Color' /etc/pacman.conf.bkp >/etc/pacman.conf
if ! grep -q myvezfs /etc/pacman.conf
then
sed '/\[core\]/i [myvezfs]\
Server = https://mirror.myvelabs.com/repo/$repo\
Server = https://repo.myvelabs.com/$repo\n' -i /etc/pacman.conf
fi
# Internet connection check
if nc -z -w 1 archlinux.org 443 >/dev/null 2>&1 || nc -z -w 1 google.com 443 >/dev/null 2>&1
then
timedatectl set-ntp true
pacman -Sy --ask 4 >/dev/null 2>&1
else
die 'No internet connectivity detected, plug in an ethernet cable and try again'
fi
# Determine microcode
if lscpu | grep 'Model name:' | grep -q AMD
then
ucode=amd-ucode
elif lscpu | grep 'Model name:' | grep -q Intel
then
ucode=intel-ucode
else
die 'Unable to determine CPU type'
fi
# Network interface details
originaliface=$(dmesg | grep $(ip r | grep 'default via' | awk '{print $5}') | grep 'renamed from' | awk '{print $NF}')
iface=$(ip r | grep 'default via' | awk '{print $5}')
macaddress=$(cat /sys/class/net/${iface}/address)
# Detect needed firmwares
if ls -l /dev/disk/* | grep -q VBOX
then
linux_firmware+=(virtualbox-guest-utils)
systemd_services+=(vboxservice.service)
[ ${cacheserver} ] || cacheserver=http://10.0.2.2:9090
elif ls -l /dev/disk/* | grep -q 'virtio\|QEMU'
then
linux_firmware+=(qemu-guest-agent spice-vdagent)
systemd_services+=(qemu-guest-agent.service)
[ ${cacheserver} ] || cacheserver=http://192.168.122.1:9090
if lspci | grep VGA | grep -q QXL
then
linux_firmware+=(xf86-video-qxl)
fi
else
linux_firmware+=(linux-firmware)
for firmware in $(pacman -Ssq linux-firmware- | sed 's/linux-firmware-//')
do
if lspci | grep -q -i ${firmware}
then
linux_firmware+=(linux-firmware-${firmware})
fi
done
fi
# Virtual machine defaults
function virtualMachineDefaults
{
hostname=archzfs
userpass=123
lukspass=12345678
installation_drive_array=($(lsblk -d -n -o name,type | awk '($2 == "disk") {print $1}'))
primaryip=192.168.122.122
gateway=192.168.122.1
netconf=ip=${primaryip}::${gateway}:255.255.255.0::\${originaliface}:none
say in italic 'Troubleshooting defaults loaded'
echo
}
# Fetch disk ID
function returnDISKID
{
find -L /dev/disk/by-id/ -samefile /dev/${1} | grep -E "(wwn|nvme-(uuid|nvme|eui)|QEMU|virtio|VBOX)" | head -1
}
# Grab system variables
function initialSetup
{
# Hostname
say as title "Create a name for your computer"
until [ ${hostname} ]
do
ask for hostname "Hostname"
if [ -z ${hostname} ]
then
say as warning "Hostname cannot be empty, try again"
fi
done
# User password
say as title "Set a password for user"
until [ "${userpass}" = "${userpass2}" -a "${userpass}" ]
do
ask for userpass in secret "User password"
ask for userpass2 in secret "Verify user password"
echo
if [ -z "${userpass}" ]
then
say as warning "Password field cannot be empty, try again"
elif [ "${userpass}" != "${userpass2}" ]
then
say as warning "Passwords did not match, try again"
fi
done
say as success "user@${hostname}'s password has been saved"
echo
# Encryption key
say as title "Create an encryption passphrase for the system drive"
until [ "${lukspass}" = "${lukspass2}" -a "${lukspass}" -a "${#lukspass}" -ge 8 ]
do
ask for lukspass in secret "Encryption password"
ask for lukspass2 in secret "Verify encryption password"
echo
if [ -z "${lukspass}" ]
then
say as warning "Passphrase field cannot be empty, try again"
elif [ "${lukspass}" != "${lukspass2}" ]
then
say as warning "Passphrases did not match, try again"
elif [ "${#lukspass}" -lt 8 ]
then
say as warning "Passphrase needs to be at least 8 characters"
fi
done
say as success "Encryption passphrase has been saved"
echo
# Assign system drive
local installation_drive tmpavailabledrives availabledrives valid
say as title "Identify the system drive from the list of available devices below"
say in italic '## SSD and HDD device format begins with "sd" or "hd" (sda, sdb, sd[*])'
say in italic '## NVME and PCI device format is "nvme[*]n1" (nvme0n1, nvme1n1, nvme[*]n1)'
while true
do
if [ "${installation_drive}" ]
then
installation_drive=($(printf "%s\n" ${installation_drive} | sort -u))
local tmpavailabledrives=(${availabledrives[@]})
for i in ${installation_drive[@]}
do
if [[ " ${availabledrives[@]} " =~ " ${i} " ]]
then
installation_drive_array+=(${i})
availabledrives=(${availabledrives[@]/${i}})
valid=1
else
say as warning "Invalid selection: ${i}"
say as warning "Try again with the following valid drives:"
echo "$(tput smul)${tmpavailabledrives[@]}$(tput sgr0)"
echo
valid=0
break
fi
done
[ ${valid} -eq 0 ] || break
fi
# Reset arrays
unset installation_drive_array
availabledrives=($(lsblk -d -n -o name,type | awk '($2 == "disk") {print $1}'))
lsblk -d -o name,model,size,mountpoint,type | grep "NAME\|disk"
ask for installation_drive "Installation device"
done
say as success "You have selected: ${installation_drive_array[@]/#/\/dev\/}"
echo
# Assign more system drives if available
local tmpavailabledrives next_installation_drive tmpinstallation_drive_array
while true
do
if [ "${availabledrives[*]}" ]
then
say as title "More available drives detected: ${availabledrives[@]}"
say in italic "(leave blank to skip this step)"
say in italic '## SSD and HDD device format begins with "sd" or "hd" (sda, sdb, sd[*])'
say in italic '## NVME and PCI device format is "nvme[*]n1" (nvme0n1, nvme1n1, nvme[*]n1)'
unset tmpinstallation_drive_array
tmpavailabledrives=(${availabledrives[@]})
ask for next_installation_drive "Installation device"
# Skip if blank
if [ "${next_installation_drive}" ]
then
next_installation_drive=($(printf "%s\n" ${next_installation_drive} | sort -u))
for i in ${next_installation_drive[@]}
do
if [[ " ${tmpavailabledrives[@]} " =~ " ${i} " ]]
then
tmpinstallation_drive_array+=(${i})
tmpavailabledrives=(${tmpavailabledrives[@]/${i}})
valid=1
else
say as warning "Invalid selection: ${i}"
say as warning "Try again with the following valid drives:"
echo "$(tput smul)${availabledrives[@]}$(tput sgr0)"
echo
valid=0
break
fi
done
else
break
fi
# Success
if [ ${valid} -eq 1 ]
then
installation_drive_array+=(${tmpinstallation_drive_array[@]})
availabledrives=(${tmpavailabledrives[@]})
say as success "You have added: ${tmpinstallation_drive_array[@]/#/\/dev\/}"
echo
fi
else
break
fi
done
# Static IP
while true
do
ask for staticip in blue press 'Would you like to use a static IP? (y/n)'
echo
case ${staticip} in
[yY])
# Static IP
say as title "Fill in your machine's static IP"
until grep -qE -o '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<<${primaryip}
do
ask for primaryip "IP Address"
if ! grep -qE -o '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<<${primaryip}
then
say as warning "Invalid address format, try again"
fi
done
# Gateway
say as title "Fill in your network's gateway"
until grep -qE -o '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<<${gateway}
do
ask for gateway "Gateway"
if ! grep -qE -o '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<<${gateway}
then
say as warning "Invalid address format, try again"
fi
done
# Set boot parameter netconf variable for static ip
netconf=ip=${primaryip}::${gateway}:255.255.255.0::\${originaliface}:none
break
;;
[nN])
# Set boot parameter netconf variable for dhcp
netconf=ip=:::::\${originaliface}:dhcp
break
;;
*) say as warning 'Not a valid answer, type "y" or "n"' ;;
esac
done
}
# Customize mirrored zpools
function mirrorsetup
{
local mirrorsetupArray=(${installation_drive_array[@]})
local mirror pair pairArray diskid pool
local index=1
while true
do
# Exit only when all drives are assigned
if [[ $(wc -w <<<${mirrorsetupArray[@]}) -gt 0 ]]
then
say in black bold "# System drives: $(echo ${mirrorsetupArray[@]})"
ask for pool${index} in array "Enter mirror pool ${index}"
else
break 1
fi
# Temporary variables
pair=pool${index}[@]
pairArray=($(printf '%s\n' ${!pair}))
# Escape only if selections are valid
while true
do
# Ensure drive is chosen from installation drive pool
for drive in ${pairArray[@]}
do
if ! [[ " ${mirrorsetupArray[*]} " =~ " ${drive} " ]]
then
say as warning "Invalid drive selected"
echo
break 2
fi
done
# Reject mirrored pair if drives are entered more than once
if [ $(printf '%s\n' ${!pair} | sort | uniq -d) ]
then
say as warning "Duplicate entries detected"
echo
break 1
# Need at least 2 drives per mirrored pool
elif [[ ${#pairArray[@]} -lt 2 ]]
then
say as warning 'Mirrored pairs must have at least two drives'
echo
break 1
# Stray protection
elif [[ $((${#mirrorsetupArray[@]}-${#pairArray[@]})) -eq 1 ]]
then
say as warning 'Remaining drive does not have a mirror pair'
echo
break 1
# Success: remove entries from remaining drive pool
else
for delete in ${pairArray[@]}
do
mirrorsetupArray=(${mirrorsetupArray[@]/${delete}})
done
# Add an index if there are more drives remaining
((index++))
break 1
fi
done
done
# Convert drives to persistent disk/by-id naming
for pool in ${!pool*}
do
((mirror++))
local diskid=${pool}[@]
for drive in ${!diskid}
do
eval "mirrorpool${mirror}+=(${drive})"
done
done
# Export zroot layout
unset zrootlayout
pool=0
for mirrorpool in ${!mirrorpool*}
do
((pool++))
zpool=${mirrorpool}[@]
say as success "zroot mirror ${pool}: ${!zpool}"
zrootlayout+=($(echo "mirror ${!zpool}"))
done
echo
}
# Generate SSH keys
function gensshid
{
ssh-keygen -q \
-t ed25519 \
-P "" \
-C "${USER}@${hostname}" \
-f ~/.ssh/id_ed25519
mkdir ~/.ssh/sockets/
}
clear
say in italic "## rev ${revision}"
echo
say in uline white dim "Full Disk Encryption with EFISTUB"
echo
presskeytoresume
echo
# Setup
if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU'
then
while true
do
if ! [ -v load_defaults ]
then
ask for load_defaults in blue press "Would you like to load virtual machine troubleshooting defaults? (y/n)"
echo
fi
case ${load_defaults} in
[yY])
virtualMachineDefaults
break
;;
[nN])
initialSetup
break
;;
*) say as warning 'Not a valid answer, type "y" or "n"' ;;
esac
done
else
initialSetup
fi
# Default zroot layout
zrootlayout=($(echo mirror ${installation_drive_array[@]}))
# Optional: choose zroot mirror layout
if [ ${#installation_drive_array[@]} -ge 4 ]
then
while true
do
ask for mirror_setup in blue press "Would you like to split the installation disks into mirrored pairs? (y/n)"
echo
case ${mirror_setup} in
[yY])
mirrorsetup
break
;;
[nN])
break
;;
*) say as warning 'Not a valid answer, type "y" or "n"' ;;
esac
done
elif [ ${#installation_drive_array[@]} -eq 1 ]
then
zrootlayout=(${installation_drive_array[@]})
fi
# Fetch disk ID of installation disks
for drive in ${installation_drive_array[@]}
do
diskid=$(returnDISKID ${drive})
zrootlayout=(${zrootlayout[@]/${drive}/${diskid}-part2})
installation_disks+=(${diskid})
done
# Device shredding
while true
do
ask for shred_disk in blue press 'Would you like to shred your installation drive? (y/n)'
echo
case ${shred_disk} in
[yY])
say as title "Select an overwrite source"
cat <<- shred
1) zero
2) urandom
shred
while true
do
ask for shred_source in press ":"
case ${shred_source} in
1)
shred=zero
break
;;
2)
shred=urandom
break
;;
*)
echo
say as warning "Invalid selection, type an option from 1 to 2"
;;
esac
done
echo
# Disk shredding function
function destroy_disk
{
say as title "Shredding ${1}"
dd if=/dev/${shred} of=${1} bs=4M status=progress
echo 3 >/proc/sys/vm/drop_caches
say as success "${1} shredded"
echo
}
break
;;
[nN])
say in yellow "Disk shredding skipped"
echo
# Empty disk shredding function
function destroy_disk
{
true
}
break
;;
*) say as warning 'Not a valid answer, type "y" or "n"' ;;
esac
done
say as heading "Shredding drives"
cat <<- warning
$(tput setab 1)#############################################################$(tput sgr0)
$(tput setab 1)#############################################################$(tput sgr0)
$(tput setab 1) $(tput sgr0)
$(tput setab 1) $(tput blink)!!!WARNING!!! $(tput sgr0)
$(tput setab 1) !!!ALL DATA WILL BE WIPED FROM THE DRIVE!!! $(tput sgr0)
$(tput setab 1) $(tput sgr0)
$(tput setab 1)#############################################################$(tput sgr0)
$(tput setab 1)#############################################################$(tput sgr0)
## This will overwrite the drive by one pass
## Confirm by typing "Y" to proceed
## Anything other than capital Y will abort the installation
warning
ask for begin_install in press ":"
if ! [[ ${begin_install} = Y ]]
then
echo
say as warning in bold 'Aborting installation
Installer script stopped'
echo
exit 1
fi
echo
# Unmount if mounted
until ! mount | grep -q /mnt/boot
do
umount /mnt/boot
done
# Export zpool
zpool export -a
# Clean slate
for drive in ${installation_disks[@]}
do
destroy_disk ${drive} &
done
wait
for drive in ${installation_disks[@]}
do
blkdiscard --quiet --force --secure ${drive} >/dev/null 2>&1
wipefs --quiet --force --all ${drive}
done
# Partition each drive
say as heading "Partitioning system drive"
for drive in ${installation_disks[@]}
do
parted --script --align=optimal ${drive} \
mklabel gpt \
mkpart boot 1MiB 300MiB \
mkpart zroot 300MiB 100% \
set 1 esp on
partprobe ${drive}
done
say as success "Partitioning completed!"
echo
# Format zfs
say as heading "Formatting zfs partitions"
until printf '%s' "${lukspass}" |\
zpool create -f zroot \
-o ashift=12 \
-o autotrim=on \
-R /mnt \
-O acltype=posixacl \
-O canmount=off \
-O compression=zstd \
-O dnodesize=auto \
-O normalization=formD \
-O atime=off \
-O xattr=sa \
-O mountpoint=none \
-O encryption=aes-256-gcm \
-O keyformat=passphrase \
-O keylocation=prompt \
${zrootlayout[@]} \
>/dev/null
do
sleep 1
done
# Mount datasets
zfs create -o mountpoint=/ -o canmount=noauto zroot/ROOT
zfs create -o mountpoint=/.boot zroot/BOOT
zfs create -o mountpoint=/home zroot/HOME
zfs create -o mountpoint=/etc/letsencrypt zroot/HTTPS
zfs create -o mountpoint=/opt/local zroot/LOCAL
zfs create -o mountpoint=/var/lib/libvirt/images -o recordsize=64K zroot/QEMU
zfs create -o mountpoint=/var/lib/docker zroot/DOCKER
zfs create -o mountpoint=/var/cache/pacman/pkg zroot/PKG
zfs create -o mountpoint=/var/log zroot/LOG
zfs create -o mountpoint=/var/tmp zroot/TMP
zpool export zroot
zpool import -R /mnt zroot -N -d ${installation_disks}-part2
printf '%s' "${lukspass}" | zfs load-key zroot
zfs mount zroot/ROOT
zfs mount -a
# Format efi partition
for drive in ${installation_disks[@]}
do
yes | mkfs.fat -F 32 ${drive}-part1
done
echo
# Mount efi partition
mount --mkdir ${installation_disks}-part1 /mnt/boot/
# # Confirm mounts
findmnt '/mnt' | grep -q -w 'zroot/ROOT' || die 'Root partition has not been mounted properly'
findmnt '/mnt/boot' | grep -q -w '/mnt/boot' || die 'Boot partition has not been mounted properly'
chmod 1777 /mnt/var/tmp/
# Delete unneccessary EFI files
rm -f /sys/firmware/efi/efivars/dump-*
# Default packages
archpkgs="smartmontools \
${linux_firmware[@]} \
${ucode} \
opendoas openssh efibootmgr fakeroot \
vim pacman-contrib bash-completion parted \
mkinitcpio-netconf mkinitcpio-tinyssh tinyssh \
reflector rsync sshfs \
archiso \
fail2ban \
nginx-mainline certbot certbot-nginx apache \
nextcloud-client gnome-keyring \
qemu-desktop virt-manager edk2-ovmf \
iptables-nft dnsmasq dmidecode vde2 bridge-utils openbsd-netcat \
docker docker-compose docker-buildx \
pipewire-jack ttf-dejavu ttf-hack \
sway seatd swaybg bemenu bemenu-wayland foot kate dolphin konsole kompare breeze-icons kde-cli-tools \
wayvnc polkit-kde-agent \
firefox firefox-decentraleyes firefox-ublock-origin \
shotwell mpv ffmpegthumbs \
git less pv" # i3status ark okular inetutils
# Add CacheServer if inside a VM
if [ ${cacheserver} ]
then
grep -q "^CacheServer" /etc/pacman.conf ||\
sed "/^\[core\]$\|^\[extra\]$\|^\[myvezfs\]$/a CacheServer = ${cacheserver}" -i /etc/pacman.conf
fi
# Temporarily disable mkinitcpio
ln -s -f /dev/null /etc/pacman.d/hooks/90-mkinitcpio-install.hook
# Install archzfs
say as heading "Installing Arch Linux base packages"
repeat pacstrap -K /mnt --ask 4 \
linux zfs-linux zfs-utils \
base mkinitcpio \
${archpkgs}
# Configure zfs for pacman
sed -e '/ParallelDownloads/c ParallelDownloads = 10' \
-e '/Color/c Color' \
-e '/\[core\]/i [myvezfs]\
Server = https://mirror.myvelabs.com/repo/$repo\
Server = https://repo.myvelabs.com/$repo\n' \
-i /mnt/etc/pacman.conf
# Add CacheServer if inside a VM
if [ ${cacheserver} ]
then
sed "/^\[core\]$\|^\[extra\]$\|^\[myvezfs\]$/a CacheServer = ${cacheserver}" -i /mnt/etc/pacman.conf
fi
# Add archzfs keys
arch-chroot /mnt pacman-key -r ${zfsgpgkey} >/dev/null 2>&1
arch-chroot /mnt pacman-key --lsign-key ${zfsgpgkey} >/dev/null 2>&1
# Manually configure mkinitcpio
sed -e "s|%PKGBASE%|linux|g" \
-e "s/^fallback/#&/g" \
-e "s/ 'fallback'//" \
/mnt/usr/share/mkinitcpio/hook.preset >/mnt/etc/mkinitcpio.d/linux.preset
rsync -a /mnt/usr/lib/modules/*/vmlinuz /mnt/boot/vmlinuz-linux
# Create custom system dirs
mkdir -p /mnt/etc/{zfs/zfs-list.cache,pacman.d/hooks,nginx/sites-{available,enabled}}/ \
/mnt/{zfs/{bin,snapshots},etc/libvirt/hooks/qemu.d}/ \
/mnt/etc/systemd/{logind,journald}.conf.d/ \
/mnt/opt/local/{bin,systemd,hooks}/ \
/mnt/srv/{repo,sftp}/
# Configure zfs for mkinitcpio
2025-12-10 22:43:38 +00:00
grep "^HOOKS" /mnt/etc/mkinitcpio.conf |\
sed -e 's/filesystems/netconf tinyssh zfsencryptssh zfs &/' \
-e 's/systemd/udev/' \
-e 's/sd-vconsole/consolefont/' \
-e 's/ fsck//' >/mnt/etc/mkinitcpio.conf.d/zz-hooks.conf
2025-11-22 23:15:27 +00:00
cat >/mnt/etc/mkinitcpio.conf.d/zz-binaries.conf <<- 'binaries'
BINARIES+=(/usr/bin/zfs)
binaries
cat >/mnt/etc/mkinitcpio.conf.d/zz-modules.conf <<- 'modules'
MODULES_DECOMPRESS="yes"
MODULES+=(zfs)
modules
# ZFS properties
zpool set bootfs=zroot/ROOT zroot
zpool set cachefile=/etc/zfs/zpool.cache zroot
rsync -a /etc/zfs/zpool.cache /mnt/etc/zfs/zpool.cache
touch /mnt/etc/zfs/zfs-list.cache/zroot
# Networking
ln -s -f /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf
cat >/mnt/etc/systemd/network/20-${iface}.link <<- link
[Match]
PermanentMACAddress=${macaddress}
[Link]
Description=Rename primary network interface to eth0
Name=eth0
link
if [ ${primaryip} ]
then
# Static IP
rsync -a /etc/systemd/network/20-ethernet.network /mnt/etc/systemd/network/zz-fallback.network
cat >/mnt/etc/systemd/network/20-${iface}.network <<- network
[Match]
Name=eth0
[Network]
Description=Static network
DHCP=no
# IPv4
Address=${primaryip}/${netmask4:-24}
Gateway=${gateway}
network
else
# DHCP
rsync -a /etc/systemd/network/20-ethernet.network /mnt/etc/systemd/network/20-${iface}.network
fi
# SSH and Dropbear
cat sshkeys >/mnt/etc/tinyssh/root_key
sed -i "s/ 22 / ${port:-22} /" /mnt/usr/lib/initcpio/hooks/tinyssh
# Chroot into new root
arch-chroot /mnt /usr/bin/bash <<- "CHROOT"
# Global bashrc
tee -a /etc/skel/.bashrc >/dev/null <<'bashglobal'
# Environment additions
export PATH=${PATH}:/opt/local/bin:/zfs/bin
export SUDO_PROMPT=$'\a'"$(tput rev)[sudo] password for %p:$(tput sgr0)"' '
alias sudoedit='sudo vim'
# Colored prompts
alias diff='diff --color=auto'
alias ip='ip -color=auto'
export LESS='-R --use-color -Dd+r$Du+b$'
# Source bash functions
for file in $(find ~/.local/functions -type f)
do
. ${file}
done
# Auto cd into directory
shopt -s autocd
# Enable tab complete for sudo
complete -c -f doas
complete -F _command doas
complete -c -f sudo
complete -F _command sudo
# Ignore duplicate and whitespace history entries
export HISTCONTROL=ignoreboth
#
# ~/.bash_aliases
#
# ZFS/btrfs
alias zpool-monitor='watch -d -n 1 zpool status -c smart'
# alias zfs='sudo zfs'
# alias zpool='sudo zpool'
# Shutdown reboot
alias poweroff='sudo poweroff'
alias reboot='sudo reboot'
# Clear bash history
alias clearhistory='rm ${HISTFILE}; history -c -w'
# Miscellanous pacman
alias orphans='pacman -Qdtq && pacman -Qdtq | sudo pacman -Rns -'
alias dbunlock='sudo rm -f /var/lib/pacman/db.lck && sudo pacman -Syyu'
# Rsync
alias rsync='rsync -v -h --progress --info=progress2 --partial --append-verify'
#
# ~/.bash_functions
#
# Remove login lock (for successive failed login attempts)
function unlock-user
{
faillock --user ${1:-user} --reset
}
# Pacman tools
function installer
{
sudo pacman --sync ${@}
echo
}
function uninstall
{
sudo pacman --remove --cascade --nosave --recursive ${@}
echo
}
function syur
{
/opt/local/bin/syu &&
reboot
}
function syup
{
/opt/local/bin/syu &&
poweroff
}
# Update bash
function update-bash
{
vim ~/.bashrc &&
source ~/.bashrc
}
# Restart swayvnc
function restart-swayvnc
{
systemctl --quiet --user daemon-reload
systemctl --quiet --user restart sway.service wayvnc.service
systemctl --user status sway.service wayvnc.service
}
bashglobal
# Root bashrc
rsync -a /etc/skel/.bashrc ~/
mkdir -p ~/.local/functions/
cat > ~/.local/functions/root <<- 'rootbashrc'
#!/usr/bin/env bash
# Root shell color
PS1="$(tput setaf 1)[\u@\h \W \$?]\$$(tput sgr0) "
# Colored prompts
alias ll='ls --color=auto -l -a -h'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
# Disable history
unset HISTFILE
rm -f ${HISTFILE}
history -c -w
rootbashrc
# Configure ssh
gensshid
ssh-keygen -A >/dev/null
cat >/etc/ssh/sshd_config.d/zz-homelab.conf <<- sshd
Port ${port:-22}
PermitRootLogin no
PasswordAuthentication no
AuthenticationMethods publickey
sshd
# tinyssh
rm -r -f /etc/tinyssh/sshkeydir/
tinyssh-convert /etc/tinyssh/sshkeydir/ </etc/ssh/ssh_host_ed25519_key
# ZFS setup
zpool set cachefile=/etc/zfs/zpool.cache zroot
zgenhostid $(hostid)
# Trim zroot monthly
cat >/etc/systemd/system/zfs-trim@.timer <<'TRIM'
[Unit]
Description=Monthly zpool trim on %i
[Timer]
OnCalendar=monthly
AccuracySec=1h
Persistent=true
[Install]
WantedBy=multi-user.target
TRIM
cat >/etc/systemd/system/zfs-trim@.service <<'TRIM'
[Unit]
Description=zpool trim on %i
Documentation=man:zpool-trim(8)
Requires=zfs.target
After=zfs.target
ConditionACPower=true
ConditionPathIsDirectory=/sys/module/zfs
[Service]
Nice=19
IOSchedulingClass=idle
KillSignal=SIGINT
ExecStart=/bin/sh -c '\
if /usr/bin/zpool status %i | grep "trimming"; then\
exec /usr/bin/zpool wait -t trim %i;\
else exec /usr/bin/zpool trim -w %i; fi'
ExecStop=-/bin/sh -c '/usr/bin/zpool trim -s %i 2>/dev/null || true'
[Install]
WantedBy=multi-user.target
TRIM
# Scrub zroot monthly
cat >/etc/systemd/system/zfs-scrub@.timer <<'SCRUB'
[Unit]
Description=Monthly zpool scrub on %i
[Timer]
OnCalendar=monthly
AccuracySec=1h
Persistent=true
[Install]
WantedBy=multi-user.target
SCRUB
cat >/etc/systemd/system/zfs-scrub@.service <<'SCRUB'
[Unit]
Description=zpool scrub on %i
[Service]
Nice=19
IOSchedulingClass=idle
KillSignal=SIGINT
ExecStart=/usr/bin/zpool scrub %i
[Install]
WantedBy=multi-user.target
SCRUB
echo
# mkinitcpio
say as heading "Regenerating cpio image"
mkinitcpio -P
echo
# Locale
sed -i '/#en_US.UTF-8 UTF-8/ s/#//' /etc/locale.gen
locale-gen >/dev/null
echo 'LANG=en_US.UTF-8' >>/etc/locale.conf
say as heading "Locale configured"
# Time zone
if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU'
then
ln -s -f $(find /usr/share/zoneinfo/ | shuf -n 1) /etc/localtime
else
ln -s -f /usr/share/zoneinfo/UTC /etc/localtime
fi
hwclock --systohc --utc
say as heading "Time zone configured"
# Hostname
echo ${hostname} >/etc/hostname
cat >>/etc/hosts <<- host
127.0.0.1 localhost
127.0.1.1 ${hostname}
host
say as heading "Hostname configured"
# User
useradd --create-home --gid users --groups wheel,libvirt,seat,docker --shell /usr/bin/bash user || die "User account creation has failed"
printf '%s\n' "${userpass}" "${userpass}" | passwd user >/dev/null 2>&1
unset userpass userpass2
# Disable root account
passwd -l root >/dev/null 2>&1
# Sudoers
# install -m 0440 /dev/stdin /etc/sudoers.d/01-DEFAULTS <<- 'DEFAULTS'
# Defaults passwd_timeout=0
# Defaults timestamp_type=global
# Defaults insults
# DEFAULTS
#
# install -m 0440 /dev/stdin /etc/sudoers.d/02-COMMANDS <<- 'COMMANDS'
# Cmnd_Alias POWER = /usr/bin/poweroff, /usr/bin/reboot
# Cmnd_Alias ZFS = /usr/bin/zfs, /usr/bin/zpool
# Cmnd_Alias QEMU = /usr/bin/virsh, /usr/bin/qemu-system-x86_64, /usr/bin/virt-install
# Cmnd_Alias FAIL2BAN = /usr/bin/fail2ban-client
# Cmnd_Alias PACMAN = /usr/bin/pacman
# Cmnd_Alias IPTABLES = /usr/bin/iptables, /usr/bin/iptables-save
# Cmnd_Alias MISC = /usr/bin/rsync
# COMMANDS
#
# install -m 0440 /dev/stdin /etc/sudoers.d/03-WHEEL <<- 'WHEEL'
# %wheel ALL=(ALL:ALL) ALL
# %wheel ALL=(ALL:ALL) NOPASSWD: POWER, ZFS, QEMU, FAIL2BAN, PACMAN, IPTABLES, MISC
# WHEEL
#
# install -m 0440 /dev/stdin /etc/sudoers.d/zz-NOPASSWD <<- 'NOPASSWD'
# Defaults:user !authenticate
# NOPASSWD
# Doas
install -m 0440 /dev/stdin /etc/doas.conf <<- 'doas'
permit nopass :root
permit nopass :wheel
doas
install /dev/stdin /usr/local/bin/sudo <<- 'doas'
#!/usr/bin/env bash
exec doas "${@/--preserve-env*/}"
doas
say as heading "Configured superuser and user"
# ZFS files
touch /zfs/snapshots/syu
chown user:users /zfs/snapshots/syu
# Sysctl custom settings
cat >/etc/sysctl.d/zz-sysctl.conf <<- SYSCTL
net.core.netdev_max_backlog = 16384
net.core.somaxconn = 8192
net.core.rmem_default = 1048576
net.core.rmem_max = 16777216
net.core.wmem_default = 1048576
net.core.wmem_max = 16777216
net.core.optmem_max = 65536
net.ipv4.tcp_rmem = 4096 1048576 2097152
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.udp_rmem_min = 8192
net.ipv4.udp_wmem_min = 8192
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_timestamps = 0
net.core.default_qdisc = cake
net.ipv4.tcp_congestion_control = bbr
# Swapiness
vm.swappiness=10
vm.vfs_cache_pressure=50
SYSCTL
# iptables
cat >/etc/iptables/userinput.rules <<- IPTABLES
## User input
# Pacman cache server
-A INPUT -s 192.168.0.0/16 -p tcp -m tcp --dport 9090 -j ACCEPT -m comment --comment "Pacman cache server"
# Open ports
-A INPUT -p tcp -m tcp --dport ${port:-22} -j ACCEPT -m comment --comment "SSH Port"
-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT -m comment --comment "HTTP Port"
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT -m comment --comment "HTTPS Port"
-A INPUT -p udp -m udp --dport 443 -j ACCEPT -m comment --comment "HTTP3 Port"
## Simple Firewall
IPTABLES
sed "/OUTPUT ACCEPT/r /etc/iptables/userinput.rules" /etc/iptables/simple_firewall.rules >/etc/iptables/iptables.rules
# zram
echo 'zram' >/etc/modules-load.d/zram.conf
echo 'options zram num_devices=1' >/etc/modprobe.d/zram.conf
echo 'KERNEL=="zram0", ATTR{comp_algorithm}="lz4", ATTR{disksize}="512M" RUN="/usr/bin/mkswap /dev/zram0", TAG+="systemd"' >/etc/udev/rules.d/99-zram.rules
# Global ssh config
cat >/etc/ssh/ssh_config.d/zz-homelab.conf <<- sshconfig
# Preferred ciphers
Ciphers aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com
# Only use ipv4
AddressFamily inet
# Multiplex
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 10m
# Ease up on local area network devices
Host 192.168.* *.kvm *.local
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
LogLevel Error
sshconfig
# Paccache hook
cat >/etc/pacman.d/hooks/zz-paccache.hook <<- PACCACHE
[Trigger]
Operation = Upgrade
Operation = Install
Operation = Remove
Type = Package
Target = *
[Action]
Description = Cleaning pacman cache...
When = PostTransaction
Exec = /usr/bin/paccache --remove
PACCACHE
# Locale.gen.pacnew hook
install /dev/stdin /opt/local/hooks/localegen <<hook
#!/usr/bin/env bash
if [ -f /etc/locale.gen.pacnew ]
then
sed '/#en_US.UTF-8 UTF-8/ s/#//' /etc/locale.gen.pacnew >/etc/locale.gen
rm /etc/locale.gen.pacnew
locale-gen >/dev/null
fi
hook
# locale.gen.pacnew hook
cat >/etc/pacman.d/hooks/100-localegen.hook <<localegen
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = glibc
[Action]
Description = Fixing locale.gen
When = PostTransaction
Exec = /opt/local/hooks/localegen
localegen
# Environment
tee -a /etc/environment >/dev/null <<environment
EDITOR=vim
SUDO_EDITOR=vim
QT_QPA_PLATFORM=wayland
DOCKER_BUILDKIT=1
COMPOSE_PARALLEL_LIMIT=-1
environment
# Sway user service
cat >/etc/systemd/user/sway.service <<- 'swayvnc'
[Unit]
Description=Sway
After=network.target systemd-user-sessions.service
[Service]
Type=simple
Environment=WLR_BACKENDS=headless
Environment=WLR_LIBINPUT_NO_DEVICES=1
ExecStart=/usr/bin/sway
[Install]
WantedBy=default.target
swayvnc
# WayVNC user service
cat >/etc/systemd/user/wayvnc.service <<- 'swayvnc'
[Unit]
Description=WayVNC
Requires=sway.service
After=sway.service
[Service]
Type=simple
Environment=WAYLAND_DISPLAY=wayland-1
ExecStart=/usr/bin/wayvnc --verbose localhost 5900
ExecStartPost=/usr/bin/bash -c "exec /usr/lib/polkit-kde-authentication-agent-1"
Restart=on-failure
RestartSec=2
TimeoutStopSec=10
[Install]
WantedBy=default.target
swayvnc
# Switch into new user
su user <<- "CHANGEUSER"
mkdir -p ~/.local/{bin,functions}/ \
~/.config/{sway/config.d,foot}/ \
~/.config/menus/
# # i3status
# mkdir ~/.config/i3status/
# cp /etc/i3status.conf ~/.config/i3status/config
# Identify sway conf locations
cat > ~/.config/sway/config <<- 'config'
include /etc/sway/config.d/*
include ~/.config/sway/config.d/*
config
# sway config
sed -e 's/mod Mod4/mod Mod1/' \
-e 's/term foot/&client/' \
-e '/set $menu/c set $menu bemenu-run -p "" --no-overlap --tb "#285577" --hb "#285577" --tf "#eeeeee" --hf "#eeeeee" --nf "#bbbbbb"' \
-e 's/position top/position bottom/' \
/etc/sway/config > ~/.config/sway/config.d/00-config
cat > ~/.config/sway/config.d/zz-wayvnc <<- 'swayvnc'
# WayVNC
output HEADLESS-1 {
pos 0,0
mode 1920x1080@60Hz
scale 1.25
}
swayvnc
cat > ~/.config/sway/config.d/zz-sway <<- 'sway'
# Disable xwayland
xwayland disable
# Start foot terminal server
exec foot --server
# Floating windows
for_window [window_role="About"] floating enable
for_window [window_role="Organizer"] floating enable
for_window [window_role="Preferences"] floating enable
for_window [window_role="bubble"] floating enable
for_window [window_role="page-info"] floating enable
for_window [window_role="pop-up"] floating enable
for_window [window_role="task_dialog"] floating enable
for_window [window_role="toolbox"] floating enable
for_window [window_role="webconsole"] floating enable
for_window [window_type="dialog"] floating enable
for_window [window_type="menu"] floating enable
# Solid black background
output HEADLESS-1 bg #000000 solid_color
# Mouse and keyboard defaults
input type:keyboard xkb_numlock enabled
sway
# Nextcloud
cat > ~/.config/sway/config.d/zz-nextcloud <<- 'nextcloud'
# Nextcloud
exec nextcloud
for_window [title="Nextcloud Settings"] floating enable
nextcloud
# Foot terminal config (/etc/xdg/foot/foot.ini)
cat > ~/.config/foot/foot.ini <<- 'foot'
[main]
term=xterm-256color
include=/usr/share/foot/themes/kitty
font=Hack:style=Regular:size=12
font-bold=Hack:style=Bold:size=12
font-italic=Hack:style=Italic:size=12
font-bold-italic=Hack:style=Bold Italic:size=12
workers=32
[scrollback]
lines=16384
foot
# Dolphin
curl --fail --silent https://raw.githubusercontent.com/KDE/plasma-workspace/master/menu/desktop/plasma-applications.menu -o ~/.config/menus/applications.menu
kbuildsycoca6 >/dev/null 2>&1
cat >> ~/.config/kdeglobals <<- foot
[General]
TerminalApplication=footclient
foot
if ls -l /dev/disk/* | grep -q 'VBOX'
then
echo 'exec VBoxClient-all' >> ~/.config/sway/config.d/zz-vbox
fi
# Generate SSH identity
gensshid
cat /etc/tinyssh/root_key >~/.ssh/authorized_keys
cat >> ~/.bashrc <<- 'BASHRC'
# Custom shell color
PS1="$(tput setaf 4)[\u@\h \W \$?]\$$(tput sgr0) "
# Colored prompts
alias ll='ls --color=auto -l -a -h'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
BASHRC
# Startup script
cat > ~/.local/functions/init <<- 'init'
#!/usr/bin/env bash
if [ -f ~/.local/bin/startup ]
then
echo -e "\n\e[1;31mInit script hasn't been run yet, executing script before proceeding...\e[0m\n"
echo "Choose a subnet for libvirt"
until grep -qE -o '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<<${subnet}
do
read -r -p "Subnet: " subnet
echo
if ! grep -qE -o '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' <<<${subnet}
then
echo "Invalid address format, try again"
echo
fi
done
~/.local/bin/startup ${subnet} || exit 1
fi
init
install /dev/stdin ~/.local/bin/startup <<'EOF'
#!/usr/bin/env bash
# eg., 192.168.222
subnet=${@}
## Tput codes
reset=$(tput sgr0)
red=${reset}$(tput setaf 1)
yellow=${reset}$(tput setaf 3)
# Exit function
trap '[ "${?}" -ne 77 ] || exit 77' ERR
function die
{
cat <<- abort
${red}
Error encountered for the following reason:
${yellow}
"${1}"
${red}
Script aborted...
${reset}
abort
exit 77
}
# Subnet check
[ ${subnet} ] || die 'Define ${subnet} before proceeding (eg, 192.168.222)'
# Internet connection check
if nc -z -w 1 archlinux.org 443 >/dev/null 2>&1 || nc -z -w 1 google.com 443 >/dev/null 2>&1
then
sudo timedatectl set-ntp true
else
die 'No internet connectivity detected, plug in an ethernet cable and try again'
fi
# SwayVNC services
systemctl --user --quiet enable --now sway.service wayvnc.service
# Start spice agent
if ls -l /dev/disk/* | grep -q 'virtio\|QEMU'
then
systemctl --user --quiet enable --now spice-vdagent.service
fi
# Services
sudo systemctl --quiet restart systemd-journald.service
sudo systemctl --quiet disable zfs-mount.service
sudo systemctl --quiet enable --now zfs-zed.service
sudo zfs set canmount=off zroot/TMP
sudo zfs set canmount=on zroot/TMP
# QEMU
# Macvtap
cat >/tmp/macvtap.xml <<- macvtap
<network>
<name>macvtap</name>
<forward mode='bridge'>
<interface dev='$(ip r | grep 'default via' | awk '{print $5}')'/>
</forward>
</network>
macvtap
sudo virsh -q net-define /tmp/macvtap.xml
sudo virsh -q net-autostart macvtap
sudo virsh -q net-start macvtap
# Isolated
cat >/tmp/isolated.xml <<- isolated
<network>
<name>isolated</name>
<ip address='${subnet}.1' netmask='255.255.255.0'>
<dhcp>
<range start='${subnet}.2' end='${subnet}.254' />
</dhcp>
</ip>
</network>
isolated
sudo virsh -q net-define /tmp/isolated.xml
sudo virsh -q net-autostart isolated
sudo virsh -q net-start isolated
# Cleanup
rm -f /tmp/*.xml
# Sway VNC service
loginctl enable-linger
if [ -f ~/.config/sway/config ]
then
rm -f ${0} ~/.local/functions/init
# sudo mv /etc/sudoers.d/zz-NOPASSWD /etc/sudoers.d/.zz-NOPASSWD
echo -e 'Supplementary installer completed, reboot one last time\e[0m\n'
else
echo -e '\n\e[31mSway configuration file not found'
echo -e 'Try again'
echo -e 'Aborting installer...\e[0m\n'
exit 1
fi
EOF
CHANGEUSER
# fail2ban
install /dev/stdin /opt/local/bin/fail2ban-jails <<- 'jails'
#!/usr/bin/env bash
JAILS=$(sudo fail2ban-client status | grep "Jail list" | sed -E 's/^[^:]+:[ \t]+//' | sed 's/,//g')
for JAIL in ${JAILS}
do
sudo fail2ban-client status ${JAIL}
done
jails
cat >/etc/fail2ban/jail.d/sshd.conf <<- 'sshd'
[sshd]
enabled = true
filter = sshd
backend = systemd
maxretry = 5
findtime = 1d
bantime = 4w
ignoreip = 127.0.0.1/8
sshd
# Nginx
sed '/^http {/a\
include http_redirect;\
include sites-enabled/\*.conf;\n\
types_hash_max_size 4096;\
server_names_hash_bucket_size 128;\n' \
-i /etc/nginx/nginx.conf
cat >/etc/nginx/http_redirect <<- 'redirect'
server {
listen 80;
server_name _;
if ($scheme = "http") {
return 301 https://$host$request_uri;
}
}
redirect
cat >/etc/nginx/proxy_params <<- 'proxy_params'
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Accept-Encoding "";
proxy_set_header Host $host;
proxy_next_upstream error timeout;
client_body_buffer_size 512k;
proxy_read_timeout 86400s;
client_max_body_size 0;
# Websocket
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_params
cat >/etc/nginx/http_upgrade <<- 'http_upgrade'
# Security
server_tokens off;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "noindex, nofollow" always;
# http2
http2 on;
# http3 (open port 443/udp to use http3)
# Add reuseport to ONLY ONE virtual host: listen 443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';
quic_retry on;
http3 on;
# Certbot defaults
add_header Strict-Transport-Security "max-age=31536000" always;
http_upgrade
# Nginx hook
install /dev/stdin /opt/local/hooks/nginx <<- 'hook'
#!/usr/bin/env bash
if [ -f /etc/nginx/nginx.conf.pacnew ]
then
sed '/^http {/a\
include http_redirect;\
include sites-enabled/\*.conf;\n\
types_hash_max_size 4096;\
server_names_hash_bucket_size 128;\n' \
-i /etc/nginx/nginx.conf.pacnew
mv /etc/nginx/nginx.conf.pacnew /etc/nginx/nginx.conf
fi
hook
# /etc/nginx/nginx.conf.pacnew hook
cat >/etc/pacman.d/hooks/100-nginx.hook <<- nginx
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = nginx-mainline
[Action]
Description = Fixing nginx.conf
When = PostTransaction
Exec = /opt/local/hooks/nginx
nginx
# Pacman cache
# Nginx conf
cat >/etc/nginx/sites-available/pacman.conf <<- 'pacman'
server {
listen 9090;
client_max_body_size 0;
location / {
alias /var/cache/pacman/pkg/;
autoindex on;
error_log /var/log/nginx/pacman_error.log;
access_log /var/log/nginx/pacman_access.log;
}
}
pacman
ln -s /etc/nginx/sites-available/pacman.conf /etc/nginx/sites-enabled/
# # Systemd service
# cat >/etc/systemd/system/local-update-pkg-cache.service <<'service'
# [Unit]
# Description=Refresh package cache twice daily
#
# [Service]
# Type=oneshot
# ExecStart=/usr/bin/bash -c "/usr/bin/pacman -Syw -d --ask 4 $(curl --fail --silent -L https://git.myvelabs.app/lab/archlinux/raw/branch/master/pkg/homelab)"
# service
# # Timer
# cat >/etc/systemd/system/local-update-pkg-cache.timer <<'timer'
# [Unit]
# Description=Refresh pacman package cache
#
# [Timer]
# OnCalendar=*-*-* 00/12:00:00
# RandomizedDelaySec=12h
# Persistent=true
#
# [Install]
# WantedBy=timers.target
# timer
# Libvirt
echo 'firewall_backend = "iptables"' >/etc/libvirt/network.conf
# Pacman hooks
# pacman.conf hook
cat >/etc/pacman.d/hooks/100-pacman.conf.hook <<- pacman
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = pacman
[Action]
Description = Fixing pacman.conf
When = PostTransaction
Exec = /opt/local/hooks/pacman.conf
pacman
install /dev/stdin /opt/local/hooks/pacman.conf <<- 'hook'
#!/usr/bin/env bash
if [ -f /etc/pacman.conf.pacnew ]
then
sed -e '/ParallelDownloads/c ParallelDownloads = 10' \
-e '/Color/c Color' \
-e '/\[core\]/i [myvezfs]\
Server = https://mirror.myvelabs.com/repo/$repo\
Server = https://repo.myvelabs.com/$repo\n' \
/etc/pacman.conf.pacnew >/etc/pacman.conf
rm /etc/pacman.conf.pacnew
fi
hook
# mkinitcpio conf hook
cat >/etc/pacman.d/hooks/85-mkinitcpio.conf.hook <<- preset
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = mkinitcpio
[Action]
Description = Updating mkinitcpio.conf
When = PostTransaction
Exec = /opt/local/hooks/mkinitcpio.conf
preset
install /dev/stdin /opt/local/hooks/mkinitcpio.conf <<- 'hook'
#!/usr/bin/env bash
# Hooks
2025-12-10 22:43:38 +00:00
grep "^HOOKS" /etc/mkinitcpio.conf |\
sed -e 's/filesystems/netconf tinyssh zfsencryptssh zfs &/' \
-e 's/systemd/udev/' \
-e 's/sd-vconsole/consolefont/' \
-e 's/ fsck//' >/etc/mkinitcpio.conf.d/zz-hooks.conf
2025-11-22 23:15:27 +00:00
# Linux preset
sed -e "s|%PKGBASE%|linux|g" \
-e "s/^fallback/#&/g" \
-e "s/ 'fallback'//" \
/usr/share/mkinitcpio/hook.preset >/etc/mkinitcpio.d/linux.preset
hook
# # bash.bashrc.pacnew hook
# cat >/etc/pacman.d/hooks/100-bash.bashrc.hook <<bashrc
# [Trigger]
# Operation = Install
# Operation = Upgrade
# Type = Package
# Target = bash
#
# [Action]
# Description = Fixing global bashrc
# When = PostTransaction
# Exec = /opt/local/hooks/bashrc
# bashrc
# install /dev/stdin /opt/local/hooks/bashrc <<'hook'
# #!/usr/bin/env bash
# if [ -f /etc/bash.bashrc.pacnew ]
# then
# tee -a /etc/bash.bashrc.pacnew >/dev/null <<bashglobal
#
# . /etc/bash.bashrc.d/global
# bashglobal
# mv /etc/bash.bashrc.pacnew /etc/bash.bashrc
# fi
# hook
# iptables.rules
cat >/etc/pacman.d/hooks/100-iptables.rules.hook <<- iptables
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = iptables-nft
[Action]
Description = Fixing iptables rules
When = PostTransaction
Exec = /opt/local/hooks/iptables.rules
iptables
install /dev/stdin /opt/local/hooks/iptables.rules <<- hook
#!/usr/bin/env bash
if [ -f /etc/iptables/iptables.rules.pacsave ]
then
sed "/OUTPUT ACCEPT/r /etc/iptables/userinput.rules" /etc/iptables/simple_firewall.rules >/etc/iptables/iptables.rules
rm /etc/iptables/iptables.rules.pacsave
fi
hook
# tinyssh
if [ "${port}" ]
then
cat >/etc/pacman.d/hooks/100-tinyssh.hook <<- tinyssh
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = mkinitcpio-tinyssh
[Action]
Description = Fixing tinyssh port
When = PostTransaction
Exec = /opt/local/hooks/tinyssh
tinyssh
install /dev/stdin /opt/local/hooks/tinyssh <<- hook
#!/usr/bin/env bash
if [ -f /usr/lib/initcpio/hooks/tinyssh.pacnew ]
then
sed -i "s/ 22 / ${port} /" /usr/lib/initcpio/hooks/tinyssh.pacnew
mv /usr/lib/initcpio/hooks/tinyssh.pacnew /usr/lib/initcpio/hooks/tinyssh
fi
hook
fi
# Disable power button
cat >/etc/systemd/logind.conf.d/zz-disablepowerbutton.conf <<- eof
[Login]
HandlePowerKey=ignore
HandlePowerKeyLongPress=ignore
PowerKeyIgnoreInhibited=yes
eof
# Persistent journal logging
cat >/etc/systemd/journald.conf.d/zz-journald.conf <<- eof
[Journal]
Storage=persistent
SystemMaxUse=100M
eof
# Custom pacman update wrapper
install /dev/stdin /opt/local/bin/syu <<- 'syu'
#!/usr/bin/env bash
set -e
# Record current time
echo $(date "+%Y-%m-%d-%H:%M:%S") >/zfs/snapshots/syu
# Check for new packages and continue if found
newpkg=($(checkupdates --nocolor | awk '{print $1}'))
if [ "${newpkg}" ]
then
# Sync pacman dbs
sudo pacman --ask 4 --sync --refresh >/dev/null
# Check zfs-linux kernel dependency
zfslinux=$(pacman -Si zfs-linux | grep "Depends On" | sed "s|.*linux=||" | awk '{print $1}')
linux=$(pacman -Si linux | grep "Version" | awk '{print $3}')
# Perform update if dependencies are satisfied
if [ ${zfslinux} = ${linux} ]
then
# Update archlinux-keyring first
if [[ ${newpkg[@]} =~ "archlinux-keyring" ]]
then
sudo pacman --ask 4 --sync --needed archlinux-keyring
echo
fi
if sudo pacman --ask 4 --sync --sysupgrade --needed
then
echo
sudo pacdiff
exit 0
fi
else
# Show update list if unable to perform update
printf "%s\n" "${newpkg[@]}"
exit 1
fi
fi
syu
CHROOT
## Pre and post update backup hooks
# Boot
cat >/mnt/etc/pacman.d/hooks/55-bootbackup_pre.hook <<- pre
[Trigger]
Operation = Upgrade
Operation = Install
Operation = Remove
Type = Path
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*
Target = usr/lib/firmware/*
Target = usr/src/*/dkms.conf
[Action]
Depends = rsync
Description = Backing up pre /boot...
When = PreTransaction
Exec = /usr/bin/bash -c 'mount ${installation_disks}-part1; rsync -a --mkpath --delete /boot/ "/.boot/\$(cat /zfs/snapshots/syu)_pre"/'
AbortOnFail
pre
cat >/mnt/etc/pacman.d/hooks/95-bootbackup_post.hook <<- post
[Trigger]
Operation = Upgrade
Operation = Install
Operation = Remove
Type = Path
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*
Target = usr/lib/firmware/*
Target = usr/src/*/dkms.conf
[Action]
Depends = rsync
Description = Backing up post /boot...
When = PostTransaction
Exec = /usr/bin/bash -c 'rsync -a --mkpath --delete /boot/ "/.boot/\$(cat /zfs/snapshots/syu)_post"/; /opt/local/hooks/post-install-boot'
post
# zroot
cat >/mnt/etc/pacman.d/hooks/00-syu_pre.hook <<- pre
[Trigger]
Type = Path
Operation = Upgrade
Operation = Install
Operation = Remove
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*
Target = usr/lib/firmware/*
Target = usr/src/*/dkms.conf
[Action]
Description = Creating pre zroot snapshot...
When = PreTransaction
Exec = /usr/bin/bash -c 'zfs snapshot zroot/ROOT@pre-\$(cat /zfs/snapshots/syu)'
AbortOnFail
pre
cat >/mnt/etc/pacman.d/hooks/zz-syu_post.hook <<- post
[Trigger]
Type = Path
Operation = Upgrade
Operation = Install
Operation = Remove
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*
Target = usr/lib/firmware/*
Target = usr/src/*/dkms.conf
[Action]
Description = Creating post zroot snapshot...
When = PostTransaction
Exec = /usr/bin/bash -c 'zfs snapshot zroot/ROOT@post-\$(cat /zfs/snapshots/syu)'
post
# efistub
cat <<- EFISTUB |\
sed s/{{ipconfig}}/${netconf}/ |\
install /dev/stdin /mnt/opt/local/bin/efistub
#!/usr/bin/env bash
# Network adapter
originaliface=\$(dmesg | grep \$(ip r | grep 'default via' | awk '{print \$5}') | grep 'renamed from' | awk '{print \$NF}')
# Kernel parameters
kernelparams="zfs=bootfs {{ipconfig}} rw quiet bgrt_disable"
# Delete old bootloader entries
for entry in \$(efibootmgr | grep "Arch" | grep -o 'Boot....' | sed 's/Boot//')
do
efibootmgr --quiet -Bb \${entry}
done
# Add bootloader entries
for drive in ${installation_disks[@]}
do
efibootmgr --quiet --create \\
--disk \${drive} --part 1 \\
--label "Arch Server (${hostname}) - \${drive}" \\
--loader "/vmlinuz-linux" \\
--unicode "initrd=\initramfs-linux.img \${kernelparams}"
done
echo -e "\$(tput setaf 5)\$(tput bold):: Added efi boot entries\$(tput sgr0)"
EFISTUB
/mnt/opt/local/bin/efistub
# Sync multiple boot partitions
if [ ${#installation_disks[@]} -gt 1 ]
then
install /dev/stdin /mnt/opt/local/hooks/post-install-boot <<- backup
#!/usr/bin/env bash
# Clone boot files
for drive in $(printf '%s-part1 ' ${installation_disks[@]:1})
do
backupdir=\$(mktemp -d)
mount \${drive} \${backupdir}
rsync -a --delete /boot/ \${backupdir}
umount \${backupdir}
rmdir \${backupdir}
done
# Unmount boot partition
umount /boot
backup
# Clone boots
arch-chroot /mnt /opt/local/hooks/post-install-boot
say as heading "Cloned boot drives"
# Disk recovery script
install /dev/stdin /mnt/zfs/bin/recover <<- RECOVERY
#!/usr/bin/env bash
## Tput codes
reset=\$(tput sgr0)
bold=\$(tput bold)
italic=\$(tput sitm)
blink=\$(tput blink)
black=\$(tput setaf 0)
red=\$(tput setaf 1)
green=\$(tput setaf 2)
yellow=\$(tput setaf 3)
blue=\$(tput setaf 4)
magenta=\$(tput setaf 5)
cyan=\$(tput setaf 6)
white=\$(tput setaf 7)
##
## functions start
##
# Color codes
function say
{
for format in \${@:2}
do
echo -n \${!format}
done
echo -e "\${1}"
echo -n \${reset}
}
function say_n
{
for format in \${@:2}
do
echo -n \${!format}
done
echo -e -n "\${1}"
echo -n \${reset}
}
function heading
{
say ":: \${1}" magenta bold
}
# Exit function
trap '[ "\$?" -ne 77 ] || exit 77' ERR
function die
{
cat <<- abort
\${red}
Error encountered for the following reason:
\${reset}\${yellow}
"\${1}"
\${reset}\${red}
Script aborted...
\${reset}
abort
exit 77
}
zpool status | grep -q DEGRADED || die 'Nothing is wrong with zroot'
# New disks
new_disk=\${1} ## eg. vda, nvme2n1
# Exit if no replacement disk specified
if ! lsblk -d -n -o name,type | grep disk | grep -q -w \${new_disk}
then
die 'Replacement disk is invalid'
elif zpool status | grep -q "\$(ls /dev/disk/by-id/ -l | grep \${new_disk}\$ | awk '{print \$9}')-part2"
then
die 'Replacement disk cannot be the same as existing disk'
fi
# Surviving drive/s
for drive in \$(sudo zpool status | grep -- -part2 | grep ONLINE | awk '{print \$1}')
do
existing_disk+=(/dev/disk/by-id/\${drive})
done
# ID
for drive in \${existing_disk[@]}
do
existing_disk_id+=(\$(echo \${drive} | sed 's/-part2\$//'))
done
# UUID
for drive in \${existing_disk_id[@]}
do
existing_disk_uuid+=(\$(sudo blkid -s UUID -o value \${drive}-part1))
done
heading 'Beginning failed disk replacement'
echo
# Missing/dead drive
missing_disk=\$(sudo zpool status | grep UNAVAIL | grep -o 'was.*' | sed 's/was //')
missing_disk_id=\$(echo \${missing_disk} | sed 's/-part2\$//')
# Replacement disk ID
replacement_disk=\$(ls -l /dev/disk/by-id/* | grep "\${new_disk}\$" | grep 'wwn\|nvme-uuid\|nvme-nvme\|nvme-eui\|QEMU\|virtio\|VBOX' | awk '{print \$9}' | head -1)
# Partition new disk
heading 'Partitioning replacement disk'
sudo blkdiscard --quiet --force --secure \${replacement_disk} >/dev/null 2>&1
sudo wipefs --quiet --force --all \${replacement_disk}
sudo parted --script --align=optimal \${replacement_disk} \\
mklabel gpt \\
mkpart boot 1MiB 300MiB \\
mkpart zroot 300MiB 100% \\
set 1 esp on
sudo partprobe \${replacement_disk}
# Clone and replace zpool
heading 'Replacing missing disk in zroot'
until sudo zpool replace -f zroot \${missing_disk} \${replacement_disk}-part2
do
sleep 3
done
yes | sudo mkfs.fat -F 32 \${replacement_disk}-part1 >/dev/null
# Update /etc/fstab boot partition of new disk
sudo sed -i "/\$(printf '%s\|' \${existing_disk_uuid[@]} | sed 's/\\\|\$//')/! s|UUID=.*/boot|UUID=\$(sudo blkid -s UUID -o value \${replacement_disk}-part1) /boot|" /etc/fstab
sudo systemctl daemon-reload
heading 'Updated /etc/fstab with replacement boot partition'
## Update /etc/pacman.d/hooks/55-bootbackup_pre.hook
sudo sed -i "s|/dev/disk/by-id/.*-part1|\${replacement_disk}-part1|" /etc/pacman.d/hooks/55-bootbackup_pre.hook
heading 'Updated /etc/pacman.d/hooks/55-bootbackup_pre.hook boot partition reference'
## Update /opt/local/hooks/post-install-boot
sudo sed -i "/^for drive in/c\for drive in \$(printf '%s-part1 ' \${existing_disk_id[@]})" /opt/local/hooks/post-install-boot
heading 'Updated /opt/local/hooks/post-install-boot boot partition reference'
# efibootmgr
for boot in \$(mount | grep -w '/boot' | grep -w -v 'systemd-1' | awk '{print \$1}')
do
sudo umount \${boot}
done
sudo mount \${replacement_disk}-part1
sudo rsync -a /usr/lib/modules/\$(uname -r)/vmlinuz /boot/vmlinuz-linux
sudo zpool set cachefile=/etc/zfs/zpool.cache zroot
sudo sed -i "/^for drive in/c\for drive in \${replacement_disk} \$(echo \${existing_disk_id[@]})" /opt/local/bin/efistub
sudo /opt/local/bin/efistub
echo
# mkinitcpio
heading 'Regenerating initramfs'
sudo mkinitcpio -P
echo
# Clone boot partitions
sudo /opt/local/hooks/post-install-boot
heading 'Cloned boot drives'
echo
# Wait for resilver to complete
watch -d sudo zpool status
RECOVERY
fi
# System services
arch-chroot /mnt systemctl enable ${systemd_services[@]} \
systemd-resolved.service systemd-networkd.service sshd.service iptables.service \
libvirtd.service docker.service \
nginx.service \
fail2ban.service \
zfs.target zfs-import-cache.service zfs-mount.service zfs-import.target \
zfs-trim@zroot.timer zfs-scrub@zroot.timer \
seatd.service \
certbot-renew.timer || die "Unable to start systemd services"
# Generate fstab
for drive in ${installation_disks[@]}
do
mount ${drive}-part1 /mnt/boot
done
genfstab -U -p /mnt/boot | sed "s|vfat.*rw|vfat rw,x-systemd.idle-timeout=1min,x-systemd.automount,noauto,nofail|" >>/mnt/etc/fstab
sed -i '/UUID.*vfat/ s|/|/boot|' /mnt/etc/fstab
# Add zram to fstab
echo '/dev/zram0 none swap defaults 0 0' >>/mnt/etc/fstab
echo
# Reboot only if script succeeded
if /usr/bin/bash -c 'arch-chroot /mnt uname -a' | grep -q Linux
then
until ! mount | grep -q /mnt/boot
do
umount /mnt/boot
done
zfs snapshot zroot/ROOT@fresh-installation
zpool export -a
say as success in bold "Installer has completed and system drive has been unmounted"
say as success in bold "Boot into the new system, connect to a network and run $(tput smso)startup$(tput rmso) in the terminal"
say as success in bold "Rebooting..."
echo
reboot
else
die 'Something does not feel right'
fi