#!/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 sed -e '/^HOOKS/ s/filesystems/netconf tinyssh zfsencryptssh zfs &/' \ -e '/^HOOKS/ s/ fsck//' \ -e '/^HOOKS/ s/systemd/udev/' \ /mnt/etc/mkinitcpio.conf >/mnt/etc/mkinitcpio.conf.d/zz-hooks.conf 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/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 </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 </dev/null </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 macvtap 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 isolated 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 sed -e '/^HOOKS/ s/filesystems/netconf tinyssh zfsencryptssh zfs &/' \ -e '/^HOOKS/ s/ fsck//' \ -e '/^HOOKS/ s/systemd/udev/' \ /etc/mkinitcpio.conf >/etc/mkinitcpio.conf.d/zz-hooks.conf # 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 </dev/null </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