commit 36ad41a2fc9e93b03348b8b3c1a8f9842ad91a84 Author: myve Date: Sat Nov 22 23:15:27 2025 +0000 First commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..57e446d --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# MyveArch + +This repo is a compilation of installation scripts written in bash that install [Arch Linux](https://archlinux.org/) on any x86_64 EFI system. From a home server ([homelab.sh](homelab.sh)) to a laptop or desktop computer ([arch.sh](arch.sh)), these scripts are highly customizable and can be tailored to very specific needs. + +## :: Pre-installation +Acquire an installation ISO and burn it into a USB drive: +``` +dd bs=4M if=${/path/to/archlinux-version-x86_64.iso} of=${/dev/disk/by-id/usb-My_flash_drive} conv=fsync oflag=direct status=progress +``` + +## :: Usage +Download the script of choice and execute it in the terminal: + +``` +wget https://myvelabs.com/lab/archlinux/raw/branch/master/arch.sh +bash arch.sh +``` +OR +``` +curl --fail -s -L -O https://myvelabs.com/lab/archlinux/raw/branch/master/homelab.sh +bash homelab.sh +``` +OR +``` +git clone https://myvelabs.com/lab/archlinux.git +cd archlinux/ +bash arch.sh +``` + +## :: Installation +The following flags are available: +``` +-s, --ssh-key Authorized SSH public key (one entry per flag, enclosed in quotes) +-c, --cache Pacman cache server address +-k, --kernel Linux kernel (eg, linux, linux-lts, linux-hardened, linux-zen) +-f, --filesystem Choice of filesystem (eg, ext4, btrfs, zfs) +-o, --option Arch installation setup, if known +``` +Fill out the prompts and you'll have a full-fledged Arch Linux system up and running in no time. \ No newline at end of file diff --git a/arch.sh b/arch.sh new file mode 100755 index 0000000..ee3ee4d --- /dev/null +++ b/arch.sh @@ -0,0 +1,1755 @@ +#!/usr/bin/env bash +revision=1.0l +set -a +set -E + +# ZFS key +# zfsgpgkey=DDF7DB817396A49B2A2723F7403BD972F75D9D76 # archzfs +zfsgpgkey=D0F2AE55C1BF11A026D155813A658A95B8CFCC51 # myvezfs + +# 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 + +# 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 3) $(tput bold)) # bold yellow + 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 +} + +# 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 +else + die "No internet connectivity detected, plug in an ethernet cable or run ${reset}${green}iwd-connect${yellow} if using wifi and try again" +fi + +## +## functions start +## + +# Default systemd services +systemd_services+=(systemd-networkd.service systemd-resolved.service sshd.service iptables.service) + +# Repeat a command until it exits with 0 +function repeat +{ + until ${@} + do + sleep 3 + done +} + +# Pacstrap new root and generate fs tables +function pacstrapGenfstab +{ + local archpkgs="sudo openssh efibootmgr pacman-contrib \ + vim rsync pv git less openbsd-netcat \ + man-db bash-completion reflector \ + wireguard-tools systemd-resolvconf \ + ${linux_firmware[@]} \ + ${btrfs[@]} ${zfs[@]} \ + ${ucode} \ + ${grub[@]} \ + ${iwd} \ + ${yubikey[@]} \ + ${nvidia[@]} \ + ${bluetooth[@]}" + + # Delete unneccessary EFI files + rm -f /sys/firmware/efi/efivars/dump-* + for entry in $(efibootmgr | grep "Arch" | grep -o 'Boot....' | sed 's/Boot//') + do + efibootmgr --quiet -Bb ${entry} + done + + # Ensure pacman.conf settings are properly set + sed -e '/Color/c Color' \ + -e '/ParallelDownloads/c ParallelDownloads = 10' \ + -i /etc/pacman.conf + + # Add CacheServer + if [ ${cacheserver} ] + then + sed -e "/^\[core\]$\|^\[extra\]$/a CacheServer = ${cacheserver}" \ + -i /etc/pacman.conf + fi + + # Temporarily disable mkinitcpio install + ln -s -f /dev/null /etc/pacman.d/hooks/90-mkinitcpio-install.hook + + echo + say as heading "Installing Arch Linux base packages" + case ${filesystem} in + zfs) + # Add zfs repo + grep -q '\[myvezfs\]' /etc/pacman.conf || sed -i '/\[core\]/i [myvezfs]\ +Server = https://mirror.myvelabs.com/repo/$repo\ +Server = https://repo.myvelabs.com/$repo\n' /etc/pacman.conf + + # Add CacheServer + if [ ${cacheserver} ] + then + sed -e "/^\[myvezfs\]$/a CacheServer = ${cacheserver}" \ + -i /etc/pacman.conf + fi + + local zfslinux=$(pacman -Syi zfs-${linux_kernel} | grep "Depends On" | sed "s|.*${linux_kernel}=||" | awk '{print $1}') + + # Install zfs-compatible linux kernel + if [ ${zfslinux} = $(pacman -Si myvezfs/${linux_kernel} | grep "Version" | awk '{print $3}') ] + then + repeat pacstrap -K /mnt --ask 4 \ + base mkinitcpio iptables-nft ${linux_kernel} ${archpkgs} + else + repeat pacstrap -K /mnt base mkinitcpio iptables-nft + if [ ${cacheserver} ] + then + pacstrap -K -U /mnt ${cacheserver}/${linux_kernel}-${zfslinux}-x86_64.pkg.tar.zst ||\ + repeat pacstrap -K -U /mnt https://archive.archlinux.org/packages/l/${linux_kernel}/${linux_kernel}-${zfslinux}-x86_64.pkg.tar.zst + else + repeat pacstrap -K -U /mnt https://archive.archlinux.org/packages/l/${linux_kernel}/${linux_kernel}-${zfslinux}-x86_64.pkg.tar.zst + fi + repeat pacstrap -K /mnt --ask 4 ${archpkgs} + fi + + # Add archzfs repo + sed -e '/\[core\]/i [myvezfs]\ +Server = https://mirror.myvelabs.com/repo/$repo\ +Server = https://repo.myvelabs.com/$repo\n' \ + -i /mnt/etc/pacman.conf + + # Add archzfs repo keys + arch-chroot /mnt pacman-key -r ${zfsgpgkey} + arch-chroot /mnt pacman-key --lsign-key ${zfsgpgkey} + + # 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/ + + # Create zfs directory + mkdir -p /mnt/zfs/bin/ + + # Configure zfs for mkinitcpio + echo 'BINARIES+=(/usr/bin/zfs)' >/mnt/etc/mkinitcpio.conf.d/zz-binaries.conf + sed -e '/^HOOKS/ s/filesystems/zfs &/' \ + -e '/^HOOKS/ s/ fsck//' \ + /mnt/etc/mkinitcpio.conf >/mnt/etc/mkinitcpio.conf.d/zz-hooks.conf + + # Generate fstab + case ${arch} in + 1|2) + 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 + ;; + *) genfstab -U -p /mnt | grep -v zroot >>/mnt/etc/fstab + ;; + esac + + # Add systemd services + systemd_services+=(zfs.target zfs-import-cache.service zfs-mount.service zfs-import.target zfs-trim@zroot.timer zfs-scrub@zroot.timer) + ;; + btrfs|ext4) + repeat pacstrap -K /mnt --ask 4 \ + base mkinitcpio iptables-nft ${linux_kernel} ${archpkgs} + + # Generate fstab + genfstab -U -p /mnt >>/mnt/etc/fstab + + case ${filesystem} in + btrfs) echo 'BINARIES+=(/usr/bin/btrfs)' >/mnt/etc/mkinitcpio.conf.d/zz-binaries.conf + ;; + esac + ;; + esac + + # Ensure pacman.conf settings are properly set + sed -e '/Color/c Color' \ + -e '/ParallelDownloads/c ParallelDownloads = 10' \ + -i /mnt/etc/pacman.conf + + # Custom mkinitcpio.conf + echo 'MODULES_DECOMPRESS="yes"' >/mnt/etc/mkinitcpio.conf.d/zz-modules_decompress.conf + + # Add CacheServer + if [ ${cacheserver} ] + then + grep -q "^CacheServer" /etc/pacman.conf ||\ + sed -e "/^\[core\]$\|^\[extra\]$\|^\[myvezfs\]$/a CacheServer = ${cacheserver}" \ + -i /mnt/etc/pacman.conf + fi + + # Make custom directories + mkdir -p /mnt/etc/pacman.d/hooks/ /mnt/opt/local/{bin,hooks}/ + + # Manually configure mkinitcpio + ln -s -f /dev/null /mnt/etc/pacman.d/hooks/90-mkinitcpio-install.hook + + # Add hooks + hooks +} + +# Update mkinitcpio with encrypt hooks +function encryptHook +{ + case ${filesystem} in + zfs) + sed "s/block/& ${1}/" -i /mnt/etc/mkinitcpio.conf.d/zz-hooks.conf + # Pacman hook + install /dev/stdin /mnt/opt/local/hooks/mkinitcpio.conf <<- hook + #!/usr/bin/env bash + grep '^HOOKS' /etc/mkinitcpio.conf | sed -e "s/filesystems/zfs &/" -e "s/ fsck//" -e "s/block/& ${1}/" >/etc/mkinitcpio.conf.d/zz-hooks.conf + + sed -e "s|%PKGBASE%|${linux_kernel}|g" \\ + -e "s/^fallback/#&/g" \\ + -e "s/ 'fallback'//" \\ + /usr/share/mkinitcpio/hook.preset >/etc/mkinitcpio.d/${linux_kernel}.preset + hook + ;; + *) + grep '^HOOKS' /mnt/etc/mkinitcpio.conf | sed "s/block/& ${1}/" >/mnt/etc/mkinitcpio.conf.d/zz-hooks.conf + # Pacman hook + install /dev/stdin /mnt/opt/local/hooks/mkinitcpio.conf <<- hook + #!/usr/bin/env bash + grep '^HOOKS' /etc/mkinitcpio.conf | sed "s/block/& ${1}/" >/etc/mkinitcpio.conf.d/zz-hooks.conf + + sed -e "s|%PKGBASE%|${linux_kernel}|g" \\ + -e "s/^fallback/#&/g" \\ + -e "s/ 'fallback'//" \\ + /usr/share/mkinitcpio/hook.preset >/etc/mkinitcpio.d/${linux_kernel}.preset + hook + ;; + esac + + cat >/mnt/etc/pacman.d/hooks/85-mkinitcpio.hook <<- mkinitcpio + [Trigger] + Operation = Install + Operation = Upgrade + Type = Package + Target = mkinitcpio + + [Action] + Description = Fixing mkinitcpio.conf + When = PostTransaction + Exec = /opt/local/hooks/mkinitcpio.conf + mkinitcpio +} + +# Compile yubikey if arch package fails +function compileYubikey +{ + cd yubikey-full-disk-encryption + make install || die 'Yubikey full disk encryption (github) package installation failed' +} + +# Install yubikey inside new root +function chrootYubikey +{ + if [ -d ~/yubikey-full-disk-encryption ] + then + rsync -a ~/yubikey-full-disk-encryption /mnt/ + arch-chroot /mnt /usr/bin/bash <<- YUBIKEY + compileYubikey + rm -r /yubikey-full-disk-encryption + YUBIKEY + fi + + rsync -a /etc/ykfde.conf /mnt/etc/ + chmod 600 /mnt/etc/ykfde.conf + encryptHook ykfde + + echo + say in blue 'Copied yubikey configuration files' +} + +# Yubikey root encryption +function encryptRootYubikey +{ + yubikey=(yubikey-personalization) + + for package in yubikey-personalization yubikey-full-disk-encryption + do + if ! pacman -Q | grep -q -w ${package} + then + missing_yubikey_requirements+=(${package}) + fi + done + + if [ ${missing_yubikey_requirements} ] + then + yes | pacman -Sy ${missing_yubikey_requirements[@]} + echo + fi + + function ykfdeFormat + { + # Custom challenge slot + # sed -i '/YKFDE_CHALLENGE_SLOT/c YKFDE_CHALLENGE_SLOT="1"' /etc/ykfde.conf + + case ${decryptroot} in + [yY]) sed '/YKFDE_CHALLENGE=/c YKFDE_CHALLENGE="'"$(printf '%q' "${lukspass}")"'"' -i /etc/ykfde.conf ;; + [nN]) sed '/YKFDE_CHALLENGE_PASSWORD_NEEDED/c YKFDE_CHALLENGE_PASSWORD_NEEDED="1"' -i /etc/ykfde.conf ;; + esac + + say as heading "Encrypting ${rootpart} with yubikey HMAC encryption" + until printf '%s\n' "${lukspass}" "${lukspass}" | ykfde-format ${encryptopts} ${luks_header} ${rootpart} + do + sleep 3 + done + printf '%s\n' "${lukspass}" |\ + ykfde-open \ + -d ${rootpart} \ + -n cryptroot \ + -- ${luks_header} + } + + while true + do + # Try with yubikey-full-disk-encryption pacman package + ykfdeFormat + + if dmsetup ls | grep -q 'cryptroot' + then + yubikey+=(yubikey-full-disk-encryption) + echo + break + fi + + for package in git make + do + if ! pacman -Q | grep -q -w ${package} + then + make_yubikey_requirements+=(${package}) + fi + done + + echo + + say in blue 'Setting up yubikey' + if [ ${make_yubikey_requirements} ] + then + yes | pacman -Sy ${make_yubikey_requirements[@]} + echo + fi + + # Try yubikey full disk encryption again using latest git + cd ~/ + git clone https://github.com/agherzan/yubikey-full-disk-encryption.git + compileYubikey + ykfdeFormat + + if dmsetup ls | grep -q 'cryptroot' + then + echo + break + fi + + die 'Unable to encrypt root partition' + done +} + +# Root encryption +function encryptRootLuks +{ + say as heading "Encrypting ${rootpart}" + until printf '%s' "${lukspass}" | cryptsetup luksFormat -v ${encryptopts} ${luks_header} ${rootpart} + do + sleep 3 + done + printf '%s' "${lukspass}" | cryptsetup open -v ${rootpart} cryptroot ${luks_header} + + if dmsetup ls | grep -q 'cryptroot' + then + echo + else + die 'Unable to encrypt root partition' + fi +} + +# External boot encryption +function encryptBootLuks +{ + say as heading "Encrypting ${bootpart}" + until printf '%s' "${grubpass}" | cryptsetup luksFormat -v --type luks1 ${bootpart} + do + sleep 3 + done + printf '%s' "${grubpass}" | cryptsetup open -v ${bootpart} cryptboot + yes | mkfs.ext2 -q /dev/mapper/cryptboot + + if dmsetup ls | grep -q cryptboot + then + echo + else + die 'Unable to encrypt boot partition' + fi +} + +# Default cryptsetup options +encryptopts="--cipher aes-xts-plain64 \ + --key-size 256 \ + --iter-time 2000 \ + --hash sha512" + +# Generate keyfile for external boot +function generateKeyfile +{ + echo + say as heading 'Generating keyfile' + dd bs=1 count=256 if=/dev/random of=/mnt/crypto_keyfile.bin status=progress + chmod 000 /mnt/crypto_keyfile.bin + printf '%s' "${grubpass}" | cryptsetup luksAddKey ${bootpart} /mnt/crypto_keyfile.bin + + if [[ ${decryptroot} = [yY] ]] + then + case ${arch} in + 3) printf '%s' "${lukspass}" | cryptsetup luksAddKey ${rootpart} /mnt/crypto_keyfile.bin ;; + 6) printf '%s' "${lukspass}" | cryptsetup luksAddKey /mnt/boot/header.img /mnt/crypto_keyfile.bin ;; + esac + fi +} + +# Format partitions +function configureVolumes +{ + say as heading 'Configuring volumes' + + # Root + case ${filesystem} in + ext4) + yes | mkfs.ext4 ${rootvol} + mount -o noatime ${rootvol} /mnt + ;; + btrfs) + mkfs.btrfs -f ${rootvol} + btrfsopts="compress=zstd:2,noatime,space_cache=v2" + + # Create btrfs subvolumes + mount ${rootvol} /mnt + for subvol in @{,home,var{,/pkg,/log,/tmp,/qemu},opt} # ,snapshots{,/root,/home} + do + btrfs subvolume create /mnt/${subvol} + done + chattr +C /mnt/@var/qemu + chmod 1777 /mnt/@var/tmp + umount /mnt + + # Mount @root subvolume + mount --mkdir -o ${btrfsopts},subvol=@ ${rootvol} /mnt + # Mount home and home snapshots subvolumes + mount --mkdir -o ${btrfsopts},subvol=@home ${rootvol} /mnt/home + # mount --mkdir -o ${btrfsopts},subvol=@snapshots/home ${rootvol} /mnt/home/.snapshots + # mount --mkdir -o ${btrfsopts},subvol=@snapshots/root ${rootvol} /mnt/.snapshots + # Mount remaining subvolumes + mount --mkdir -o ${btrfsopts},subvol=@var/pkg ${rootvol} /mnt/var/cache/pacman/pkg + mount --mkdir -o ${btrfsopts},subvol=@var/log ${rootvol} /mnt/var/log + mount --mkdir -o ${btrfsopts},subvol=@var/tmp ${rootvol} /mnt/var/tmp + mount --mkdir -o ${btrfsopts},subvol=@var/qemu ${rootvol} /mnt/var/lib/libvirt/images + + mount --mkdir -o ${btrfsopts},subvol=@opt ${rootvol} /mnt/opt + mount --mkdir -o ${btrfsopts},subvol=/ ${rootvol} /mnt/btrfs + echo + ;; + zfs) + 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 \ + ${zpoolencryption} ${rootvol} + do + sleep 3 + 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=/var/log zroot/LOG + zfs create -o mountpoint=/var/tmp zroot/TMP + zfs create -o mountpoint=/var/cache/pacman/pkg zroot/PKG + zfs create -o mountpoint=/var/lib/libvirt/images zroot/QEMU + + # Verify zpool + zpool export zroot + zpool import -R /mnt zroot -N -d ${rootvol} + # Native encryption + case ${arch} in + 2) printf '%s' "${lukspass}" | zfs load-key zroot ;; + esac + zfs mount zroot/ROOT + zfs mount -a + ;; + esac + + # Boot volume + if [[ ${arch} = [3567] ]] + then + yes | mkfs.fat -F 32 ${efivol} + else + yes | mkfs.fat -F 32 ${bootvol} + fi + + mount --mkdir ${bootvol} /mnt/boot + + # EFI volume + if [[ ${arch} = [3567] ]] + then + mount --mkdir ${efivol} /mnt/boot/efi + grub+=(grub) + fi +} + +# Configure grub +function configureGrub +{ + sed '/GRUB_TIMEOUT=/c GRUB_TIMEOUT=3' -i /etc/default/grub + sed '/GRUB_ENABLE_CRYPTODISK/c GRUB_ENABLE_CRYPTODISK=y' -i /etc/default/grub + echo 'cryptboot '${bootpart}' /crypto_keyfile.bin luks' >>/etc/crypttab +} + +# Ask for system settings +function initialSetup +{ + echo + + # Username + say in blue 'Create a new user' + until [ ${username} ] + do + ask for username 'Username' + if [ -z ${username} ] + then + say as warning 'Username cannot be empty, try again' + fi + done + + # Hostname + say in blue '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 in blue "Set a password for ${username}" + 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 "${username}@${hostname}'s password has been saved" + echo + + # Encryption key + if [[ ${arch} = [2-7] ]] + then + say in blue '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 + fi + + # GRUB password + if [[ ${arch} = [3567] ]] + then + say in blue 'Create an encryption passphrase for the bootloader' + until [ "${grubpass}" = "${grubpass2}" -a "${grubpass}" -a "${#grubpass}" -ge 6 ] + do + ask for grubpass in secret 'Bootloader password' + ask for grubpass2 in secret 'Verify bootloader password' + echo + if [ -z "${grubpass}" ] + then + say as warning 'Password field cannot be empty, try again' + elif [ "${grubpass}" != "${grubpass2}" ] + then + say as warning 'Passwords did not match, try again' + elif [ "${#grubpass}" -lt 6 ] + then + say as warning 'Passphrase needs to be at least 6 characters' + fi + done + say as success 'Bootloader password has been saved' + echo + fi + + # Assign system drive + say in blue 'Identify the system drive from the list of available devices below' + lsblk -d -o name,model,size,mountpoint | grep -v 'archiso' + say in uline white dim '## SSD and HDD device format begins with "sd" or "hd" (sda, sdb, sd[*])' + say in uline white dim '## NVME and PCI device format is "nvme[*]n1" (nvme0n1, nvme1n1, nvme[*]n1)' + while true + do + ask for drive 'Installation device' + if [ ${drive} ] && lsblk -o name | grep -q -w ${drive} + then + break + else + lsblk -d -o name,model,size,mountpoint | grep -v 'archiso' + if [ -z ${drive} ] + then + say as warning 'Field cannot be empty, try again' + else + say as warning 'Invalid selection or drive not available, try again' + fi + fi + done + say as success "Installation drive set as $(tput smso)/dev/${drive}$(tput rmso)" + + # Assign detached boot drive (if applicable) + if [[ ${arch} = [67] ]] + then + echo + say in blue 'Identify the removable boot drive from the list of available devices below' + lsblk -d -o name,model,size,mountpoint | grep -v "archiso\|$drive" + say in uline white dim '## Removable drives usually begin with "sd" (sda, sdb, sd[*])' + while true + do + ask for external_drive 'Removable boot device' + if [ ${external_drive} ] && lsblk -o name | grep -v ${drive} | grep -q -w ${external_drive} + then + break + else + lsblk -d -o name,model,size,mountpoint | grep -v "archiso\|$drive" + if [ -z ${external_drive} ] + then + say as warning 'Field cannot be empty, try again' + elif [ ${external_drive} = ${drive} ] + then + say as warning 'Boot drive cannot be the same as the system drive, try again' + else + say as warning 'Invalid selection or drive not available, try again' + fi + fi + done + say as success "Detached boot drive set as $(tput smso)/dev/${external_drive}$(tput rmso)" + fi +} + +# Virtual machine defaults +function virtualMachineSetup +{ + if ls -l /dev/disk/* | grep -q VBOX + then + hostname=vbox + drive=sda + external_drive=sdb + else + hostname=qemu + if ls -l /dev/disk/* | grep -q virtio + then + drive=vda + elif ls -l /dev/disk/* | grep -q QEMU + then + drive=sda + fi + fi + + username=user + userpass=123 + lukspass=12345678 + grubpass=123456 + + echo + say in uline white dim 'Troubleshooting defaults loaded' +} + +function hooks +{ + # Common pacman.conf + install /dev/stdin /mnt/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' /etc/pacman.conf.pacnew >/etc/pacman.conf + rm /etc/pacman.conf.pacnew + fi + hook + + case ${filesystem} in + zfs) + # pacman.conf + sed -e "/Color\/c Color/a\\ + -e '/^\\\[core\\\]$/i [myvezfs]\\\\\\ +Server = https://mirror.myvelabs.com/repo/\$repo\\\\\\ +Server = https://repo.myvelabs.com/\$repo\\\n' \\\\" \ + -i /mnt/opt/local/hooks/pacman.conf + + cat >/mnt/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 + + # mkinitcpio.conf + install /dev/stdin /mnt/opt/local/hooks/mkinitcpio.conf <<- 'hook' + #!/usr/bin/env bash + grep '^HOOKS' /etc/mkinitcpio.conf | sed 's/filesystems fsck/zfs filesystems/' >/etc/mkinitcpio.conf.d/zz-hooks.conf + + sed -e "s|%PKGBASE%|${linux_kernel}|g" \\ + -e "s/^fallback/#&/g" \\ + -e "s/ 'fallback'//" \\ + /usr/share/mkinitcpio/hook.preset >/etc/mkinitcpio.d/${linux_kernel}.preset + hook + + cat >/mnt/etc/pacman.d/hooks/85-mkinitcpio.conf.hook <<- mkinitcpio + [Trigger] + Operation = Install + Operation = Upgrade + Type = Package + Target = mkinitcpio + + [Action] + Description = Fixing mkinitcpio.conf + When = PostTransaction + Exec = /opt/local/hooks/mkinitcpio.conf + mkinitcpio + ;; + *) + # pacman.conf hook + cat >/mnt/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 + ;; + esac +} + +## +## functions end +## + +## +## Static variables +## + +# Determine network controller +if lspci | grep -q 'Network controller' +then + if ! lspci | grep -q 'Ethernet controller' + then + iwd=iwd + systemd_services+=(iwd.service) + fi +fi + +# Determine ucode +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 + +# Detect bluetooth +if lsmod | grep -q bluetooth || [ -d /sys/class/bluetooth ] +then + bluetooth=(bluez bluez-utils) +fi + +# Read 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 + ;; + -o | --option ) + if [ "${2}" ] + then + arch=${2} + shift + fi + ;; + -c | --cache ) + if [ "${2}" ] + then + cacheserver=${2} + shift + fi + ;; + -k | --kernel ) + case "${2}" in + linux) linux_kernel=linux + shift + ;; + linux-lts) linux_kernel=linux-lts + shift + ;; + linux-hardened) linux_kernel=linux-hardened + shift + ;; + linux-zen) linux_kernel=linux-zen + shift + ;; + *) die "Invalid option for flag \"${1}\"" + ;; + esac + ;; + -f | --filesystem ) + case "${2}" in + ext4) + filesystem_choice=1 + shift + ;; + btrfs) + filesystem_choice=2 + shift + ;; + zfs) + filesystem_choice=3 + shift + ;; + *) die "Invalid option for flag \"${1}\"" + ;; + esac + ;; + -y | --noconfirm ) + noconfirm=yes + load_defaults=y + shred_installation_disk=n + shred_boot=n + begin_install=Y + ;; + -? | -h | --help ) + cat <<- help + Parameters: + -s, --ssh-key Add SSH public key (enclosed in quotes) + -o, --option Arch installation setup + -c, --cache Pacman cache server + -k, --kernel Linux kernel + -f, --filesystem Filesystem choice + -y, --noconfirm Skip choices + -?, -h, --help This help screen + help + exit 0 + ;; + * ) die "Unknown flag: ${1}" + ;; + esac + shift +done + +clear + +say in uline white dim "## rev ${revision}" +echo +say in bold blue "$(tput smso)Arch Linux Installer" +echo +say in yellow "Select a system configuration from the options below" +cat <<- menu + 1) Basic + 2) Full Disk Encryption with EFISTUB + 3) Full Disk Encryption with Secured GRUB + 4) Yubikey Full Disk Encryption with EFISTUB + 5) Yubikey Full Disk Encryption with Secured GRUB +menu + +if ls -l /dev/disk/* | grep -q 'virtio\|QEMU' +then + echo + until [[ ${arch} = [1-5] ]] + do + ask for arch in press ':' + echo + [[ ${arch} = [1-5] ]] || say as warning 'Invalid selection, type an option from 1 to 5' + done +else + cat <<- menu + 6) Full Disk Encryption with Detached LUKS Header and Secured GRUB + 7) Yubikey Encrypted Detached LUKS Header with Secured GRUB + menu + echo + until [[ ${arch} = [1-7] ]] + do + ask for arch in press ':' + echo + [[ ${arch} = [1-7] ]] || say as warning 'Invalid selection, type an option from 1 to 7' + done +fi + +# Prevent continuing unless yubikey is detected, only applies to yubikey setups +if [[ ${arch} = [457] ]] && ! dmesg | grep -q -i YubiKey +then + say as warning 'Yubikey not detected' + say as warning 'Insert one to continue...' + until dmesg | grep -q -i YubiKey + do + sleep 1 + done +fi + +clear + +say in uline white dim "## rev ${revision}" +echo -e -n "\n$(tput setab 7)$(tput setaf 4)$(tput bold)\t" +case ${arch} in + 1) echo 'Basic Installation' ;; + 2) echo 'Full Disk Encryption with EFISTUB' ;; + 3) echo 'Full Disk Encryption with Secured GRUB' ;; + 4) echo 'Yubikey Full Disk Encryption with EFISTUB' ;; + 5) echo 'Yubikey Full Disk Encryption with Secured GRUB' ;; + 6) echo 'Full Disk Encryption with Detached LUKS Header and Secured GRUB' ;; + 7) echo 'Yubikey Encrypted Detached LUKS Header with Secured GRUB' ;; +esac +echo "$(tput sgr0)" + +presskeytoresume + +# Initial setup +if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' +then + cacheserver=http://192.168.122.1:9090 + + case ${noconfirm} in + yes) + virtualMachineSetup + ;; + *) + echo + while true + do + ask for load_defaults in press yellow 'Would you like to load virtual machine troubleshooting defaults? (y/n)' + case ${load_defaults} in + [yY]) + virtualMachineSetup + break + ;; + [nN]) + initialSetup + break + ;; + *) + echo + say as warning 'Not a valid answer, type "y" or "n"' + ;; + esac + done + ;; + esac +else + initialSetup +fi + +# Linux firmware +if ls -l /dev/disk/* | grep -q VBOX +then + linux_firmware=(virtualbox-guest-utils) + systemd_services+=(vboxservice.service) +elif ls -l /dev/disk/* | grep -q 'virtio\|QEMU' +then + linux_firmware=(qemu-guest-agent spice-vdagent) + systemd_services+=(qemu-guest-agent.service) +else + # Detect needed firmwares + 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 + +# Choose linux kernel +echo +say in blue "Select a linux kernel:" +cat <<- menu + 1) linux + 2) linux-lts + 3) linux-hardened + 4) linux-zen +menu +until [ ${linux_kernel} ] +do + ask for linux_kernel_choice in press ':' + case ${linux_kernel_choice} in + 1) linux_kernel=linux ;; + 2) linux_kernel=linux-lts ;; + 3) linux_kernel=linux-hardened ;; + 4) linux_kernel=linux-zen ;; + *) echo + say as warning 'Invalid selection, type an option from 1 to 4' + ;; + esac +done +echo +say as success "You have chosen $(tput smso)${linux_kernel}$(tput rmso) for a kernel" + +# GPU firmware +if lspci | grep VGA | grep -q QXL +then + linux_firmware+=(xf86-video-qxl) +fi +if lspci | grep VGA | grep -q NVIDIA +then + case ${linux_kernel} in + linux) nvidia=(nvidia-open nvidia-settings) ;; + linux-lts) nvidia=(nvidia-lts nvidia-settings) ;; + *) nvidia=(nvidia-dkms nvidia-settings) ;; + esac +fi + +# Choose filesystem +echo +say in blue "Select a filesystem:" +cat <<- menu + 1) ext4 + 2) btrfs +menu +if modprobe zfs >/dev/null 2>&1 +then + cat <<- MENU + 3) zfs + MENU +fi +until [[ ${filesystem_choice} = [1-3] ]] +do + ask for filesystem_choice in press ':' + if ! [[ ${filesystem_choice} = [1-3] ]] + then + echo + say as warning 'Invalid selection, type an option from 1 to 3' + fi +done +case ${filesystem_choice} in + 1) + filesystem=ext4 + rootflags='quiet rw' + ;; + 2) + filesystem=btrfs + rootflags='rootflags=subvol=@ quiet rw' + btrfs=(btrfs-progs timeshift cronie) + systemd_services+=(cronie.service) + ;; + 3) + filesystem=zfs + rootflags='zfs=bootfs quiet rw' + zfs=(zfs-${linux_kernel} zfs-utils) + ;; +esac +echo +say as success "You have chosen $(tput smso)${filesystem}$(tput rmso) for a filesystem" + +# Option to automatically decrypt / using a keyfile, only applies to encrypted GRUB/yubikey 1FA setups +if [[ ${arch} = [3-7] ]] +then + echo + if [[ ${arch} = [457] ]] + then + say in uline white dim '## Root will be decrypted as long as yubikey is inserted when prompted' + fi + until [[ ${decryptroot} = [yYnN] ]] + do + ask for decryptroot in press yellow "Would you like to automatically decrypt the root partition upon boot? (y/n)" + if ! [[ ${decryptroot} = [yYnN] ]] + then + echo + say as warning 'Not a valid answer, type "y" or "n"' + fi + done +fi + + # Skip confirmations if flag is on + if [ -z ${noconfirm} ] + then + +echo +# Device shredding +until [[ ${shred_installation_disk} = [yYnN] ]] +do + ask for shred_installation_disk in press yellow "Would you like to shred your installation drive? (y/n)" + + if ! [[ ${shred_installation_disk} = [yYnN] ]] + then + echo + say as warning 'Not a valid answer, type "y" or "n"' + fi +done + +if [[ ${arch} = [67] ]] +then + echo + until [[ ${shred_boot} = [yYnN] ]] + do + if [[ ${shred_installation_disk} = [yY] ]] + then + ask for shred_boot in press yellow "Would you also like to shred your removable drive? (y/n)" + else + ask for shred_boot in press yellow "Would you like to shred your removable drive? (y/n)" + fi + if ! [[ ${shred_boot} = [yYnN] ]] + then + echo + say as warning 'Not a valid answer, type "y" or "n"' + fi + done +fi +echo + +if [[ ${shred_installation_disk} = [nN] && ${shred_boot} = [nN] ]] || [[ ${arch} != [67] && ${shred_installation_disk} = [nN] ]] +then + say in yellow 'Disk shredding skipped' + echo + +elif [[ ${shred_installation_disk} = [yY] || ${shred_boot} = [yY] ]] +then + say in blue "Select an overwrite source:" + cat <<- menu + 1) zero + 2) urandom + menu + until [[ ${disk_destroyer} = [12] ]] + do + ask for disk_destroyer in press ':' + if ! [[ ${disk_destroyer} = [12] ]] + then + echo + say as warning 'Invalid selection, type an option from 1 to 2' + fi + done + echo + + case ${disk_destroyer} in + 1) dd=zero ;; + 2) dd=urandom ;; + esac +fi + +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 'Aborting installation' + say as warning 'Installer script stopped' + echo + exit 1 +fi + + fi # Skip disk shredding + +# Unmount if mounted +for mounted in /mnt/boot /mnt +do + if mount | grep -q -w ${mounted} + then + umount -R ${mounted} || die 'A drive or partition is already mounted on /mnt, unable to proceed' + fi +done + +# Export existing zpools +zpool export -a >/dev/null 2>&1 + +# Close opened luks containers +for mounted in cryptroot cryptboot +do + if dmsetup ls | grep -q -w ${mounted} + then + cryptsetup close "/dev/mapper/${mounted}" || die 'LUKS container already exists, unable to proceed' + fi +done + +# Grab disk-id of ${drive} +installation_disk=$(ls -l /dev/disk/by-id/* | grep "${drive}$" | grep 'wwn\|nvme-uuid\|nvme-nvme\|nvme-eui\|QEMU\|virtio\|VBOX' | awk '{print $9}' | head -1) +external_boot=$(ls -l /dev/disk/by-id/* | grep "${external_drive}$" | grep 'wwn\|nvme-uuid\|nvme-nvme\|nvme-eui\|QEMU\|virtio\|VBOX\|usb' | awk '{print $9}' | head -1) + +# Exit if disk IDs are null +if [ -z ${installation_disk} ] +then + die 'Ensure installation disk has a valid ID' +fi +if [ ${external_drive} ] && [ -z ${external_boot} ] +then + die 'Ensure external boot disk has a valid ID' +fi + +echo + +# Shred installation drive +blkdiscard --quiet --force --secure ${installation_disk} >/dev/null 2>&1 +wipefs --quiet --force --all ${installation_disk} + +if [[ ${shred_installation_disk} = [yY] ]] +then + say as heading "Shredding ${installation_disk}" + dd if=/dev/${dd} of=${installation_disk} bs=4M status=progress + echo 3 >/proc/sys/vm/drop_caches + echo +fi + +# Shred external boot drive +if [[ ${arch} = [67] ]] +then + wipefs --quiet --force --all ${external_boot} +fi +if [[ ${shred_boot} = [yY] ]] +then + say as heading "Shredding ${external_boot}" + dd if=/dev/${dd} of=${external_boot} bs=4M status=progress + echo 3 >/proc/sys/vm/drop_caches + say as success 'Device shredded' + echo +fi + +case ${arch} in + 1) # Basic setup + parted --script --align=optimal ${installation_disk} \ + mklabel gpt \ + mkpart boot 1MiB 300MiB \ + mkpart root 300MiB 100% \ + set 1 esp on + partprobe ${installation_disk} + + export {efivol,bootvol}=${installation_disk}-part1 + export {rootpart,rootvol}=${installation_disk}-part2 + + configureVolumes + pacstrapGenfstab + + echo + say as heading 'Configuring efistub' + efibootmgr --disk ${installation_disk} --part 1 --create --label "Arch Linux (${hostname})" --loader "/vmlinuz-${linux_kernel}" --unicode "initrd=\initramfs-${linux_kernel}.img root=${rootpart} ${rootflags}" + ;; + + 2|4) # FDE with EFISTUB + parted --script --align=optimal ${installation_disk} \ + mklabel gpt \ + mkpart boot 1MiB 300MiB \ + mkpart root 300MiB 100% \ + set 1 esp on + partprobe ${installation_disk} + + export {efivol,bootvol}=${installation_disk}-part1 + rootpart=${installation_disk}-part2 + rootvol=/dev/mapper/cryptroot + + case ${arch} in + 2) + case ${filesystem} in + zfs) + zpoolencryption="-O encryption=aes-256-gcm \ + -O keyformat=passphrase \ + -O keylocation=prompt" + rootvol=${installation_disk}-part2 + ;; + *) encryptRootLuks ;; + esac + ;; + 4) encryptRootYubikey ;; + esac + + configureVolumes + pacstrapGenfstab + + case ${arch} in + 2) + echo + say as heading 'Configuring efistub' + case ${filesystem} in + zfs) efibootmgr --disk ${installation_disk} --part 1 --create --label "Arch Linux (${hostname})" --loader "/vmlinuz-${linux_kernel}" --unicode "initrd=\initramfs-${linux_kernel}.img ${rootflags}" + ;; + btrfs|ext4) + encryptHook encrypt + efibootmgr --disk ${installation_disk} --part 1 --create --label "Arch Linux (${hostname})" --loader "/vmlinuz-${linux_kernel}" --unicode "initrd=\initramfs-${linux_kernel}.img cryptdevice=${rootpart}:cryptroot root=/dev/mapper/cryptroot ${rootflags}" + ;; + esac + ;; + 4) + chrootYubikey + echo + say as heading 'Configuring efistub' + efibootmgr --disk ${installation_disk} --part 1 --create --label "Arch Linux (${hostname})" --loader "/vmlinuz-${linux_kernel}" --unicode "initrd=\initramfs-${linux_kernel}.img cryptdevice=${rootpart}:cryptroot root=/dev/mapper/cryptroot ${rootflags}" + ;; + esac + ;; + + 3|5) # FDE with Secured GRUB + parted --script --align=optimal ${installation_disk} \ + mklabel gpt \ + mkpart efi 1MiB 40MiB \ + mkpart boot 40MiB 300MiB \ + mkpart root 300MiB 100% \ + set 1 esp on + partprobe ${installation_disk} + + efivol=${installation_disk}-part1 + bootpart=${installation_disk}-part2 + rootpart=${installation_disk}-part3 + rootvol=/dev/mapper/cryptroot + bootvol=/dev/mapper/cryptboot + + case ${arch} in + 3) encryptRootLuks ;; + 5) encryptRootYubikey ;; + esac + + encryptBootLuks + configureVolumes + generateKeyfile + pacstrapGenfstab + + case ${arch} in + 3) + encryptHook encrypt + if [[ ${decryptroot} = [yY] ]] + then + echo "FILES=(/crypto_keyfile.bin)" >/mnt/etc/mkinitcpio.conf.d/zz-files.conf + fi + ;; + 5) + chrootYubikey + echo "MODULES=(loop)" >/mnt/etc/mkinitcpio.conf.d/zz-modules.conf + ;; + esac + + sed "/^GRUB_CMDLINE_LINUX_DEFAULT/c GRUB_CMDLINE_LINUX_DEFAULT=\"cryptdevice=${rootpart}:cryptroot root=/dev/mapper/cryptroot ${rootflags}\"" \ + -i /mnt/etc/default/grub + grep -q "${rootpart}" /mnt/etc/default/grub || die 'Grub cfg generation failed' + + arch-chroot /mnt /usr/bin/bash <<- grub + configureGrub + grub + ;; + + 6|7) # FDE with detached luks header + parted --script --align=optimal ${external_boot} \ + mklabel gpt \ + mkpart efi 1MiB 40MiB \ + mkpart boot 40MiB 300MiB \ + mkpart none 300MiB 100% \ + set 1 esp on + partprobe ${external_boot} + + rootpart=${installation_disk} + efivol=${external_boot}-part1 + bootpart=${external_boot}-part2 + rootvol=/dev/mapper/cryptroot + bootvol=/dev/mapper/cryptboot + luks_header=--header=header.img + + case ${arch} in + 6) encryptRootLuks ;; + 7) encryptRootYubikey ;; + esac + + encryptBootLuks + configureVolumes + mv header.img /mnt/boot + generateKeyfile + pacstrapGenfstab + + # Mkinitcpio files + cpiofiles=(/boot/header.img) + + case ${arch} in + 6) + sed -e '/for cryptopt in/i\\n local headerFlag=false' \ + -e '/case ${cryptopt} in/a\ + header)\ + cryptargs="${cryptargs} --header /boot/header.img"\ + headerFlag=true\ + ;;' \ + -e 's/cryptsetup isLuks/$headerFlag || &/' \ + -i /mnt/usr/lib/initcpio/hooks/encrypt + + encryptHook encrypt + + # encrypt hook + install /dev/stdin /mnt/opt/local/hooks/encrypt-initcpio <<- 'hook' + #!/usr/bin/env bash + if [ -f /usr/lib/initcpio/hooks/encrypt.pacnew ] + then + sed -e '/for cryptopt in/i\\n local headerFlag=false' \ + -e '/case ${cryptopt} in/a\ + header)\ + cryptargs="${cryptargs} --header /boot/header.img"\ + headerFlag=true\ + ;;' \ + -e 's/cryptsetup isLuks/$headerFlag || &/' \ + -i /usr/lib/initcpio/hooks/encrypt.pacnew + mv /usr/lib/initcpio/hooks/encrypt.pacnew /usr/lib/initcpio/hooks/encrypt + fi + hook + cat >/mnt/etc/pacman.d/hooks/85-encrypt.hook <<- encrypt + [Trigger] + Operation = Install + Operation = Upgrade + Type = Package + Target = cryptsetup + + [Action] + Description = Fixing encrypt hook for detached headers + When = PostTransaction + Exec = /opt/local/hooks/encrypt-initcpio + encrypt + + if [[ ${decryptroot} = [yY] ]] + then + cpiofiles+=(/crypto_keyfile.bin) + fi + ;; + 7) + chrootYubikey + sed '/YKFDE_LUKS_OPTIONS/c YKFDE_LUKS_OPTIONS="--header=/boot/header.img"' -i /mnt/etc/ykfde.conf + ;; + esac + + # mkinitcpio.conf + echo "FILES=(${cpiofiles[@]})" >/mnt/etc/mkinitcpio.conf.d/zz-files.conf + echo "MODULES=(ext2 loop)" >/mnt/etc/mkinitcpio.conf.d/zz-modules.conf + + sed "/^GRUB_CMDLINE_LINUX_DEFAULT/c GRUB_CMDLINE_LINUX_DEFAULT=\"cryptdevice=${rootpart}:cryptroot:header root=/dev/mapper/cryptroot ${rootflags}\"" \ + -i /mnt/etc/default/grub + grep -q "${rootpart}" /mnt/etc/default/grub || die 'Grub cfg generation failed' + + arch-chroot /mnt /usr/bin/bash <<- grub + configureGrub + grub + ;; +esac + +unset lukspass grubpass + +# Re-activate mkinitcpio +rm -f /mnt/etc/pacman.d/hooks/90-mkinitcpio-install.hook +sed -e "s|%PKGBASE%|${linux_kernel}|g" \ + -e "s/^fallback/#&/g" \ + -e "s/ 'fallback'//" \ +/mnt/usr/share/mkinitcpio/hook.preset >/mnt/etc/mkinitcpio.d/${linux_kernel}.preset +rsync -a /mnt/usr/lib/modules/*/vmlinuz /mnt/boot/vmlinuz-${linux_kernel} + +# Copy some networking files +ln -s -f /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf +rsync -a /etc/systemd/network/20-ethernet.network /mnt/etc/systemd/network/ + +if [ ${iwd} ] +then + rsync -a /usr/local/bin/iwd-connect /mnt/opt/local/bin/ + chmod +x /mnt/opt/local/bin/iwd-connect + + mkdir -p /mnt/var/lib/iwd /mnt/etc/iwd + + rsync -a /var/lib/iwd/*.psk /mnt/var/lib/iwd/ + # sed -i '/^Passphrase=/d' /mnt/var/lib/iwd/*.psk + + cat >/mnt/etc/iwd/main.conf <<- iwd + [General] + EnableNetworkConfiguration=true + iwd +fi + +# chroot setup +arch-chroot /mnt /usr/bin/bash <<- chroot || die "chroot setup failed" +curl --fail --silent https://git.myvelabs.com/lab/archlinux/raw/branch/master/functions/chroot -o /tmp/chroot +/usr/bin/bash /tmp/chroot +chroot + +# System services +arch-chroot /mnt systemctl enable ${systemd_services[@]} || die "Unable to start systemd services" + +# SSH Authentication +if [ -f sshkeys ] +then + cat sshkeys >/mnt/home/${username}/.ssh/authorized_keys +fi + +# DE installer +install /dev/stdin /mnt/opt/local/bin/desktop <<- desktop +#!/usr/bin/env bash +set -e +if curl --fail --silent https://git.myvelabs.com/lab/archlinux/raw/branch/master/functions/desktop -o /tmp/desktop +then + /usr/bin/bash /tmp/desktop \${@} + sudo rm -f \${0} +else + exit 1 +fi +desktop + +# BTRFS fresh snapshot +case ${filesystem} in + btrfs) + arch-chroot /mnt /usr/bin/bash <<- "timeshift" + # Take fresh snapshots + timeshift --btrfs >/dev/null + timeshift --create --comments "Fresh install" >/dev/null + sed \ + -e '/stop_cron_emails/ s/false/true/' \ + -e '/schedule_monthly/ s/false/true/' \ + -e '/schedule_weekly/ s/false/true/' \ + -e '/schedule_daily/ s/false/true/' \ + -e '/schedule_hourly/ s/false/true/' \ + -e '/schedule_boot/ s/true/false/' \ + -e '/count_monthly/ s/: ".*",/: "3",/' \ + -e '/count_weekly/ s/: ".*",/: "4",/' \ + -e '/count_daily/ s/: ".*",/: "5",/' \ + -e '/count_hourly/ s/: ".*",/: "6",/' \ + -i /etc/timeshift/timeshift.json + echo -e '\e[1;33mSnapshots taken\e[0m\n' + timeshift + ;; +esac + +# Reboot only if script succeeded +if arch-chroot /mnt uname -a | grep -q Linux +then + for mounted in /mnt/boot /mnt + do + umount -R ${mounted} + done + case ${filesystem} in + zfs) + zfs snapshot zroot/ROOT@fresh-installation + zpool export -a + ;; + esac + 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 install a DE by running $(tput smso)desktop$(tput rmso) in the terminal" + say as success in bold "Rebooting..." + echo + reboot +else + die 'Something does not feel right' +fi diff --git a/functions/chroot b/functions/chroot new file mode 100755 index 0000000..ac4a272 --- /dev/null +++ b/functions/chroot @@ -0,0 +1,728 @@ +#!/usr/bin/env bash +revision=0.1a +set -a +set -E +echo + +# Environment variables +tee -a /etc/environment >/dev/null <<- environment +EDITOR=vim +SUDO_EDITOR=vim +environment + +# Global bashrc +tee -a /etc/skel/.bashrc >/dev/null <<- 'bashglobal' + +# Source bash functions +if [ -d ~/.local/functions ] +then + for file in $(find ~/.local/functions -type f) + do + . ${file} + done +fi + +# Add local functions folder to path +export PATH=${PATH}:${HOME}/.local/bin:/zfs/bin:/opt/local/bin +export SUDO_PROMPT=$'\a'"$(tput rev)[sudo] password for %p:$(tput sgr0)"' ' + +# Colored prompts +alias diff='diff --color=auto' +alias ip='ip -color=auto' +export LESS='-R --use-color -Dd+r$Du+b$' + +# Adjust terminal upon window resize +shopt -s checkwinsize + +# Auto cd into directory +shopt -s autocd + +# Enable tab complete for sudo +complete -c -f sudo + +# Ignore duplicate and whitespace history entries +export HISTCONTROL=ignoreboth + +# +# ~/.bash_aliases +# + +# ZFS/btrfs +alias zfs='sudo zfs' +alias zpool='sudo zpool' +alias btrfs='sudo btrfs' + +# Shutdown reboot +alias poweroff='sudo poweroff' +alias reboot='sudo reboot' + +# Text editors +alias v='vim' +alias sv='sudo vim' + +# Clear bash history +alias clearhistory='rm ${HISTFILE}; history -c -w' + +# Miscellanous pacman +alias orphans='sudo pacman -Rcns $(pacman -Qtdq)' +alias unlockpacman='sudo rm /var/lib/pacman/db.lck && sudo pacman -Syyu' + +# Rsync +alias rsync='rsync -v -h --progress --info=progress2 --partial --append-verify' +# --log-file= +# --remove-source-files + +# +# ~/.bash_functions +# + +# Pacman tools +function installer +{ + /opt/local/bin/cacheserver + sudo pacman -S ${@} + echo +} + +function uninstall +{ + sudo pacman -Rcns ${@} + echo +} + +function mirrors +{ + echo + sudo reflector --country CA,US --age 24 --latest 20 --protocol https --fastest 25 --sort rate --save /etc/pacman.d/mirrorlist + echo + cat /etc/pacman.d/mirrorlist + echo +} + +function syur +{ + /opt/local/bin/syu && + reboot +} + +function syup +{ + /opt/local/bin/syu && + poweroff +} + +# Update bash +function update-bash +{ + vim ~/.bashrc && + source ~/.bashrc +} +bashglobal + +# Root bashrc +rsync -a /etc/skel/.bashrc ~/ +mkdir -p ~/.local/functions/ +cat > ~/.local/functions/bashrc <<- 'bashrc' +#!/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 +bashrc + +# Locale +sed '/#en_US.UTF-8 UTF-8/ s/#//' -i /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 </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 BTRFS = /usr/bin/btrfs, /usr/bin/timeshift, /usr/bin/timeshift-gtk, /usr/bin/timeshift-launcher +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 ARCHISO = /opt/local/bin/mkairgap, /opt/local/bin/mkiso +Cmnd_Alias PACMAN = /usr/bin/pacman -Sy +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, BTRFS, QEMU, FAIL2BAN, ARCHISO, PACMAN, IPTABLES, MISC +WHEEL + +install -m 0440 /dev/stdin /etc/sudoers.d/.zz-NOPASSWD <<- NOPASSWD +Defaults:${username} !authenticate +NOPASSWD + +case ${filesystem} in + zfs) + # ZFS setup + zpool set cachefile=/etc/zfs/zpool.cache zroot + zgenhostid $(hostid) + + # ZFS files + touch /zfs/snapshot-syu + chown ${username}:users /zfs/snapshot-syu + + # 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 + + # Pre and Post update backup hooks + cat >/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/snapshot-syu)' + AbortOnFail + pre + cat >/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 /boot; rsync -a --mkpath --delete /boot/ "/.boot/\$(cat /zfs/snapshot-syu)_pre"/' + AbortOnFail + pre + cat >/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/snapshot-syu)_post"/' + post + cat >/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/snapshot-syu)' + post + + # Custom pacman wrapper + install /dev/stdin /opt/local/bin/syu <<- syu + #!/usr/bin/env bash + set -e + mirrorlist= + + # Enable or disable pacman cache server + /opt/local/bin/cacheserver + + # Fetch latest mirrors + sudo curl --fail --silent \${mirrorlist} -o /etc/pacman.d/mirrorlist + + # Record current time + echo \$(date "+%Y-%m-%d-%H:%M:%S") >/zfs/snapshot-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 -Sy >/dev/null + + # Check zfs-linux kernel dependency + zfslinux=\$(pacman -Si zfs-${linux_kernel} | grep "Depends On" | sed "s|.*${linux_kernel}=||") + linux=\$(pacman -Si ${linux_kernel} | grep "Version" | awk '{print \$3}') + if [ \${zfslinux} != \${linux} ] + then + archzfs="--ignore zfs-utils,zfs-${linux_kernel},${linux_kernel}" + fi + + # Update archlinux-keyring first + if [[ \${newpkg[@]} =~ "archlinux-keyring" ]] + then + sudo pacman --ask 4 -S archlinux-keyring + echo + fi + + # Perform update if dependencies are satisfied + if sudo pacman --ask 4 -Syu \${archzfs} --needed + then + echo + sudo pacdiff + exit 0 + fi + fi + syu + ;; + *) + case ${filesystem} in + btrfs) + # Pre and post update backup hooks + cat >/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] + Depends = timeshift + Description = Creating pre root snapshot... + When = PreTransaction + Exec = /usr/bin/bash -c 'timeshift --create --comments "pre_\$(date "+%Y-%m-%d-%H:%M:%S")" >/dev/null' + AbortOnFail + pre + cat >/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 'rsync -a --mkpath --delete --exclude 'header.img' /boot/ "/.boot/\$(date "+%Y-%m-%d-%H:%M:%S")_pre"/' + AbortOnFail + pre + cat >/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 --exclude 'header.img' /boot/ "/.boot/\$(date "+%Y-%m-%d-%H:%M:%S")_post"/' + post + cat >/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] + Depends = timeshift + Description = Creating post root snapshot... + When = PostTransaction + Exec = /usr/bin/bash -c 'timeshift --create --comments "post_\$(date "+%Y-%m-%d-%H:%M:%S")" >/dev/null' + post + + install /dev/stdin /usr/local/bin/timeshift-gui <<- 'timeshift' + #!/bin/sh + pkexec env WAYLAND_DISPLAY="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" XDG_RUNTIME_DIR=/run/user/0 timeshift-launcher + timeshift + ;; + esac + + # Custom pacman wrapper + install /dev/stdin /opt/local/bin/syu <<- syu + #!/usr/bin/env bash + set -e + mirrorlist= + + # Enable or disable pacman cache server + /opt/local/bin/cacheserver + + # Fetch latest mirrors + sudo curl --fail --silent \${mirrorlist} -o /etc/pacman.d/mirrorlist + + # Check for new packages and continue if found + newpkg+=(\$(checkupdates --nocolor | awk '{print \$1}')) + if [ "\${newpkg}" ] + then + # Update archlinux-keyring first + if [[ \${newpkg[@]} =~ "archlinux-keyring" ]] + then + sudo pacman --ask 4 -S archlinux-keyring + echo + fi + + # Perform update if dependencies are satisfied + if sudo pacman --ask 4 -Syu --needed + then + echo + sudo pacdiff + exit 0 + fi + fi + syu + ;; +esac +echo + +# /opt/local/bin/cacheserver +install /dev/stdin /opt/local/bin/cacheserver <<- 'cacheserver' +#!/usr/bin/env bash +set -e + +# Enable cacheserver if active +cacheserver= +port= +scheme= + +if [ -z ${cacheserver} ] +then + exit 1 +fi + +if nc -z -4 -w 3 ${cacheserver} ${port:-80} >/dev/null 2>&1 +then + if grep -q "CacheServer" /etc/pacman.conf + then + sudo sed "/CacheServer/ s/^\(#\)*//g" -i /etc/pacman.conf + else + sudo sed "/^\[core\]$\|^\[extra\]$\|^\[myvezfs\]$/a CacheServer = ${scheme:-http}://${cacheserver}:${port:-80}" -i /etc/pacman.conf + fi +else + sudo sed "/CacheServer/ s/^/#/g" -i /etc/pacman.conf +fi +cacheserver +if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' +then + sed -e "/^cacheserver=/c cacheserver=192.168.122.1" \ + -e "/^port=/c port=9090" \ + -i /opt/local/bin/cacheserver + /opt/local/bin/cacheserver +fi + +# mkinitcpio +say as heading 'Regenerating cpio image' +mkinitcpio -P +echo + +# Install GRUB +if [[ ${arch} = [3567] ]] +then + say as heading 'Installing GRUB' + grub-install --target=x86_64-efi --bootloader-id="Arch Linux (${hostname})" --efi-directory=/boot/efi --recheck || die 'Grub installation failed' + grub-mkconfig -o /boot/grub/grub.cfg + echo +fi + +# Sysctl custom settings +cat >/etc/sysctl.d/99-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 +vm.swappiness=10 +vm.vfs_cache_pressure=50 +SYSCTL + +# iptables +cat >/etc/iptables/userinput.rules <<- IPTABLES +## User input + +# SSH port +-A INPUT -p tcp -m tcp --dport ${port:-22} -j ACCEPT -m comment --comment "SSH 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 +echo '/dev/zram0 none swap defaults 0 0' >>/etc/fstab + +# Configure ssh and ssh_config.d/10-global.conf +ssh-keygen -q \ + -t ed25519 \ + -P "" \ + -C "${USER}@${hostname}" \ + -f ~/.ssh/id_ed25519 +mkdir ~/.ssh/sockets/ +cat >/etc/ssh/sshd_config.d/10-sshd.conf <<- sshd +PermitRootLogin no +PasswordAuthentication no +AuthenticationMethods publickey +sshd +cat >/etc/ssh/ssh_config.d/10-global.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.* + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null + LogLevel Error +sshconfig + +# Polkit +mkdir -p /etc/polkit-1/rules.d/ +cat >/etc/polkit-1/rules.d/49-nopasswd_global.rules <<- 'polkit' +/* Allow members of the wheel group to execute any actions + * without password authentication, similar to "sudo NOPASSWD:" + */ +polkit.addRule(function(action, subject) { + if (subject.isInGroup("wheel")) { + return polkit.Result.YES; + } +}); +polkit + +# Persistent journal logging +mkdir -p /etc/systemd/journald.conf.d/ +cat >/etc/systemd/journald.conf.d/zz-journald.conf <<- eof +[Journal] +Storage=persistent +eof + +# makepkg +mkdir -p /etc/makepkg.conf.d/ +cat >/etc/makepkg.conf.d/zz-makepkg.conf <<- makepkg +PKGEXT=".pkg.tar" +MAKEFLAGS="--jobs=$(nproc)" +COMPRESSZST=(zstd -c -T0 --auto-threads=logical -) +makepkg + +# Virtual machine settings +if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' +then + mv /etc/sudoers.d/.zz-NOPASSWD /etc/sudoers.d/zz-NOPASSWD +fi + +## +## Hooks +## + +# paccache +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/#//' -i /etc/locale.gen.pacnew + mv /etc/locale.gen.pacnew /etc/locale.gen + 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 + +# iptables +cat >/etc/pacman.d/hooks/100-iptables.rules.hook </etc/iptables/iptables.rules + rm /etc/iptables/iptables.rules.pacsave +fi +hook + +su ${username} <<- "user" +curl --fail --silent https://git.myvelabs.com/lab/archlinux/raw/branch/master/functions/user -o /tmp/user +bash /tmp/user +user diff --git a/functions/desktop b/functions/desktop new file mode 100755 index 0000000..b419e35 --- /dev/null +++ b/functions/desktop @@ -0,0 +1,670 @@ +#!/usr/bin/env bash +revision='0.1f (feb 24/25)' +set -a +set -E + +# Exit function +trap '[ "${?}" -ne 77 ] || exit 77' ERR +function die +{ + if [ ${@} ] + then + echo + echo -e "\e[1;31mError encountered for the following reason:\e[0m + + \e[33m${@}\e[0m + +\e[1;31mScript aborted...\e[0m" + echo + else + echo + echo -e '\e[1;31mError encountered, script aborted...\e[0m' + echo + fi + exit 77 +} + +# 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 or run \e[32miwd-connect\e[33m if using wifi and try again' +fi + +totalde=21 +cat <<- menu +$(tput setaf 5 bold) +Select a desktop$(tput sgr0) + $(tput smul setaf 7 dim)# none$(tput sgr0) + 0) none + $(tput smul setaf 7 dim)# wayland$(tput sgr0) + 1) sway + 2) hyprland + 3) qtile-wayland + 4) river + 5) plasma-wayland + 6) gnome-wayland + 7) cosmic + $(tput smul setaf 7 dim)# x11$(tput sgr0) + 8) qtile-x11 + 9) bspwm + 10) awesome + 11) i3 + 12) i3-plasma + 13) plasma-x11 + 14) gnome-x11 + 15) xfce4 + 16) budgie + 17) cinnamon + 18) deepin + 19) mate + 20) pantheon + 21) cutefish + 22) lxde + 23) lxqt +menu +if [ ${1} ] +then + desktop=${1} +else + desktop=-1 +fi +until [[ ${desktop} -ge 0 && ${desktop} -le ${totalde} ]] +do + read -p '> ' desktop + [[ ${desktop} -ge 0 && ${desktop} -le ${totalde} ]] ||\ + echo -e "\n\n\e[1;31mInvalid selection, type an option from 0 to ${totalde}\e[0m" +done + +# Configure btrfs +if findmnt / | grep -w -q btrfs +then + desktop_packages+=(btrfs-progs) + if pacman -Q | grep -q "^grub" + then + desktop_packages+=(grub-btrfs inotify-tools) + systemd_services+=(grub-btrfsd.service) + fi + + # echo -e '\n\e[1;35mConfiguring snapper\e[0m' + # sudo pacman --sync --ask 4 snapper snap-pac + # echo + # sudo umount /.snapshots /home/.snapshots + # sudo rm -r /.snapshots /home/.snapshots + # sudo snapper -c root create-config / + # sudo snapper -c home create-config /home + # sudo sed -i 's/TIMELINE_LIMIT_HOURLY="10"/TIMELINE_LIMIT_HOURLY="4"/' /etc/snapper/configs/root + # sudo sed -i 's/TIMELINE_LIMIT_DAILY="10"/TIMELINE_LIMIT_DAILY="7"/' /etc/snapper/configs/root + # sudo sed -i 's/TIMELINE_LIMIT_MONTHLY="10"/TIMELINE_LIMIT_MONTHLY="1"/' /etc/snapper/configs/root + # sudo sed -i 's/TIMELINE_LIMIT_YEARLY="10"/TIMELINE_LIMIT_YEARLY="0"/' /etc/snapper/configs/root + # sudo sed -i 's/TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/home + # sudo btrfs subvolume delete /.snapshots + # sudo btrfs subvolume delete /home/.snapshots + # sudo mkdir -p --mode=750 /.snapshots /home/.snapshots + # sudo mount -a + # sudo systemctl --quiet enable --now snapper-cleanup.timer snapper-timeline.timer + + # # Create snapshots for fresh installation + # sudo snapper -c root create --description "fresh install" + # sudo snapper -c home create --description "fresh install" +fi + +# Assign DE variables +case ${desktop} in + 0) + # TTY only + desktop_choice=none + echo + ;; + *) + systemd_user_services+=(wireplumber.service pipewire-pulse.service pipewire.service) + desktop_packages+=(ttf-dejavu pipewire pipewire-audio pipewire-pulse pipewire-jack wireplumber) + case ${desktop} in + [1-7]) + # Wayland + desktop_packages+=(wayland) + case ${desktop} in + 1) + desktop_choice=sway + desktop_packages+=(sway seatd swaylock swayidle swaybg bemenu bemenu-wayland i3status foot kate dolphin konsole kompare kcalc breeze-icons kde-cli-tools brightnessctl gnome-keyring fakeroot qt5-wayland polkit-kde-agent) + systemd_services+=(seatd.service) + ;; + 2) + desktop_choice=hyprland + desktop_packages+=(hyprland uwsm swaylock swayidle bemenu bemenu-wayland waybar foot kate dolphin konsole kompare kcalc breeze-icons kde-cli-tools ttf-font-awesome brightnessctl gnome-keyring fakeroot qt5-wayland polkit-kde-agent) + systemd_user_services+=(foot-server.service) + ;; + 3) + desktop_choice=qtile-wayland + desktop_packages+=(qtile python-pywlroots xorg-xwayland foot) + systemd_user_services+=(foot-server.service) + ;; + 4) + desktop_choice=river + desktop_packages+=(river foot) + systemd_user_services+=(foot-server.service) + ;; + 5) + desktop_choice=plasma-wayland + desktop_packages+=($(pacman -Sgq plasma | grep -v 'discover\|oxygen\|plasma-nm') konsole foot kate dolphin kompare kcalc) + systemd_services+=(sddm.service) + systemd_user_services+=(foot-server.service) + ;; + 6) + desktop_choice=gnome-wayland + desktop_packages+=(gnome foot) + systemd_services+=(gdm.service) + systemd_user_services+=(foot-server.service) + ;; + 7) + desktop_choice=cosmic + desktop_packages+=(cosmic) + systemd_services+=(cosmic-greeter.service) + if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' + then + desktop_packages+=(vulkan-virtio) + elif lspci | grep VGA | grep -q NVIDIA + then + desktop_packages+=(nvidia-utils) + elif lscpu | grep 'Model name:' | grep -q AMD + then + desktop_packages+=(amdvlk) + elif lscpu | grep 'Model name:' | grep -q Intel + then + desktop_packages+=(vulkan-intel) + fi + ;; + esac + # Environment + sudo tee -a /etc/environment >/dev/null <<- environment + QT_QPA_PLATFORM=wayland + environment + ;; + *) + # Xorg + desktop_packages+=(xorg) + case ${desktop} in + 8) + desktop_choice=qtile-x11 + desktop_packages+=(qtile xorg-xinit konsole) + ;; + 9) + desktop_choice=bspwm + desktop_packages+=(bspwm sxhkd xorg-xinit polybar konsole) + ;; + 10) + desktop_choice=awesome + desktop_packages+=(awesome xorg-xinit konsole) + ;; + 11) + desktop_choice=i3 + desktop_packages+=(i3-wm i3status i3lock dmenu lightdm lightdm-gtk-greeter pavucontrol konsole kate dolphin kompare breeze-icons) + systemd_services+=(lightdm.service) + ;; + 12) + desktop_choice=i3-plasma + desktop_packages+=($(pacman -Sgq plasma | grep -v 'discover\|oxygen\|plasma-nm') konsole kate dolphin kompare kcalc i3-wm i3status dmenu wmctrl feh) + systemd_services+=(sddm.service) + ;; + 13) + desktop_choice=plasma-x11 + desktop_packages+=($(pacman -Sgq plasma | grep -v 'discover\|oxygen\|plasma-nm') konsole kate dolphin kompare kcalc) + systemd_services+=(sddm.service) + ;; + 14) + desktop_choice=gnome-x11 + desktop_packages+=(gnome) + systemd_services+=(gdm.service) + ;; + 15) + desktop_choice=xfce4 + desktop_packages+=(xfce4 lightdm lightdm-gtk-greeter) + systemd_services+=(lightdm.service) + ;; + 16) + desktop_choice=budgie + desktop_packages+=(budgie lightdm lightdm-gtk-greeter tilix) + systemd_services+=(lightdm.service) + ;; + 17) + desktop_choice=cinnamon + desktop_packages+=(cinnamon lightdm lightdm-gtk-greeter gnome-console) + systemd_services+=(lightdm.service) + ;; + 18) + desktop_choice=deepin + desktop_packages+=(deepin deepin-kwin $(pacman -Sgq deepin-extra | grep -v deepin-reader) lightdm lightdm-gtk-greeter gnome-keyring) + systemd_services+=(lightdm.service) + if uname -r | grep -q 'lts\|hardened\|zen' + then + desktop_packages+=(deepin-anything-dkms) + else + desktop_packages+=(deepin-anything-arch) + fi + ;; + 19) + desktop_choice=mate + desktop_packages+=(mate mate-extra lightdm lightdm-gtk-greeter) + systemd_services+=(lightdm.service) + ;; + 20) + desktop_choice=pantheon + desktop_packages+=(pantheon lightdm-pantheon-greeter lightdm) + systemd_services+=(lightdm.service) + ;; + 21) + desktop_choice=cutefish + desktop_packages+=(cutefish sddm) + systemd_services+=(sddm.service) + ;; + 22) + desktop_choice=lxde + desktop_packages+=(lxde lxdm) + systemd_services+=(lxdm.service) + ;; + 23) + desktop_choice=lxqt + desktop_packages+=(lxqt sddm breeze-icons) + systemd_services+=(sddm.service) + ;; + esac + ;; + esac + echo + echo -e "\t\e[1mYou have chosen \e[32m${desktop_choice}\e[0m\e[1m desktop\e[0m" + echo + echo -e '\e[1;35mInstalling base packages\e[0m' + sudo pacman --sync --ask 4 ${desktop_packages[@]} || die 'Failed to install required packages' + echo + ;; +esac + +case ${desktop_choice} in + plasma-wayland) + mkdir -p ~/.config/{autostart-scripts,foot}/ + # install /dev/stdin ~/.config/autostart-scripts/foot.sh <<- foot + # #!/bin/sh + # foot --server + # foot + cat >>~/.config/kglobalshortcutsrc <<- shortcuts + + [services][footclient.desktop] + _launch=Alt+Return + + [services][org.kde.konsole.desktop] + _launch=none + shortcuts + ;; + gnome-wayland) + mkdir -p ~/.config/{autostart,foot}/ + # cat > ~/.config/autostart/foot-server.desktop <<- foot + # [Desktop Entry] + # Type=Application + # Name=Foot server + # Exec=foot --server + # foot + gsettings set $(gsettings list-schemas | grep terminal) exec footclient + ;; + cosmic) + mkdir -p ~/.config/foot/ + ;; + i3|i3-plasma) + # i3-config + mkdir -p ~/.config/i3/ + curl --fail -s -L https://raw.githubusercontent.com/i3/i3/next/etc/config | sed 's/exec i3-config-wizard/# &/' > ~/.config/i3/config + case ${desktop_choice} in + i3-plasma) + kwriteconfig6 --file startkderc --group General --key systemdBoot false + sudo install /dev/stdin /opt/local/bin/plasma-i3.sh <<- EOF + #!/usr/bin/env bash + export KDEWM=/usr/bin/i3 + /usr/bin/startplasma-x11 + EOF + sudo tee /usr/share/xsessions/plasma-i3.desktop >/dev/null <<- EOF + [Desktop Entry] + Type=XSession + Exec=/opt/local/bin/plasma-i3.sh + DesktopNames=KDE + Name=Plasma (i3) + Comment=KDE Plasma with i3 as the WM + EOF + ;; + esac + if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' + then + cat >> ~/.config/i3/config <<- 'i3' + + exec xrandr --output $(xrandr -q | grep -w 'connected primary' | awk '{print $1}') --mode 1920x1080 + exec xrandr --dpi 192 + + exec spice-vdagent + exec VBoxClient-all + i3 + fi + ;; + qtile-x11) + echo 'exec qtile start' >~/.xinitrc + cat >> ~/.bash_profile <<- 'autostart' + + if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = 1 ] + then + exec startx + fi + autostart + ;; + qtile-wayland) + mkdir -p ~/.config/foot/ + cat >> ~/.bash_profile <<- 'autostart' + + # Start sway on login from tty + if [ -z "${WAYLAND_DISPLAY}" ] && [ "${XDG_VTNR}" = 1 ] + then + exec qtile start -b wayland + fi + autostart + ;; + river) + mkdir -p ~/.config/foot/ + cat >> ~/.bash_profile <<- 'autostart' + + # Start sway on login from tty + if [ -z "${WAYLAND_DISPLAY}" ] && [ "${XDG_VTNR}" = 1 ] + then + exec river + fi + autostart + ;; + bspwm) + cat >> ~/.bash_profile <<- 'autostart' + + if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = 1 ] + then + exec startx /usr/bin/bspwm + fi + autostart + mkdir -p ~/.config/{bspwm,sxhkd,polybar}/ + install -Dm755 /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/bspwmrc + install -Dm644 /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/sxhkdrc + cp /etc/polybar/config.ini ~/.config/polybar/ + ;; + awesome) + cat >> ~/.bash_profile <<- 'autostart' + + if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = 1 ] + then + exec startx /usr/bin/awesome + fi + autostart + mkdir -p ~/.config/awesome/ + sed -e '/^terminal =/c terminal = "konsole"' \ + -e '/^modkey =/c modkey = "Mod1"' /etc/xdg/awesome/rc.lua > ~/.config/awesome/ + ;; + sway) + # Sway + sudo gpasswd -a ${USER} seat >/dev/null + + # Dolphin default apps + mkdir -p ~/.config/menus/ + curl --fail -s -L https://raw.githubusercontent.com/KDE/plasma-workspace/master/menu/desktop/plasma-applications.menu -o ~/.config/menus/applications.menu + kbuildsycoca6 >/dev/null 2>&1 + + # Dolphin default terminal + cat >> ~/.config/kdeglobals <<- foot + [General] + TerminalApplication=footclient + foot + + # Create conf directories + mkdir -p ~/.config/{sway/config.d,foot}/ + + # Identify conf locations + cat > ~/.config/sway/config <<- 'config' + include /etc/sway/config.d/* + include ~/.config/sway/config.d/* + config + + # Default 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"' \ + /etc/sway/config > ~/.config/sway/config.d/00-config + sed -n "/^# Status Bar:$/q;p" -i ~/.config/sway/config.d/00-config + + # Sway config + cat > ~/.config/sway/config.d/zz-sway <<- 'config' + # Disable xwayland + xwayland disable + + # Start foot terminal server + exec foot --server + + # Use i3status + bar { + status_command i3status + } + + # 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 + + # Floating for KCalc + for_window [title="KCalc"] floating enable + + # Bind keys for brightness + bindsym XF86MonBrightnessDown exec brightnessctl set 5%- + bindsym XF86MonBrightnessUp exec brightnessctl set 5%+ + + # Mouse and keyboard defaults + input type:keyboard xkb_numlock enabled + input type:touchpad { + tap enabled + natural_scroll enabled + } + config + + # Display config + cat > ~/.config/sway/config.d/zz-display <<- 'display' + # # Declare output + # set $laptop eDP-1 (swaymsg -t get_outputs) + + # Swayidle + exec swayidle -w \ + timeout 300 'swaylock -e -f -c 000000' \ + timeout 315 'swaymsg "output * power off"' \ + resume 'swaymsg "output * power on"' \ + timeout 600 'systemctl suspend' \ + before-sleep 'swaylock -e -f -c 000000' + + # Laptop lid switches + bindswitch --reload --locked lid:on output * disable + bindswitch --reload --locked lid:off output * enable + + # Swaylock + bindsym Mod4+l exec swaylock -e -f -c 000000 + + # Solid black background + output * bg #000000 solid_color + + # Prevent swayidle when a window is in fullscreen + for_window [class=".*"] inhibit_idle fullscreen + for_window [app_id=".*"] inhibit_idle fullscreen + display + + # Polkit + cat > ~/.config/sway/config.d/zz-polkit <<- 'polkit' + exec "/usr/lib/polkit-kde-authentication-agent-1" + polkit + + # Monitor + if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' + then + cat > ~/.config/sway/config.d/zz-virtualmonitor <<- 'monitor' + # Virtual monitor + output Virtual-1 { + pos 0,0 + mode 1920x1080@60Hz + scale 1.25 + } + monitor + fi + + # TTY sway autostart + cat >> ~/.bash_profile <<- 'sway' + + # Start sway on login from tty + if [ -z "${WAYLAND_DISPLAY}" ] && [ "${XDG_VTNR}" = 1 ] + then + exec sway + fi + sway + ;; + hyprland) + # Hyprland + mkdir -p ~/.config/{menus,hypr/scripts,foot,waybar}/ + + # Dolphin default apps + curl --fail -s -L https://raw.githubusercontent.com/KDE/plasma-workspace/master/menu/desktop/plasma-applications.menu -o ~/.config/menus/applications.menu + kbuildsycoca6 >/dev/null 2>&1 + + # Dolphin default terminal + cat >> ~/.config/kdeglobals <<- foot + [General] + TerminalApplication=footclient + foot + + # Swayidle + install /dev/stdin ~/.config/hypr/scripts/sleep.sh <<- 'swayidle' + swayidle -w timeout 300 'swaylock -f -c 000000' \ + timeout 600 'systemctl suspend' \ + before-sleep 'swaylock -f -c 000000' & + swayidle + + # Config + curl --silent --fail https://raw.githubusercontent.com/hyprwm/Hyprland/refs/heads/main/example/hyprland.conf |\ + sed -e '/Autostart/i exec-once = /usr/lib/polkit-kde-authentication-agent-1' \ + -e '/Autostart/i exec-once = waybar -c ~/.config/waybar/waybar.conf' \ + -e '/Autostart/i exec-once = ~/.config/hypr/scripts/sleep.sh' \ + -e '/^$terminal =/c $terminal = footclient' \ + -e '/gaps_in =/c gaps_in = 0' \ + -e '/gaps_out =/c gaps_out = 0' \ + -e '/^$menu =/c $menu = bemenu-run -p "" --no-overlap --tb "##285577" --hb "##285577" --tf "##eeeeee" --hf "##eeeeee" --nf "##bbbbbb"' \ + -e 's/bind = $mainMod, R, exec, $menu/bind = $mainMod, D, exec, $menu/' \ + -e 's/bind = $mainMod, Q, exec, $terminal/bind = $mainMod, RETURN, exec, $terminal/' \ + -e 's/bind = $mainMod, C, killactive/bind = $mainMod SHIFT, Q, killactive/' \ + -e '/$mainMod =/c $mainMod = ALT' \ + -e '/RETURN/a bind = SUPER, L, exec, swaylock -e -f -c 000000' \ + > ~/.config/hypr/hyprland.conf + # -e '/Autostart/i exec-once = uwsm app -- foot --server' \ + + # QEMU monitor + if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' + then + sed -i "/^monitor/c monitor = $(hyprctl monitors all | grep "^Monitor" | awk '{print $2}'), 1920x1080@60, 0x0, 1.5" ~/.config/hypr/hyprland.conf + fi + + # Waybar + sed -e 's|sway/workspaces|sway/workspaces|g' \ + -e 's|sway/mode|hyprland/submap|g' \ + -e '/position/ s|//||' \ + /etc/xdg/waybar/config.jsonc > ~/.config/waybar/waybar.conf + + # TTY sway autostart + cat >> ~/.bash_profile <<- 'hyprland' + + # Start sway on login from tty + if uwsm check may-start + then + exec uwsm start hyprland.desktop + fi + hyprland + ;; +esac + +# Foot terminal config (/etc/xdg/foot/foot.ini) +if [ -d ~/.config/foot/ ] +then + cat > ~/.config/foot/foot.ini <<- 'foot' + [main] + include=/usr/share/foot/themes/kitty + font=Source Code Pro:size=12 + workers=32 + + [scrollback] + # lines=1000 + foot +fi + +# Add user to shared folder group if in virtualbox guest +if ls -l /dev/disk/* | grep -q VBOX +then + sudo gpasswd -a ${USER} vboxsf >/dev/null + echo +fi + +# iwd status function +if pacman -Q | grep -w -q iwd +then + cat > ~/.local/functions/iwd-status <<- iwd + #!/usr/bin/env bash + # iwd connection status + function iwd-status + { + iwctl station "$(iwctl station list | grep connected | awk '{print $(NF-1)}')" show + } + + alias iwctl='iwctl station $(iwctl station list | grep connected | awk '{print $(NF-1)}')' + iwd +fi + +if [[ "${desktop_choice}" == "none" ]] +then + sudo rm -f ${0} + echo -e '\e[1;34mSetup complete, press any key to continue\e[5m...\e[0m\n' + read -n 1 -s -p '' +elif ls /usr/share/*sessions | grep -q desktop +then + install /dev/stdin ~/.local/bin/startup <<- 'EOF' + #!/usr/bin/env bash + + # 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 + vim ~/.local/bin/startupscript + ~/.local/bin/startupscript + rm -f ${0} + exit 0 + else + echo -e '\n\e[31mNo internet connectivity detected' + echo -e 'Connect to a network and try again' + echo -e 'Aborting installer...\e[0m\n' + exit 1 + fi + EOF + + install /dev/stdin ~/.local/bin/startupscript <<- EOF + $(curl --fail --silent https://git.myvelabs.com/lab/archlinux/raw/branch/master/functions/startup) + EOF + sudo rm -f ${0} + echo -e '\e[1;34mDesktop installed, press any key to load '${desktop_choice}'\e[5m...\e[0m' + read -n 1 -s -p '' + echo +else + die 'Something does not feel right' +fi + +# Systemd services +for service in ${systemd_user_services[@]} +do + systemctl --quiet --user enable --now ${service} +done +sudo systemctl --quiet enable --now ${systemd_services[@]} diff --git a/functions/startup b/functions/startup new file mode 100755 index 0000000..7e7a762 --- /dev/null +++ b/functions/startup @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +# AUR package list +aur_list=( +# nomachine +# realvnc-vnc-viewer +# jdownloader2 +# ledger-live-bin +# czkawka-gui-bin +) + +# Optional packages list +optional=( +firefox firefox-decentraleyes firefox-ublock-origin +ark okular shotwell mpv ffmpegthumbs +veracrypt keepassxc +# nextcloud-client +# ntfs-3g exfatprogs +# libreoffice-fresh +# remmina libvncserver +# torbrowser-launcher +# thunderbird +# filezilla +# chromium +# vlc +# gnome-disk-utility +# xdg-user-dirs +# noto-fonts-cjk +# noto-fonts-emoji +) + +function INSTALL +{ + sudo pacman --sync ${@} --ask 4 + echo +} + +# VirtualBox +until [[ ${install_virtualbox} = [yYnN] ]] +do + read -n 1 -p $'\n\e[1mWould you like to install Virtualbox? (y/n): \e[0m' install_virtualbox + [[ ${install_virtualbox} = [yYnN] ]] || echo -e -n '\n\n\e[1;31mNot a valid answer, type "y" or "n"\e[0m' +done +echo +# QEMU +until [[ ${install_qemu} = [yYnN] ]] +do + read -n 1 -p $'\n\e[1mWould you like to install QEMU? (y/n): \e[0m' install_qemu + [[ ${install_qemu} = [yYnN] ]] || echo -e -n '\n\n\e[1;31mNot a valid answer, type "y" or "n"\e[0m' +done + +echo +echo + +# Package groups installation +INSTALL ${optional[@]} + +# Remmina +if [[ ${optional[@]} =~ "remmina" ]] && [ -d ~/.config/sway/config.d/ ] +then + # Remmina passthrough + cat > ~/.config/sway/config.d/zz-remmina <<- 'remmina' + # VNC passthrough + mode remmina { + bindsym Mod4+Shift+p mode default + } + bindsym Mod4+Shift+p mode remmina + remmina +fi + +# Tor +if [[ ${optional[@]} =~ "torbrowser-launcher" ]] +then + torbrowser-launcher +fi + +# Bluetooth +if pacman -Q | grep -q bluez-utils +then + INSTALL bluedevil + sudo systemctl --quiet enable --now bluetooth.service +fi + +# AUR packages +if [ ${#aur_list[@]} -gt 0 ] +then + if ! pacman -Q | grep -q -w git + then + INSTALL git + fi + echo -e '#!/usr/bin/env bash' > ~/.local/functions/aur-packages + for aur_package in ${aur_list[@]} + do + cd + git clone https://aur.archlinux.org/${aur_package}.git + if echo ${aur_package} | grep -q nomachine + then + sed -i 's/_autoservice=n/_autoservice=y/' nomachine/PKGBUILD + sed -i 's/_autofirewall=n/_autofirewall=y/' nomachine/PKGBUILD + fi + cd ${aur_package}/ + makepkg -csi + echo + cd .. + rm -r ${aur_package}/ -f + cat >> ~/.local/functions/aur-packages <<- aur + function aur-${aur_package} + { + cd ~/ + git clone https://aur.archlinux.org/${aur_package}.git + cd ${aur_package}/ + makepkg -csi + echo + cd ~/ + rm -r ${aur_package}/ -f + } + + aur + done +fi + +# Optional AUR extras +# freefilesync +# pdfsam +# fslint pygtk + +# Printers +# yay -S --ask 4 print-manager cups system-config-printer skanlite && echo && sudo systemctl --quiet enable --now org.cups.cupsd && sudo gpasswd -a ${USER} sys >/dev/null +# Webcam +# sudo gpasswd -a ${USER} video >/dev/null + +# i3 +if [ -f ~/.config/i3/config ] +then + cat >> ~/.config/i3/config <<- 'config' + + # gaps inner 8 + # gaps outer 4 + # for_window [class="^.*"] border pixel 2 + config + + cat > ~/.local/functions/i3-config <<- 'config' + #!/usr/bin/env bash + # i3 config + function i3-config + { + vim ~/.config/i3/config + } + config + + if [ -f /usr/share/xsessions/plasma-i3.desktop ] + then + tee -a ~/.config/i3/config >/dev/null <<- 'integration' + + # >>> Plasma Integration <<< + # Try to kill the wallpaper set by Plasma (it takes up the entire workspace and hides everything) + exec --no-startup-id wmctrl -c Plasma + for_window [title="Desktop — Plasma"] kill; floating enable; border none + no_focus [class=”plasmashell”] + + # Avoid tiling popups, dropdown windows from plasma + for_window [class="plasmashell"] floating enable + for_window [class="Plasma"] floating enable + for_window [class="krunner"] floating enable + for_window [class="Kmix"] floating enable + for_window [class="Klipper"] floating enable + for_window [class="Plasmoidviewer"] floating enable + + # >>> Window Rules <<< + # >>> Avoid tiling for non-Plasma stuff <<< + for_window [window_role="pop-up"] floating enable + for_window [window_role="bubble"] floating enable + for_window [window_role="task_dialog"] floating enable + for_window [window_role="Preferences"] floating enable + for_window [window_role="About"] floating enable + for_window [window_type="dialog"] floating enable + for_window [window_type="menu"] floating enable + integration + + install /dev/stdin ~/.local/bin/rotate-wallpapers <<- feh + #!/usr/bin/env bash + sleep 1 + while true + do + feh --bg-max --randomize /home/${USER}/Pictures + sleep 15 + done + feh + else + if ls -l /dev/disk/* | grep -q 'VBOX' + then + echo 'exec VBoxClient-all' >> ~/.config/i3/config + elif ls -l /dev/disk/* | grep -q 'virtio\|QEMU' + then + echo 'exec spice-vdagent' >> ~/.config/i3/config + fi + fi +fi + +# Sway nextcloud +if [[ ${optional[@]} =~ "nextcloud-client" ]] && [ -f ~/.config/sway/config.d/zz-sway ] +then + cat > ~/.config/sway/config.d/zz-nextcloud <<- 'nextcloud' + # Nextcloud + exec nextcloud + for_window [title="Nextcloud Settings"] floating enable + nextcloud +fi + +# Virtualbox +if [[ ${install_virtualbox} = [yY] ]] +then + echo 'Installing Virtualbox' + + if uname -r | grep -q 'lts\|hardened\|zen' + then + host_modules='virtualbox-host-dkms' + else + host_modules='virtualbox-host-modules-arch' + fi + + INSTALL virtualbox ${host_modules} virtualbox-guest-iso + echo + + sudo gpasswd -a ${USER} vboxusers >/dev/null + echo + + if findmnt '/' | grep -q -w 'btrfs' + then + mkdir ~/VirtualBox\ VMs/ + chattr +C ~/VirtualBox\ VMs/ + fi +fi + +# QEMU +if [[ ${install_qemu} = [yY] ]] +then + echo 'Installing QEMU' + INSTALL qemu-desktop virt-manager edk2-ovmf \ + dnsmasq dmidecode vde2 bridge-utils + echo + + # Make host system a pacman cache server + sudo tee /etc/systemd/system/local-cacheserver.service >/dev/null <<- 'CACHESERVER' + [Unit] + Description=Python HTTP server for Pacman Cache Server + + [Service] + ExecStart=/usr/bin/python3 -m http.server --directory /var/cache/pacman/pkg/ 9090 + Restart=always + + [Install] + WantedBy=multi-user.target + CACHESERVER + + # Host iptables rules as a pacman cache server + sudo iptables -I INPUT -s 192.168.0.0/16 -p tcp -m tcp --dport 9090 -j ACCEPT -m comment --comment "Pacman cache server" + sudo sed -i '/## Simple Firewall/i\ +# 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"\n' \ + /etc/iptables/userinput.rules + sed "/OUTPUT ACCEPT/r /etc/iptables/userinput.rules" /etc/iptables/simple_firewall.rules | sudo tee /etc/iptables/iptables.rules >/dev/null + + # QEMU pacman cache service + sudo tee /etc/systemd/system/local-update-virtpkg-cache.service >/dev/null <<- '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 -s -L https://git.myvelabs.com/lab/archlinux/raw/branch/master/pkg/qemu)" + service + + # QEMU pacman cache timer + sudo tee /etc/systemd/system/local-update-virtpkg-cache.timer >/dev/null <<- 'timer' + [Unit] + Description=Refresh pacman package cache + + [Timer] + OnCalendar=*-*-* 00/12:00:00 + RandomizedDelaySec=12h + Persistent=true + + [Install] + WantedBy=timers.target + timer + + # Enable services + sudo systemctl --quiet enable --now libvirtd.service + sudo systemctl --quiet enable local-cacheserver.service local-update-virtpkg-cache.timer + sudo virsh -q net-start default + sudo virsh -q net-autostart default + sudo gpasswd -a ${USER} libvirt >/dev/null + echo +fi + +rm -f ${0} +echo -e '\e[1;32mSupplementary installer completed, reboot one last time\e[0m\n' diff --git a/functions/user b/functions/user new file mode 100755 index 0000000..4013db3 --- /dev/null +++ b/functions/user @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +revision=0.1a +set -a +set -E + +# Create local paths +mkdir -p ~/.local/{bin,functions} + +# Generate ssh identity +ssh-keygen -q \ + -t ed25519 \ + -P "" \ + -C "${username}@${hostname}" \ + -f ~/.ssh/id_ed25519 +mkdir ~/.ssh/sockets/ + +# Custom bashrc +cat > ~/.local/functions/bashrc <<- 'BASHRC' +#!/usr/bin/env bash +# Shell color +PS1="$(tput setaf 6)[\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 + +# Btrfs functions +if findmnt / | grep -w -q btrfs +then + cat > ~/.local/functions/btrfs <<- 'btrfs' + #!/usr/bin/env bash + # Btrfs check + function btrfs-check + { + sudo dmesg | grep -i btrfs + } +# # Snapper +# function snapshot-home +# { +# if [ "${*}" ] +# then +# sudo snapper -c home create --description "${*}" +# else +# sudo snapper -c home create --description "$(date)" +# fi +# } +# +# function snapshot-root +# { +# if [ "${*}" ] +# then +# sudo snapper -c root create --description "${*}" +# else +# sudo snapper -c root create --description "$(date)" +# fi +# } +# +# function snapshot-list +# { +# sudo -v +# echo -e '\n\e[1;33mRoot snapshots:\e[0m' +# sudo snapper -c root ls +# echo -e '\n\e[1;33mHome snapshots:\e[0m' +# sudo snapper -c home ls +# echo +# } +# +# function snapshot-delete +# { +# if ! grep -w -q 'root\|home' <<< "${1}" || [ -z "${2}" ] +# then +# echo -e '\n\t\e[1;31mInvalid option, use syntax: ${root,home} $snapshot\e[0m\n' +# return 1 +# elif grep -w -q '1' <<< "${2}" +# then +# echo -e '\n\t\e[1;31mUnable to delete "fresh install" snapshot\e[0m\n' +# return 1 +# fi +# +# if [[ "${1}" = "root" ]] +# then +# if ls /.snapshots | grep -q $(sed -e 's/-/\\\|/g' <<< "${2}") +# then +# sudo snapper -c root delete "${2}" +# elif ! [ -d /.snapshots/"${2}" ] +# then +# echo -e '\n\t\e[1;31mSnapshot doesn't exist, try again\e[0m\n' +# fi +# elif [[ "${1}" = "home" ]] +# then +# if ls /home/.snapshots | grep -q $(sed -e 's/-/\\\|/g' <<< "${2}") +# then +# sudo snapper -c home delete "${2}" +# elif ! [ -d /home/.snapshots/"${2}" ] +# then +# echo -e '\n\t\e[1;31mSnapshot doesn't exist, try again\e[0m\n' +# fi +# fi +# } +btrfs +fi + +if pacman -Q | grep -q yubikey +then + install /dev/stdin ~/yksetup.sh <<- 'yubikey' + $(curl --fail --silent https://git.myvelabs.com/lab/archlinux/raw/branch/master/functions/yubikey) + yubikey +fi diff --git a/functions/yubikey b/functions/yubikey new file mode 100755 index 0000000..9655cd4 --- /dev/null +++ b/functions/yubikey @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +echo + +[ -d ~/.config/Yubico ] || mkdir -p ~/.config/Yubico + +sudo pacman -S --ask 4 pam-u2f yubico-pam + +read -n 1 -s -p $'\n\e[1;33mInsert your yubikey and press the enter key to continue \e[0m' +echo -e '\n\e[1mTouch the yubikey when it starts blinking\e[0m' + +pamu2fcfg > ~/.config/Yubico/u2f_keys + +read -n 1 -p $'\n\e[1mWould you like to add another yubikey? (y/n): \e[0m' YUBIKEY + +until [[ "$YUBIKEY" = [nN] ]] +do + if [[ "$YUBIKEY" != [yYnN] ]] + then + echo -e '\n\n\e[1;31mNot a valid answer, type "y" or "n"\e[0m' + read -n 1 -p $'\e[1mWould you like to add another yubikey? (y/n): \e[0m' YUBIKEY + elif [[ "$YUBIKEY" = [yY] ]] + then + read -n 1 -s -p $'\n\n\e[1;33mInsert the next yubikey and press the enter key to continue \e[0m' + echo -e '\n\e[1mTouch the yubikey when it starts blinking\e[0m' + + pamu2fcfg -n >> ~/.config/Yubico/u2f_keys + + read -n 1 -p $'\n\e[1mWould you like to add another yubikey? (y/n): \e[0m' YUBIKEY + fi +done + +echo -e '\n\n\e[1;33mUpdating pam configs' +echo -e 'Open another terminal or tty and login as sudo in case an issue comes up' +read -n 1 -s -p $'Press the enter key once logged in as sudo in another terminal or tty \e[0m\n' + +# System wide user authentication +sudo sed -i '/^auth .* required .* pam_faillock.so .* authsucc/a\ +auth required pam_u2f.so' /etc/pam.d/system-auth + +# Polkit authentication +sudo sed -i '/^#%PAM.*/a\ +auth sufficient pam_u2f.so' /etc/pam.d/polkit-1 + +# Sudo authentication +sudo sed -i '/^#%PAM.*/a\ +auth sufficient pam_u2f.so' /etc/pam.d/sudo + +# KDE lockscreen authentication +sudo sed -i '/^#%PAM.*/a\ +auth required pam_u2f.so' /etc/pam.d/kde + +cat > ~/.local/functions/add-yubikey <<- 'ADDYUBIKEY' +# Add a new yubikey +function add-yubikey +{ + read -n 1 -s -p $'\n\e[1;33mInsert your yubikey and press the enter key to continue \e[0m' + echo -e '\n\e[1mTouch the yubikey when it starts blinking\e[0m' + + pamu2fcfg -n >> ~/.config/Yubico/u2f_keys + + read -n 1 -p $'\n\e[1mWould you like to add another yubikey? (y/n): \e[0m' YUBIKEY + until [[ "$YUBIKEY" = [nN] ]] + do + if [[ "$YUBIKEY" != [yYnN] ]] + then + echo -e '\n\n\e[1;31mNot a valid answer, type "y" or "n"\e[0m' + read -n 1 -p $'\e[1mWould you like to add another yubikey? (y/n): \e[0m' YUBIKEY + elif [[ "$YUBIKEY" = [yY] ]] + then + read -n 1 -s -p $'\n\n\e[1;33mInsert the next yubikey and press the enter key to continue \e[0m' + echo -e '\n\e[1mTouch the yubikey when it starts blinking\e[0m' + + pamu2fcfg -n >> ~/.config/Yubico/u2f_keys + + read -n 1 -p $'\n\e[1mWould you like to add another yubikey? (y/n): \e[0m' YUBIKEY + fi + done + echo -e '\n\n\e[1;34mYubikeys updated' + echo -e 'Exiting...\e[0m\n' +} +ADDYUBIKEY + +echo -e '\n\e[1;34mYubikey setup completed' +echo -e 'To update your saved yubikeys, run "add-yubikey" in the terminal' +echo -e 'Exiting...\e[0m\n' +rm -f ${0} diff --git a/homelab.sh b/homelab.sh new file mode 100755 index 0000000..1295197 --- /dev/null +++ b/homelab.sh @@ -0,0 +1,2315 @@ +#!/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 diff --git a/installer.sh b/installer.sh new file mode 100755 index 0000000..d2e07d7 --- /dev/null +++ b/installer.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +download_url=git.myvelabs.com/lab/archlinux/raw/branch/master + +# Detect network connectivity +if ! nc -z -w 1 archlinux.org 443 >/dev/null 2>&1 || ! nc -z -w 1 google.com 443 >/dev/null 2>&1 +then + echo -e '\n\t\e[31mNo internet connection detected\e[0m\n' + exit 1 +fi + +# Function to list available script options +function listScripts +{ + echo -e '\n\e[1;36mSelect a script to run\e[0m + 1) Arch Linux + 2) Arch Linux VM + 3) Arch Linux Passthrough + 4) Arch Linux Server + 5) Recovery' + + while true + do + read -n 1 -p '> ' script_selection + case ${script_selection} in + 1) script=arch + break;; + 2) script=virtual-machine + break;; + 3) script=passthrough + break;; + 4) script=homelab + break;; + 5) script=recover + break;; + *) echo -e '\n\n\e[1;31mInvalid selection, type an option from 1 to 5\e[0m' + ;; + esac + done + + echo +} + +# Detect script choice +case ${1} in + arch) + script=arch ;; + vm|virtual-machine) + script=virtual-machine ;; + passthrough) + script=passthrough ;; + server|homelab) + script=homelab ;; + recover) + script=recover ;; + *) + listScripts ;; +esac + +# Run script if successful, abort installer otherwise +if curl --fail -s -L https://${download_url}/${script}.sh -o ./arch +then + /usr/bin/bash ./arch "${@:2}" + exit 0 +else + echo -e '\e[31mInvalid script or server may be offline, make another selection or try again later\e[0m\n' + exit 1 +fi diff --git a/notes/archvps.sh b/notes/archvps.sh new file mode 100755 index 0000000..18ece9d --- /dev/null +++ b/notes/archvps.sh @@ -0,0 +1,1582 @@ +#!/usr/bin/env bash +set -a +set -E +revision=1.0 + +# Fill in details +hostname= +newuser= +port= + +primaryip= +gateway= +primaryip6= +gateway6= +subnet=255.255.255.0 + +# ZFS key +zfsgpgkey=D0F2AE55C1BF11A026D155813A658A95B8CFCC51 + +# SSH keys +curl --silent --fail https://myvelabs.com/lab/archlinux/raw/branch/master/notes/sshkeys.pub -o sshkeys + +# Static variables +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}') + +# 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 +} + +# 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 +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 + ;; + -? | -h | --help ) + cat <<- help + Parameters: + -s, --ssh-key Add SSH public key (enclosed in quotes) + -p, --port SSH port + -c, --cache Pacman cache server + -?, -h, --help This help screen +help + exit 0 + ;; + * ) + die "Unknown flag: ${1}" + ;; + esac + shift +done + +# 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' \ + -e '/\[core\]/i [myvezfs]\ +Server = https://repo.myvelabs.com/$repo\n' /etc/pacman.conf.bkp >/etc/pacman.conf + +# Internet connection check +if ping -q -c 1 -W 5 archlinux.org >/dev/null || ! ping -q -c 1 -W 5 google.com >/dev/null +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 + +# Detect needed firmwares +linux_firmware=(qemu-guest-agent) +virtualMachineService=qemu-guest-agent + +# Virtual machine defaults +function virtualMachineDefaults +{ + hostname=testzfs + userpass=123 + lukspass=12345678 + installation_drive_array=($(lsblk -d -n -o name | grep -v 'loop0\|sr0')) + + say in italic 'Troubleshooting defaults loaded' + echo +} + +# Grab system variables +function initialSetup +{ + # 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 + say as title "Identify the system drive from the list of available devices below" + lsblk -d -o name,model,size,mountpoint | grep -v "archiso\|sr0" + 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 + ask for installation_drive "Installation device" + if [ ${installation_drive} ] && \ + lsblk -o name | grep -q -w ${installation_drive} + then + installation_drive_array=${installation_drive} + break + else + lsblk -d -o name,model,size,mountpoint | grep -v "archiso\|sr0" + if [ -z ${installation_drive} ] + then + say as warning 'Field cannot be empty, try again' + else + say as warning 'Invalid selection or drive not available, try again' + fi + fi + done + say as success "Installation drive set as /dev/${installation_drive}" + 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" +echo +presskeytoresume +echo + +# Setup +if ls -l /dev/disk/* | grep -q 'VBOX\|virtio\|QEMU' +then + while true + do + ask for load_defaults in blue press "Would you like to load virtual machine troubleshooting defaults? (y/n)" + echo + + 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 + +# Fetch disk ID of installation disks +for drive in ${installation_drive_array[@]} +do + installation_disks+=(/dev/${drive}) +done + +# Default zroot layout +zrootlayout="$(printf '%s2 ' ${installation_disks[@]})" + +# 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 msdos \ + mkpart primary 1MiB 300MiB \ + mkpart primary 300MiB 100% \ + set 1 boot 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=lz4 \ + -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=/var/log zroot/LOG +zfs create -o mountpoint=/var/tmp zroot/TMP +zfs create -o mountpoint=/var/cache/pacman/pkg zroot/PKG +zfs create -o mountpoint=/etc/letsencrypt zroot/HTTPS +zfs create -o mountpoint=/docker zroot/DOCKER +zfs create -o mountpoint=/docker/SQL -o recordsize=16K zroot/DOCKER/SQL +zpool export zroot +zpool import -R /mnt zroot -N -d ${installation_disks}2 +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}1 +done +echo + +# Mount efi partition +install -d -m 755 /mnt/boot/ +mount ${installation_disks}1 /mnt/boot + +# Fix directory permissions +chmod 1777 /mnt/var/tmp + +# # 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' + +# Default packages +archpkgs="${linux_firmware[@]} \ + sudo openssh syslinux fakeroot \ + vim pacman-contrib bash-completion \ + mkinitcpio-netconf mkinitcpio-tinyssh tinyssh \ + reflector rsync \ + fail2ban \ + nginx-mainline certbot certbot-nginx apache \ + iptables-nft openbsd-netcat \ + docker docker-compose \ + wireguard-tools systemd-resolvconf \ + ufw unbound \ + git pv" + +# 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 dbus-broker-units \ + ${archpkgs} + +# Configure zfs for pacman +sed -e '/ParallelDownloads/c ParallelDownloads = 10' \ + -e '/Color/c Color' \ + -e '/\[core\]/i [myvezfs]\ +Server = https://repo.myvelabs.com/$repo\n' \ + -i /mnt/etc/pacman.conf + +# 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,pacman.d/hooks,nginx/sites-{available,enabled}} \ + /mnt/zfs/{bin,snapshots} \ + /mnt/etc/systemd/{resolved,journald,networkd}.conf.d \ + /mnt/local/{bin,systemd,hooks} + +# Configure zfs for mkinitcpio +echo 'BINARIES+=(/usr/bin/zfs)' >/mnt/etc/mkinitcpio.conf.d/zz-binaries.conf +echo 'MODULES_DECOMPRESS="yes"' >/mnt/etc/mkinitcpio.conf.d/zz-modules.conf +grep '^HOOKS' /mnt/etc/mkinitcpio.conf | sed 's/filesystems fsck/netconf tinyssh zfsencryptssh zfs filesystems/' >/mnt/etc/mkinitcpio.conf.d/zz-hooks.conf + +# 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 + +# Copy networking files +ln -s -f /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf +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=${iface} + +[Network] +Description=Static network +DHCP=no + +# IPv4 +Address=${primaryip}/24 +Gateway=${gateway} + +# IPv6 +Address=${primaryip6}/64 + +[Route] +Gateway=${gateway6} +GatewayOnLink=yes +network + +# SSH and Dropbear +cat sshkeys >/mnt/etc/tinyssh/root_key + + # Chroot into new root + arch-chroot /mnt /usr/bin/bash <<"CHROOT" + +# Global bashrc +tee -a /etc/skel/.bashrc >/dev/null <<'bashglobal' + +# Add local functions folder to path +export PATH=${PATH}:${HOME}/.local/bin:/local/bin:/zfs/bin +export SUDO_PROMPT=$'\a'"$(tput rev)[sudo] password for %p:$(tput sgr0)"' ' + +# 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 sudo + +# Ignore duplicate and whitespace history entries +export HISTCONTROL=ignoreboth + +# +# ~/.bash_aliases +# + +# ZFS/btrfs +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='sudo pacman -Rcns $(pacman -Qtdq)' +alias unlock-pacman='sudo rm /var/lib/pacman/db.lck && sudo pacman -Syyu' + +# Rsync +alias rsync='rsync -v -h --progress --info=progress2 --partial --append-verify' +# --log-file= +# --remove-source-files + +# +# ~/.bash_functions +# + +# Pacman tools +function installer +{ + sudo pacman -S ${@} + echo +} + +function uninstall +{ + sudo pacman -Rcns ${@} + echo +} + +function syur +{ + /local/bin/syu && + reboot +} + +function syup +{ + /local/bin/syu && + poweroff +} + +# Update bash +function update-bash +{ + vim ~/.bashrc && + source ~/.bashrc +} +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 + +# Syslinux +say as heading "Configuring syslinux" +mkdir /boot/syslinux/ +extlinux --install /boot/syslinux/ +syslinux-install_update -i -a -m +# sed -i 's\linux ../vmlinuz-linux.*\linux ../vmlinuz-linux\g' /boot/syslinux/syslinux.cfg +# sed -i "s/APPEND root.*/APPEND zfs=bootfs ip=${primaryip}::${gateway}:${subnet}::${originaliface}:none rw quiet bgrt_disable/g" /boot/syslinux/syslinux.cfg +# sed -i 's/.*PROMPT.*/PROMPT 0/g' /boot/syslinux/syslinux.cfg +# sed -i 's/.*TIMEOUT.*/TIMEOUT 0/g' /boot/syslinux/syslinux.cfg +# sed -i 's/.*UI menu.*/#UI menu/g' +cat >/boot/syslinux/syslinux.cfg <<-syslinux +DEFAULT arch +PROMPT 0 +TIMEOUT 0 + +MENU TITLE Arch Linux +MENU COLOR border 30;44 #40ffffff #a0000000 std +MENU COLOR title 1;36;44 #9033ccff #a0000000 std +MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all +MENU COLOR unsel 37;44 #50ffffff #a0000000 std +MENU COLOR help 37;40 #c0ffffff #a0000000 std +MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std +MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std +MENU COLOR msg07 37;40 #90ffffff #a0000000 std +MENU COLOR tabmsg 31;40 #30ffffff #00000000 std + +LABEL arch + MENU LABEL Arch Linux + LINUX ../vmlinuz-linux + APPEND zfs=bootfs ip=${primaryip}::${gateway}:${subnet}::${originaliface}:none rw quiet bgrt_disable + INITRD ../initramfs-linux.img +LABEL hdt + MENU LABEL HDT (Hardware Detection Tool) + COM32 hdt.c32 +LABEL reboot + MENU LABEL Reboot + COM32 reboot.c32 +LABEL poweroff + MENU LABEL Poweroff + COM32 poweroff.c32 +syslinux +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 +ln -s -f /usr/share/zoneinfo/UTC /etc/localtime +hwclock --systohc --utc +say as heading "Time zone configured" + +# Hostname +echo ${hostname} >/etc/hostname +cat >>/etc/hosts </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 FAIL2BAN = /usr/bin/fail2ban-client +Cmnd_Alias PACMAN = /usr/bin/pacman +Cmnd_Alias IPTABLES = /usr/bin/ufw +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, FAIL2BAN, PACMAN, IPTABLES, MISC +WHEEL + +install -m 0440 /dev/stdin /etc/sudoers.d/.zz-NOPASSWD <<'NOPASSWD' +Defaults:${newuser} !authenticate +NOPASSWD + +say as heading "Configured superuser and user" + +# ZFS files +touch /zfs/snapshots/syu +chown ${newuser}: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 + +# Swap +vm.swappiness=10 +vm.vfs_cache_pressure=50 + +# Wireguard +net.ipv4.conf.all.forwarding = 1 +net.ipv6.conf.all.forwarding = 1 +SYSCTL + +# 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 + +# Docker +install /dev/stdin /usr/local/bin/update-dockerfiles <<'dockerfiles' +#!/usr/bin/env bash +for compose in $(find /docker -maxdepth 2 -type f -name docker-compose.yaml) +do + docker compose -f ${compose} pull + docker compose -f ${compose} up --detach +done + +docker network prune -f +docker image prune -af +docker volume prune -af +dockerfiles + +# Unbound +sed -i '/include-toplevel/c include-toplevel: "/etc/unbound/unbound.conf.d/*.conf"' /etc/unbound/unbound.conf +mkdir /etc/unbound/unbound.conf.d/ +tee /etc/unbound/unbound.conf.d/unbound.conf >/dev/null <<- unbound.conf +server: + # If no logfile is specified, syslog is used + # logfile: "/var/log/unbound/unbound.log" + verbosity: 0 + + interface: 0.0.0.0@53 + do-ip4: yes + do-udp: yes + do-tcp: yes + + # May be set to yes if you have IPv6 connectivity + do-ip6: yes + + # You want to leave this to no unless you have *native* IPv6. With 6to4 and + # Terredo tunnels your web browser should favor IPv4 for the same reasons + prefer-ip6: no + + # Use this only when you downloaded the list of primary root servers! + # If you use the default dns-root-data package, unbound will find it automatically + root-hints: "/etc/unbound/root.hints" + + # Trust glue only if it is within the server's authority + harden-glue: yes + + # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS + harden-dnssec-stripped: yes + + # Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes + # see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details + use-caps-for-id: no + + # Reduce EDNS reassembly buffer size. + # IP fragmentation is unreliable on the Internet today, and can cause + # transmission failures when large DNS messages are sent via UDP. Even + # when fragmentation does work, it may not be secure; it is theoretically + # possible to spoof parts of a fragmented DNS message, without easy + # detection at the receiving end. Recently, there was an excellent study + # >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<< + # by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/) + # in collaboration with NLnet Labs explored DNS using real world data from the + # the RIPE Atlas probes and the researchers suggested different values for + # IPv4 and IPv6 and in different scenarios. They advise that servers should + # be configured to limit DNS messages sent over UDP to a size that will not + # trigger fragmentation on typical network links. DNS servers can switch + # from UDP to TCP when a DNS response is too big to fit in this limited + # buffer size. This value has also been suggested in DNS Flag Day 2020. + edns-buffer-size: 1232 + + # Perform prefetching of close to expired message cache entries + # This only applies to domains that have been frequently queried + prefetch: yes + + # One thread should be sufficient, can be increased on beefy machines. In reality for most users running on small networks or on a single machine, it should be unnecessary to seek performance enhancement by increasing num-threads above 1. + num-threads: 2 + + # Ensure kernel buffer is large enough to not lose messages in traffic spikes + so-rcvbuf: 1m + + # Ensure privacy of local IP ranges + private-address: 10.6.22.0/24 + private-address: fd6e:4f68:5f03:fffe::/64 + + # Only give access to recursion clients from LAN IPs + access-control: 10.6.22.0/24 allow + access-control: fd6e:4f68:5f03:fffe::/64 allow + + # Hide server info from clients + hide-identity: yes + hide-version: yes + + # Send minimum amount of information to upstream servers to enhance + # privacy (best privacy). + qname-minimisation: yes + + # Enable ratelimiting of queries (per second) sent to nameserver for + # performing recursion. More queries are turned away with an error + # (servfail). This stops recursive floods (e.g., random query names), but + # not spoofed reflection floods. Cached responses are not rate limited by + # this setting. Experimental option. + ratelimit: 1000 + + # Use this certificate bundle for authenticating connections made to + # outside peers (e.g., auth-zone urls, DNS over TLS connections). + tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt" + tls-system-cert: yes + +forward-zone: + # Forward all queries (except those in cache and local zone) to + # upstream recursive servers + name: "." + # Queries to this forward zone use TLS + forward-tls-upstream: yes + + # https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers + + ## Cloudflare + forward-addr: 1.1.1.1@853#cloudflare-dns.com + forward-addr: 1.0.0.1@853#cloudflare-dns.com + + ## Quad9 + # forward-addr: 9.9.9.9@853#dns.quad9.net + # forward-addr: 149.112.112.112@853#dns.quad9.net + +remote-control: + # Enable remote control with unbound-control(8) here. + control-enable: no + + # what interfaces are listened to for remote control. + # give 0.0.0.0 and ::0 to listen to all interfaces. + # set to an absolute path to use a unix local name pipe, certificates + # are not used for that, so key and cert files need not be present. + control-interface: 127.0.0.1 + # control-interface: ::1 + + # port number for remote control operations. + control-port: 8953 +unbound.conf +curl --silent --fail --output /etc/unbound/root.hints https://www.internic.net/domain/named.cache +tee /etc/systemd/resolved.conf.d/zz-unbound.conf >/dev/null <<- 'unbound.conf' +[Resolve] +DNS=127.0.0.1 +DNS=::1 +DNSStubListener=no +unbound.conf + +# 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 +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 /local/hooks/localegen </dev/null +fi +hook +# locale.gen.pacnew hook +cat >/etc/pacman.d/hooks/100-localegen.hook </dev/null <~/.ssh/authorized_keys + +cat >> ~/.bashrc <<- 'BASHRC' + +# Custom shell color +PS1="$(tput setaf 8)[\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 +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" + ~/.local/bin/startup || exit 1 +fi +init + +install /dev/stdin ~/.local/bin/startup <<'EOF' +#!/usr/bin/env bash + +## 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 +} + +# Internet connection check +if ping -q -c 1 -W 3 archlinux.org >/dev/null +then + sudo timedatectl set-ntp true +else + die 'No internet connectivity detected, plug in an ethernet cable and try again' +fi + +# Journal +sudo systemctl -q restart systemd-journald.service + +rm -f ${0} ~/.local/functions/init +echo -e 'Supplementary installer completed, reboot one last time\e[0m\n' +EOF +CHANGEUSER + +# fail2ban +install /dev/stdin /usr/local/bin/fail2ban-jails <<'ALL-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 +ALL-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; +# CSP breaks some webapps +# add_header Content-Security-Policy "default-src 'self';" always; + +# http2 +http2 on; + +# http3 +# Open port 443/udp to use http3 +# Add reuseport to ONLY ONE virtual host: listen 443 quic reuseport; +listen 443 quic; +add_header Alt-Svc 'h3=":443"; ma=86400'; +quic_retry on; +http3 on; + +# Certbot defaults +listen 443 ssl; +include /etc/letsencrypt/options-ssl-nginx.conf; +ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +add_header Strict-Transport-Security "max-age=31536000" always; +http_upgrade + +# Firewall +ufw allow ${port:-22}/tcp comment SSH >/dev/null +ufw allow 80/tcp comment HTTP >/dev/null +ufw allow 443 comment HTTPS/3 >/dev/null +ufw allow 51820/udp comment Wireguard >/dev/null +ufw allow from 10.6.22.0/24 proto udp to any port 53 comment Unbound >/dev/null +ufw allow from fd6e:4f68:5f03:fffe::1/64 proto udp to any port 53 comment Unbound >/dev/null + +# Enable system services +systemctl -q enable \ + systemd-resolved.service systemd-networkd.service sshd.service \ + ufw.service unbound.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 \ + certbot-renew.timer + +# Pacman hooks +# pacman.conf hook +cat >/etc/pacman.d/hooks/100-pacman.conf.hook </etc/pacman.d/hooks/85-mkinitcpio.conf.hook </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 + +# 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 /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 + + # 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 +fi +syu +CHROOT + +## Pre and post update backup hooks +# Boot +cat >/mnt/etc/pacman.d/hooks/55-bootbackup_pre.hook <
/mnt/etc/pacman.d/hooks/95-bootbackup_post.hook </mnt/etc/pacman.d/hooks/01-syu_pre.hook <
/mnt/etc/pacman.d/hooks/zz-syu_post.hook <>/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
\ No newline at end of file
diff --git a/notes/sshkeys.pub b/notes/sshkeys.pub
new file mode 100644
index 0000000..b609bda
--- /dev/null
+++ b/notes/sshkeys.pub
@@ -0,0 +1,12 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHGQ2rLYB6U2i3dyb1+Fn8fKSsfsTno87Vf++yFQkD2k user@zenbook
+
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPJnaY1RtJ+JdfAEJfUcO99yrSGuH0UQit0itzrpgeI user@dotfiles
+
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA5Zwmxt3kTIZT9fsQW+NCcTRYFz97Qp+hXbj7AcJXi6 root@mini
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIES0RiFcV2yoLwNrK6iB3xU3OlQ85vAWgxjoNaG3iuMp root@phone
+
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDUz2KdC9MWYLwYgGfjdxPGd0XZbdRLUJog4IbWp9EZl user@myvelabs
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGlaVV3oZo/IAGXF3F9qdaiSRTQLzc2aJ50h3MNdLUmI root@myvelabs
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIAnDy15V5hT9+TX/gZeEpStamJOJNpruKdx3PXpcIX6 user@docker
+
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDmgC/c0Rjyk6sL+PXhuxOfaBVw/sPTrWfX5GBGwqq/g user@desktop
diff --git a/notes/ubuntu-nginx b/notes/ubuntu-nginx
new file mode 100644
index 0000000..9537a75
--- /dev/null
+++ b/notes/ubuntu-nginx
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+# Ubuntu - nginx-mainline
+# https://nginx.org/en/linux_packages.html
+
+# Install the prerequisites:
+
+    sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring -y
+
+# Import an official nginx signing key so apt could verify the packages authenticity. Fetch the key:
+
+    curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
+        | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
+
+# Verify that the downloaded file contains the proper key:
+
+    gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg \
+        | grep -q 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 || exit 1
+
+# The output should contain the full fingerprint 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 as follows:
+# 
+#     pub   rsa2048 2011-08-19 [SC] [expires: 2027-05-24]
+#           573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
+#     uid                      nginx signing key 
+
+# Note that the output can contain other keys used to sign the packages.
+
+# # To set up the apt repository for stable nginx packages, run the following command:
+# 
+#     echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
+#     http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
+#         | sudo tee /etc/apt/sources.list.d/nginx.list
+
+# If you would like to use mainline nginx packages, run the following command instead:
+
+    echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
+    http://nginx.org/packages/mainline/ubuntu `lsb_release -cs` nginx" \
+        | sudo tee /etc/apt/sources.list.d/nginx.list
+
+# Set up repository pinning to prefer our packages over distribution-provided ones:
+
+    echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
+        | sudo tee /etc/apt/preferences.d/99nginx
+
+# To install nginx, run the following commands:
+
+    sudo apt update
+    sudo apt install nginx -y
\ No newline at end of file
diff --git a/notes/wireguard b/notes/wireguard
new file mode 100644
index 0000000..d96e0b9
--- /dev/null
+++ b/notes/wireguard
@@ -0,0 +1,73 @@
+############
+# Key generation
+############
+for peer in {0..9}
+do
+    wg genkey | (umask 0077 && tee peer${peer}.key) | wg pubkey > peer${peer}.pub
+done
+
+############
+# Server configuration
+############
+/etc/wireguard/wg0.conf
+############
+[Interface]
+Address = 10.200.200.1/24
+ListenPort = 51820
+PrivateKey = SERVER_PRIVATE_KEY
+
+# substitute eth0 in the following lines to match the Internet-facing interface
+# the FORWARD rules will always be needed since traffic needs to be forwarded between the WireGuard
+# interface and the other interfaces on the server.
+# if the server is behind a router and receives traffic via NAT, specify static routing back to the
+# 10.200.200.0/24 subnet, the NAT iptables rules are not needed but the FORWARD rules are needed.
+# if the server is behind a router and receives traffic via NAT but one cannot specify static routing back to
+# 10.200.200.0/24 subnet, both the NAT and FORWARD iptables rules are needed. 
+PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
+PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
+
+[Peer]
+# foo
+PublicKey = PEER_FOO_PUBLIC_KEY
+PresharedKey = PRE-SHARED_KEY
+AllowedIPs = 10.200.200.2/32
+
+[Peer]
+# bar
+PublicKey = PEER_BAR_PUBLIC_KEY
+PresharedKey = PRE-SHARED_KEY
+AllowedIPs = 10.200.200.3/32
+
+###
+# The interface may be brought up using wg-quick up wg0 respectively by starting and potentially enabling the interface via wg-quick@interface.service, e.g. wg-quick@wg0.service. To close the interface use wg-quick down wg0 respectively stop wg-quick@interface.service.
+###
+
+############
+# Client configuration
+############
+foo.conf
+[Interface]
+Address = 10.200.200.2/32
+PrivateKey = PEER_FOO_PRIVATE_KEY
+DNS = 10.200.200.1
+
+[Peer]
+PublicKey = SERVER_PUBLICKEY
+PresharedKey = PRE-SHARED_KEY
+Endpoint = my.ddns.example.com:51820
+AllowedIPs = 0.0.0.0/0, ::/0
+bar.conf
+[Interface]
+Address = 10.200.200.3/32
+PrivateKey = PEER_BAR_PRIVATE_KEY
+DNS = 10.200.200.1
+
+[Peer]
+PublicKey = SERVER_PUBLICKEY
+PresharedKey = PRE-SHARED KEY
+Endpoint = my.ddns.example.com:51820
+AllowedIPs = 0.0.0.0/0, ::/0
+
+###
+# Note: Users of NetworkManager, may need to enable the NetworkManager-wait-online.service and users of systemd-networkd may need to enable the systemd-networkd-wait-online.service to wait until devices are network-ready before attempting a WireGuard connection.
+###
\ No newline at end of file
diff --git a/passthrough.sh b/passthrough.sh
new file mode 100755
index 0000000..3523614
--- /dev/null
+++ b/passthrough.sh
@@ -0,0 +1,782 @@
+#!/usr/bin/env bash
+set -a
+set -E
+
+# SSH public keys
+sshkeys=''
+
+## Tput codes
+reset=$(tput sgr0)
+
+bold=$(tput bold)
+italic=$(tput sitm)
+blink=$(tput blink)
+
+black=${reset}$(tput setaf 0)
+red=${reset}$(tput setaf 1)
+green=${reset}$(tput setaf 2)
+yellow=${reset}$(tput setaf 3)
+blue=${reset}$(tput setaf 4)
+magenta=${reset}$(tput setaf 5)
+cyan=${reset}$(tput setaf 6)
+white=${reset}$(tput setaf 7)
+
+# 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}
+}
+
+# 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
+}
+
+# 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
+else
+    die 'No internet connectivity detected, plug in an ethernet cable or run \e[32miwd-connect\e[33m if using wifi and try again'
+fi
+
+function returnUUID
+{
+    for partuuid in \
+        $(blkid -s UUID -o value ${1}) \
+        $(lsblk -d -n -o uuid ${1}) \
+        $(ls -l -a /dev/disk/by-uuid | grep "$(echo ${1} | sed 's\/dev\\')" | awk '{print $9}')
+    do
+        if [ ${partuuid} ]
+        then
+            echo ${partuuid}
+            break
+        else
+            unset partuuid
+        fi
+    done
+
+    if [ -z ${partuuid} ]
+    then
+        die 'Unable to determine partition UUID'
+    fi
+}
+
+clear
+
+cat < ' kernel_choice
+    echo
+    if ! [[ ${kernel_choice} = [1-4] ]]
+    then
+        echo
+        say 'Invalid selection, type an option from 1 to 4' red
+    fi
+done
+case ${kernel_choice} in
+1)
+    linux_kernel='linux'
+    ;;
+2)
+    linux_kernel='linux-lts'
+    ;;
+3)
+    linux_kernel='linux-hardened'
+    ;;
+4)
+    linux_kernel='linux-zen'
+    ;;
+esac
+echo
+say "   You have chosen ${green}${linux_kernel}${reset} as a kernel" reset
+echo
+
+# Unmount if mounted
+if findmnt '/mnt' | grep -q -w '/mnt'
+then
+    umount -R /mnt
+fi
+
+# Shred installation drive
+say "Partitioning /dev/${installation_disk}" yellow bold
+wipefs -f -a /dev/${installation_disk}
+blkdiscard -f /dev/${installation_disk}
+if [ -d /sys/firmware/efi/efivars ]
+then
+    parted --script --align=optimal /dev/${installation_disk} \
+        mklabel gpt \
+        mkpart primary fat32 1MiB 100MiB \
+        mkpart primary ${partition} 100MiB 100% \
+        set 1 esp on
+    partprobe /dev/${installation_disk}
+    export {efivol,bootvol}=/dev/$(lsblk -l -o name | grep "${installation_disk}".*1$)
+    export {rootpart,rootvol}=/dev/$(lsblk -l -o name | grep "${installation_disk}".*2$)
+    echo
+
+    function bootLoader
+    {
+    # Systemd-boot
+        say "Configuring systemd-boot" yellow bold
+        bootctl --path=/boot install
+        echo -e 'title Arch Linux
+linux /vmlinuz-'${linux_kernel}'
+initrd /initramfs-'${linux_kernel}'.img
+options quiet root=UUID='$(returnUUID ${rootpart})' rw' > /boot/loader/entries/arch.conf
+    echo -e 'default arch
+timeout 0
+console-mode max
+editor no' > /boot/loader/loader.conf
+        systemdbootservice='systemd-boot-update.service'
+    }
+else
+    bios='syslinux'
+    parted --script --align=optimal /dev/${installation_disk} \
+        mklabel msdos \
+        mkpart primary ${partition} 1MiB 100% \
+        set 1 boot on || die 'Disk partitioning failed'
+    partprobe /dev/${installation_disk}
+    export {rootpart,rootvol}=/dev/$(lsblk -l -o name | grep "${installation_disk}".*1$)
+    echo
+
+    function bootLoader
+    {
+    # Syslinux
+    say "Configuring syslinux" yellow bold
+    syslinux-install_update -i -a -m
+    sed -i 's\LINUX ../vmlinuz-linux.*\LINUX ../vmlinuz-'${linux_kernel}'\g' /boot/syslinux/syslinux.cfg
+    sed -i 's/APPEND root.*/APPEND root=UUID='"$(returnUUID ${rootpart})"' rw quiet/g' /boot/syslinux/syslinux.cfg
+    sed -i 's/.*PROMPT.*/PROMPT 0/g' /boot/syslinux/syslinux.cfg
+    sed -i 's/.*TIMEOUT.*/TIMEOUT 0/g' /boot/syslinux/syslinux.cfg
+    sed -i 's/.*UI menu.*/#UI menu/g' /boot/syslinux/syslinux.cfg
+    }
+fi
+
+say "Configuring volumes" yellow bold
+yes | mkfs.ext4 ${rootvol}
+mount ${rootvol} /mnt
+
+if [ -d /sys/firmware/efi/efivars ]
+then
+    mkdir /mnt/boot
+    yes | mkfs.fat -F 32 ${efivol}
+    mount ${bootvol} /mnt/boot
+    findmnt '/mnt/boot' | grep -q -w "${bootvol}" || die 'Boot partition has not been mounted properly'
+    echo
+fi
+
+say "Installing Arch Linux base packages on /dev/${installation_disk}" yellow bold
+sed -i \
+    -e '/Color/c\Color' \
+    -e '/ParallelDownloads/c\ParallelDownloads = 10' \
+    -e 's#^\[core\]$\|^\[extra\]$#&\
+CacheServer = http://192.168.122.1:9090#' \
+    /etc/pacman.conf
+
+# Temporarily disable mkinitcpio
+ln -s -f /dev/null /etc/pacman.d/hooks/90-mkinitcpio-install.hook
+
+pacstrap -K /mnt ${linux_kernel} qemu-guest-agent base mkinitcpio sudo \
+        ${bios} reflector openssh rsync \
+        wireguard-tools systemd-resolvconf \
+        pacman-contrib bash-completion vim
+
+genfstab -U -p /mnt >> /mnt/etc/fstab
+grep -q 'UUID' /mnt/etc/fstab || die
+
+# Make custom directories
+mkdir -p /mnt/etc/pacman.d/hooks /mnt/usr/bin/local
+
+# pacman.conf hook
+install /dev/stdin /mnt/usr/bin/local/hook-pacman.conf <<hook
+#!/usr/bin/env bash
+if [ -f /etc/pacman.conf.pacnew ]
+then
+    sed -i \\
+        -e '/Color/c\Color' \\
+        -e '/ParallelDownloads/c\ParallelDownloads = 10' \\
+        -e 's#^\[core\]$\|^\[extra\]\$#&\\
+CacheServer = http://192.168.122.1:9090#' /etc/pacman.conf.pacnew
+    mv /etc/pacman.conf.pacnew /etc/pacman.conf
+fi
+hook
+
+cat >/mnt/etc/pacman.d/hooks/pacman.conf.hook <<pacman
+[Trigger]
+Operation = Install
+Operation = Upgrade
+Type = Package
+Target = pacman
+
+[Action]
+Description = Fixing pacman.conf
+When = PostTransaction
+Exec = /usr/bin/local/hook-pacman.conf
+pacman
+sed -i \
+    -e '/Color/c\Color' \
+    -e '/ParallelDownloads/c\ParallelDownloads = 10' \
+    -e 's#^\[core\]$\|^\[extra\]$#&\
+CacheServer = http://192.168.122.1:9090#' \
+    /mnt/etc/pacman.conf
+
+# 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
+rm /mnt/usr/share/mkinitcpio/hook.preset
+rsync -a /mnt/usr/lib/modules/*/vmlinuz /mnt/boot/vmlinuz-linux
+
+# mkinitcpio preset hook
+cat >/mnt/etc/pacman.d/hooks/linux.preset.hook <<preset
+[Trigger]
+Operation = Install
+Operation = Upgrade
+Type = Package
+Target = mkinitcpio
+
+[Action]
+Description = Updating mkinitcpio linux preset
+When = PostTransaction
+Exec = /usr/bin/local/hook-linux.preset
+preset
+install /dev/stdin /mnt/usr/bin/local/hook-linux.preset <<'hook'
+#!/usr/bin/env bash
+if [ -f /usr/share/mkinitcpio/hook.preset ]
+then
+    sed \
+        -e "s|%PKGBASE%|linux|g" \
+        -e "s/^fallback/#&/g" \
+        -e "s/ 'fallback'//" \
+        /usr/share/mkinitcpio/hook.preset >/etc/mkinitcpio.d/linux.preset
+    rm /usr/share/mkinitcpio/hook.preset
+fi
+hook
+
+arch-chroot /mnt mkinitcpio -P
+
+# Networking
+ln -s -f /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf
+rsync -a /etc/systemd/network/20-ethernet.network /mnt/etc/systemd/network
+
+arch-chroot /mnt /bin/bash <<"SYU"
+# Install bootloader
+echo
+bootLoader
+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 'Locale configured' yellow bold
+
+# Time zone
+ln -s -f /usr/share/zoneinfo/UTC /etc/localtime
+hwclock --systohc --utc
+say 'Time zone configured' yellow bold
+
+# Hostname
+echo ${hostname} >/etc/hostname
+cat >>/etc/hosts <<HOSTS
+127.0.0.1        localhost
+127.0.1.1        ${hostname}
+HOSTS
+say 'Hostname configured' yellow bold
+
+# User and superuser
+useradd -m -g users -G wheel -s /bin/bash user || die
+if [ ${userpass} ]
+then
+    printf '%s\n' ${userpass} ${userpass} | passwd user &>/dev/null
+    echo '%wheel ALL=(ALL:ALL) ALL' > /etc/sudoers.d/wheel
+else
+    echo -e '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/wheel
+    mkdir -p /etc/systemd/system/getty@tty1.service.d
+    echo '[Service]
+ExecStart=
+ExecStart=-/usr/bin/agetty --autologin user --noclear %I $TERM' > /etc/systemd/system/getty@tty1.service.d/override.conf
+fi
+if [ "$rootpass" ]
+then
+    printf '%s\n' "$rootpass" "$rootpass" | passwd &>/dev/null
+fi
+say "Configured superuser and user" yellow bold
+echo
+
+# Sysctl custom settings
+echo -e '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' > /etc/sysctl.d/99-sysctl.conf
+
+# iptables
+install /dev/stdin /usr/bin/local/iptables.hook <<IPTABLES
+#!/usr/bin/env bash
+
+iptables -N TCP
+iptables -N UDP
+
+iptables -P FORWARD DROP
+iptables -P OUTPUT ACCEPT
+iptables -P INPUT DROP
+
+iptables -A INPUT -s 192.168.122.0/24 -j ACCEPT
+
+iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+iptables -A INPUT -i lo -j ACCEPT
+iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
+iptables -A INPUT -p icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
+iptables -A INPUT -p udp -m conntrack --ctstate NEW -j UDP
+iptables -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP
+iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
+iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset
+iptables -A INPUT -j REJECT --reject-with icmp-proto-unreachable
+
+iptables-save -f \${1}
+IPTABLES
+/usr/bin/local/iptables.hook /etc/iptables/iptables.rules
+
+# iptables hook
+install /dev/stdin /usr/bin/local/hook-iptables.rules <<hook
+#!/usr/bin/env bash
+if [ -f /etc/iptables/iptables.rules.pacnew ]
+then
+    /usr/bin/local/iptables.hook /etc/iptables/iptables.rules.pacnew
+    mv /etc/iptables/iptables.rules.pacnew /etc/iptables/iptables.rules
+fi
+hook
+cat >/etc/pacman.d/hooks/iptables.rules.hook <<iptables
+[Trigger]
+Operation = Install
+Operation = Upgrade
+Type = Package
+Target = iptables*
+
+[Action]
+Description = Fixing iptables rules
+When = PostTransaction
+Exec = /usr/bin/local/hook-iptables.rules
+iptables
+
+# Paccache hook
+cat >/etc/pacman.d/hooks/paccache.hook <<'paccache'
+[Trigger]
+Operation = Upgrade
+Operation = Install
+Operation = Remove
+Type = Package
+Target = *
+
+[Action]
+Description = Cleaning pacman cache...
+When = PostTransaction
+Exec = /usr/bin/paccache -r
+paccache
+
+# locale.gen.pacnew hook
+install /dev/stdin /usr/bin/local/hook-localegen <<hook
+#!/usr/bin/env bash
+if [ -f /etc/locale.gen.pacnew ]
+then
+    sed -i '/#en_US.UTF-8 UTF-8/ s/#//' /etc/locale.gen.pacnew
+    mv /etc/locale.gen.pacnew /etc/locale.gen
+    locale-gen >/dev/null
+fi
+hook
+# locale.gen.pacnew hook
+cat >/etc/pacman.d/hooks/localegen.hook <<localegen
+[Trigger]
+Operation = Install
+Operation = Upgrade
+Type = Package
+Target = glibc
+
+[Action]
+Description = Fixing locale.gen
+When = PostTransaction
+Exec = /usr/bin/local/hook-localegen
+localegen
+
+# ssh
+cat >/etc/ssh/sshd_config.d/10-sshd.conf <<sshd
+PermitRootLogin no
+PasswordAuthentication no
+AuthenticationMethods publickey
+Protocol 2
+sshd
+cat >/etc/ssh/ssh_config.d/10-global.conf <<sshconfig
+# Preferred ciphers
+Ciphers aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com
+
+# Only use ipv4
+AddressFamily inet
+sshconfig
+
+# Polkit
+mkdir -p /etc/polkit-1/rules.d
+cat >/etc/polkit-1/rules.d/49-nopasswd_global.rules <<'polkit'
+/* Allow members of the wheel group to execute any actions
+ * without password authentication, similar to "sudo NOPASSWD:"
+ */
+polkit.addRule(function(action, subject) {
+    if (subject.isInGroup("wheel")) {
+        return polkit.Result.YES;
+    }
+});
+polkit
+
+# Custom pacman wrapper
+install /dev/stdin /usr/local/bin/syu <<syu
+#!/usr/bin/env bash
+
+sudo curl --fail -s -L wdas.sh/mirrorlist -o /etc/pacman.d/mirrorlist
+
+if checkupdates | grep -q 'archlinux-keyring'
+then
+    sudo pacman -Sy --noconfirm archlinux-keyring
+    echo
+fi
+if sudo pacman -Syu --noconfirm
+then
+    echo
+    sudo pacdiff
+fi
+syu
+
+# System services
+systemctl -q enable systemd-networkd.service systemd-resolved.service sshd.service iptables.service qemu-guest-agent.service ${systemdbootservice}
+
+su user <<"CHANGEUSER"
+# SSH
+ssh-keygen -t ed25519 -q -f ~/.ssh/id_ed25519 -P ""
+echo "${sshkeys[@]}" > ~/.ssh/authorized_keys
+
+# bash
+# cp /etc/skel/.bash* ~/
+
+cat >> ~/.bashrc <<'BASHRC'
+
+unset HISTFILE
+
+alias ll='ls -l -a -h'
+
+# Grep color
+alias grep='grep --color=auto'
+alias egrep='egrep --color=auto'
+alias fgrep='fgrep --color=auto'
+
+# Adjust terminal upon window resize
+shopt -s checkwinsize
+
+# Auto cd into directory
+shopt -s autocd
+
+# Ignore duplicate and whitespace history entries
+export HISTCONTROL=ignoreboth
+
+#
+# ~/.bash_aliases
+#
+
+# Reboot and poweroff
+alias poweroff='sudo poweroff'
+alias reboot='sudo reboot'
+
+# Miscellanous pacman
+alias orphans='pacman -Rcns $(pacman -Qtdq)'
+alias unlockpacmandb='rm /var/lib/pacman/db.lck && pacman -Syy'
+
+# Rsync
+alias rsync='rsync --progress --info=progress2 -v -h'
+
+#
+# ~/.bash_functions
+#
+
+# Pacman tools
+function installer
+{
+    sudo pacman -S "$@"
+    echo
+}
+
+function uninstall
+{
+    sudo pacman -Rcns "$@"
+    echo
+}
+
+function syur
+{
+    /usr/local/bin/syu &&
+    reboot
+}
+
+function syup
+{
+    /usr/local/bin/syu &&
+    poweroff
+}
+
+# Update configs
+function update-bash
+{
+    vim ~/.bashrc &&
+    source ~/.bashrc
+}
+
+BASHRC
+CHANGEUSER
+SYU
+
+# Script for installing desktop environment
+install /dev/stdin /mnt/usr/local/bin/desktop <<'DESKTOPINSTALL'
+#!/usr/bin/env bash
+set -a -E
+
+# Exit function
+trap '[ "$?" -ne 77 ] || exit 77' ERR
+die()
+{
+    echo -e '\e[1;31m\nError encountered, script aborted...\n\e[0m'
+    exit 77
+}
+
+# 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
+    echo -e '\n\e[31mNo internet connectivity detected'
+    echo -e 'Connect to a network and try again'
+    echo -e 'Aborting installer...\e[0m\n'
+    die
+fi
+
+echo -e '\n\e[1;35mSelect a desktop\e[0m'
+echo -e '\t1) i3'
+echo -e '\t2) none'
+    until [[ ${desktop} = [12] ]]
+    do
+        read -n 1 -p '> ' desktop
+        [[ ${desktop} = [12] ]] || echo -e '\n\n\e[1;31mInvalid selection, type an option from 1 to 2\e[0m'
+    done
+
+# Assign DE variables
+case ${desktop} in
+    1)
+        # Install and configure x
+        echo -e '\n\n\t\e[1mYou have chosen \e[32mi3\e[0m\e[1m desktop\e[0m'
+        echo -e '\n\e[1;35mInstalling base packages\e[0m'
+        sudo pacman -S --noconfirm spice-vdagent xf86-video-qxl xorg xorg-xinit phonon-qt5-gstreamer ttf-dejavu ttf-liberation noto-fonts noto-fonts-cjk noto-fonts-emoji firefox i3-gaps i3status i3lock dmenu lightdm lightdm-gtk-greeter pavucontrol konsole kate dolphin kompare breeze-icons
+        echo
+        echo 'exec i3' > ~/.xinitrc
+        echo -e '\nXDG_CURRENT_DESKTOP=gnome' | sudo tee -a /etc/environment >/dev/null
+
+        # i3
+        mkdir -p ~/.config/i3
+        curl -s -L https://raw.githubusercontent.com/i3/i3/next/etc/config | sed 's/exec i3-config-wizard/# &/' > ~/.config/i3/config
+        tee -a ~/.config/i3/config >/dev/null <<'I3CONFIG'
+
+exec xrandr --output $(xrandr -q | grep -w 'connected primary' | awk '{print $1}') --mode 1920x1080
+
+gaps inner 8
+gaps outer 4
+
+# for_window [class="^.*"] border pixel 2
+
+exec spice-vdagent
+I3CONFIG
+        # i3 function
+        echo -e '\n# i3 config
+function i3-config
+{
+    vim ~/.config/i3/config
+}' >> ~/.bash_functions
+        cat >> ~/.bashrc <<'BASHRC'
+
+# Autostart i3
+if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = 1 ]
+then
+    exec startx
+fi
+BASHRC
+        ;;
+    2)  echo
+        ;;
+esac
+
+sudo rm -f ${0}
+
+case ${desktop} in
+    1)
+        echo -e '\e[1;34mDesktop installed, press any key to load i3\e[5m...\e[0m\n'
+        read -n 1 -s
+        sudo systemctl -q enable --now lightdm.service
+        ;;
+    2)
+        echo -e '\e[1;34mSetup complete, press any key to continue\e[5m...\e[0m\n'
+        read -n 1 -s
+        ;;
+    *)
+        die
+        ;;
+esac
+DESKTOPINSTALL
+
+# Reboot only if script succeeded
+if /usr/bin/sh -c 'arch-chroot /mnt uname -a' | grep -q Linux
+then
+    umount -R /mnt
+    echo -e '\e[1;34mInstaller has completed and system drive has been unmounted'
+    echo -e 'Boot into the new system, connect to a network and install a DE by running \e[35mdesktop\e[34m in the terminal'
+    echo -e 'Rebooting...\n\e[0m'
+    reboot
+else
+    die
+fi
diff --git a/pkg/homelab b/pkg/homelab
new file mode 100644
index 0000000..2c6daeb
--- /dev/null
+++ b/pkg/homelab
@@ -0,0 +1,139 @@
+amd-ucode
+apache
+apcupsd
+archiso
+ark
+base
+bash-completion
+bemenu
+bemenu-wayland
+bluez
+bluez-utils
+breeze-icons
+bridge-utils
+brightnessctl
+btrfs-progs
+certbot
+certbot-nginx
+chromium
+core/linux
+dbus-broker-units
+dmenu
+dmidecode
+dnsmasq
+docker
+docker-buildx
+docker-compose
+dolphin
+dosfstools
+edk2-ovmf
+efibootmgr
+exfatprogs
+fail2ban
+fakeroot
+feh
+ffmpegthumbs
+filezilla
+firefox
+firefox-decentraleyes
+firefox-ublock-origin
+foot
+git
+gnome
+gnome-keyring
+grub
+grub-btrfs
+i3lock
+i3status
+i3-wm
+inetutils
+inotify-tools
+intel-ucode
+iptables-nft
+iwd
+jdk-openjdk
+jre-openjdk
+kate
+kcalc
+kde-cli-tools
+keepassxc
+kfind
+kompare
+konsole
+libreoffice-fresh
+libvncserver
+lightdm
+lightdm-gtk-greeter
+linux-firmware
+man-db
+mkinitcpio
+mkinitcpio-netconf
+mkinitcpio-tinyssh
+mpv
+networkmanager
+nextcloud-client
+nginx-mainline
+noto-fonts-cjk
+ntfs-3g
+nvidia-open
+nvidia-settings
+okular
+openbsd-netcat
+opendoas
+openssh
+pacman-contrib
+pam-u2f
+parted
+pavucontrol
+pipewire
+pipewire-audio
+pipewire-jack
+pipewire-pulse
+plasma
+pv
+qemu-desktop
+qemu-guest-agent
+reflector
+remmina
+rsync
+seatd
+shotwell
+smartmontools
+snap-pac
+snapper
+spice-vdagent
+sshfs
+sudo
+sudo-rs
+sway
+swaybg
+swayidle
+swaylock
+syslinux
+systemd-resolvconf
+thunderbird
+tinyssh
+torbrowser-launcher
+ttf-dejavu
+vde2
+veracrypt
+vim
+virt-manager
+virtualbox
+virtualbox-guest-iso
+virtualbox-guest-utils
+virtualbox-host-modules-arch
+wayland
+wayvnc
+wireguard-tools
+wireplumber
+wmctrl
+xf86-video-qxl
+xfce4
+xorg
+xorg-xinit
+yubico-pam
+yubikey-full-disk-encryption
+yubikey-personalization
+zfs-linux
+zfs-utils
diff --git a/pkg/qemu b/pkg/qemu
new file mode 100644
index 0000000..c8ba4e0
--- /dev/null
+++ b/pkg/qemu
@@ -0,0 +1,43 @@
+base
+bash-completion
+breeze-icons
+btrfs-progs
+chromium
+dmenu
+dolphin
+ffmpegthumbs
+firefox
+firefox-decentraleyes
+firefox-ublock-origin
+git
+i3lock
+i3status
+i3-wm
+kate
+kfind
+kompare
+konsole
+linux
+mkinitcpio
+opendoas
+openssh
+pacman-contrib
+pavucontrol
+pipewire
+pipewire-jack
+qemu-guest-agent
+reflector
+rsync
+spice-vdagent
+sudo
+sudo-rs
+syslinux
+systemd-resolvconf
+ttf-dejavu
+vim
+virtualbox-guest-utils
+wireguard-tools
+wireplumber
+xf86-video-qxl
+xorg
+xorg-xinit
diff --git a/recover.sh b/recover.sh
new file mode 100755
index 0000000..7d6a26d
--- /dev/null
+++ b/recover.sh
@@ -0,0 +1,347 @@
+#!/usr/bin/env bash
+set -a -E
+
+# Exit function
+trap '[ "${?}" -ne 77 ] || exit 77' ERR
+function die
+{
+    echo -e '\n\e[1;31mError encountered, script aborted...\e[0m\n'
+    exit 77
+}
+
+# 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
+    die 'No internet connectivity detected, plug in an ethernet cable or run \e[32miwd-connect\e[33m if using wifi and try again'
+fi
+
+clear
+
+echo -e '\e[3m## rev '${revision}'\e[0m\n'
+echo -e '\t\e[1;4;34mArch Linux Recovery\e[0m\n\n'
+echo -e '\e[1mSelect a system configuration from the options below\e[0m\n'
+echo -e '\t1) Basic'
+echo -e '\t2) Full Disk LUKS Encryption with EFISTUB'
+echo -e '\t3) Full Disk LUKS Encryption with Secured GRUB'
+echo -e '\t4) Yubikey Full Disk Encryption with EFISTUB'
+echo -e '\t5) Yubikey Full Disk Encryption with Secured GRUB'
+
+if ls -l /dev/disk/* | grep -q 'virtio\|QEMU'
+then
+    echo
+    until [[ ${arch} = [1-5] ]]
+    do
+        read -n 1 -p '> ' arch
+        [[ ${arch} = [1-5] ]] || echo -e '\n\n\e[1;31mInvalid selection, type an option from 1 to 5\e[0m'
+    done
+else
+    echo -e '\t6) Full Disk Encryption with Detached LUKS Header and Secured GRUB'
+    echo -e '\t7) Yubikey Encrypted Detached LUKS Header with Secured GRUB\n'
+    until [[ ${arch} = [1-7] ]]
+    do
+        read -n 1 -p '> ' arch
+        [[ ${arch} = [1-7] ]] || echo -e '\n\n\e[1;31mInvalid selection, type an option from 1 to 7\e[0m'
+    done
+fi
+
+# Prevent continuing unless yubikey is detected, only applies to yubikey setups
+if [[ ${arch} = [457] ]] && ! dmesg | grep -q -i YubiKey
+then
+    echo -e '\n\n\e[1mYubikey not detected'
+    echo -e 'Insert one to continue\e[5m...\e[0m'
+    until dmesg | grep -q -i YubiKey
+    do
+        sleep 1
+    done
+fi
+
+# Installation drive
+echo -e '\n\n\e[1;36mIdentify the system drive from the list of available devices below\e[0m'
+lsblk -d -o name,model,size,mountpoint | grep -v 'archiso'
+echo -e '\e[3m## SSD and HDD device format begins with "sd" or "hd" (sda, sdb, sd[*])'
+echo -e '## NVME and PCI device format is "nvme[*]n1" (nvme0n1, nvme1n1, nvme[*]n1)\e[0m'
+    while true
+    do
+        read -r -p 'Installation device: ' installation_disk
+            lsblk -o name | grep -q -w ${installation_disk} && [ ${installation_disk} ] && break
+            lsblk -d -o name,model,size,mountpoint | grep -v 'archiso'
+                [ -z ${installation_disk} ] && echo -e '\e[1;31mField cannot be empty, try again\e[0m' ||
+                echo -e '\e[1;31mInvalid selection or drive not available, try again\e[0m'
+    done
+echo -e '\n\e[1mInstallation drive set as \e[32m/dev/'${installation_disk}'\e[0m'
+echo
+
+# Detached boot drive
+if [[ ${arch} = [67] ]]
+then
+    echo -e '\e[1;34mIdentify the removable boot drive from the list of available devices below\e[0m'
+    lsblk -d -o name,model,size,mountpoint | grep -v "archiso\|${installation_disk}"
+    echo -e '\e[3m## Removable drives usually begin with "sd" (sda, sdb, sd[*])\e[0m'
+        while true
+        do
+            read -r -p 'Removable boot device: ' external_boot_disk
+                lsblk -o name | grep -v ${installation_disk} | grep -q -w ${external_boot_disk} && [ ${external_boot_disk} ] && break
+                lsblk -d -o name,model,size,mountpoint | grep -v "archiso\|${installation_disk}"
+                if [ -z ${external_boot_disk} ]
+                then
+                    echo -e '\e[1;31mField cannot be empty, try again\e[0m'
+                elif [ ${external_boot_disk} = ${installation_disk} ]
+                then
+                    echo -e '\e[1;31mBoot drive cannot be the same as the system drive, try again\e[0m'
+                else
+                    echo -e '\e[1;31mInvalid selection or drive not available, try again\e[0m'
+                fi
+        done
+    echo -e '\n\e[1mDetached boot drive set as \e[32m/dev/'${external_boot_disk}'\e[0m'
+    echo
+fi
+
+if [[ ${arch} = [457] ]]
+then
+    until [[ ${decrypt_root} = [yYnN] ]]
+    do
+        read -n 1 -p $'\n\e[1mDoes the yubikey automatically decrypt root upon boot (1FA)? (y/n): \e[0m' decrypt_root
+        [[ ${decrypt_root} = [yYnN] ]] || echo -e -n '\n\n\e[1;31mNot a valid answer, type "y" or "n"\e[0m'
+    done
+    echo
+fi
+
+# Functions
+function decryptRootLuks
+{
+    while true
+    do
+        unset lukspass
+
+        echo -e '\e[1;36mEnter the encryption passphrase for the system drive\e[0m'
+        until [ ${lukspass} ]
+        do
+            read -s -r -p 'Encryption password: ' lukspass
+            [ ${lukspass} ] || echo -e '\n\n\e[1;31mPassphrase field cannot be empty, try again\e[0m'
+        done
+
+        echo -e '\n\n\e[1;36mAttempting to decrypt '${rootpart}'\e[0m'
+        if printf '%s' "${lukspass}" | cryptsetup open -v ${rootpart} cryptroot ${luks_header}
+        then
+            unset lukspass
+            echo -e '\e[1;92mDecryption successful\e[0m'
+            break
+        else
+            echo -e '\e[1;31mPassphrase failed, try again\e[0m\n'
+        fi
+    done
+}
+
+function decryptRootYubikey
+{
+    echo
+    yes | pacman -Sy &>/dev/null
+
+    for package in yubikey-personalization yubikey-full-disk-encryption
+    do
+        if ! pacman -Q | grep -q -w ${package}
+        then
+            missing_yubikey_req+=(${package})
+        fi
+    done
+
+    if [ ${missing_yubikey_req} ]
+    then
+        yes | pacman -Sy ${missing_yubikey_req[@]}
+        unset missing_yubikey_req
+        echo
+    fi
+
+    # # Custom challenge slot
+    # sed -i '/YKFDE_CHALLENGE_SLOT/c\YKFDE_CHALLENGE_SLOT="1"' /etc/ykfde.conf
+
+    while true
+    do
+        unset lukspass
+
+        echo -e '\e[1;36mEnter the encryption passphrase for the system drive\e[0m'
+        until [ ${lukspass} ]
+        do
+            read -s -r -p 'Encryption password: ' lukspass
+            [ ${lukspass} ] || echo -e '\n\n\e[1;31mPassphrase field cannot be empty, try again\e[0m'
+        done
+
+        if [[ ${decrypt_root} = [yY] ]]
+        then
+            sed -i '/YKFDE_CHALLENGE=/c\YKFDE_CHALLENGE="'"$(printf '%q' ${lukspass})"'"'  /etc/ykfde.conf
+        fi
+
+        echo -e '\n\n\e[1;36mAttempting to decrypt '${rootpart}' with yubikey HMAC encryption\e[0m'
+        if printf '%s\n' "${lukspass}" | ykfde-open -d ${rootpart} -n cryptroot -- ${luks_header}
+        then
+            unset lukspass
+            echo -e '\e[1;92mDecryption successful\e[0m'
+            break
+        else
+            echo -e '\e[1;31mPassphrase failed, try again\e[0m\n'
+        fi
+    done
+}
+
+function decryptBootLuks
+{
+    while true
+    do
+        unset grubpass
+
+        echo -e '\e[1;36mEnter the encryption passphrase for the bootloader\e[0m'
+        until [ ${grubpass} ]
+        do
+            read -s -r -p 'Bootloader password: ' grubpass
+            [ ${grubpass} ] || echo -e '\n\n\e[1;31mPassword field cannot be empty, try again\e[0m'
+        done
+
+        echo -e '\n\n\e[1;36mAttempting to decrypt bootloader...\e[0m'
+        if printf '%s' "${grubpass}" | cryptsetup open ${bootpart} cryptboot
+        then
+            unset grubpass
+            echo -e '\e[1;92mDecryption successful\e[0m'
+            break
+        else
+            echo -e '\e[1;31mPassphrase failed, try again\e[0m\n'
+        fi
+    done
+}
+
+function configureVolumes
+{
+    echo -e '\n\e[1;36mMounting volumes\e[0m'
+
+    case $(blkid -s TYPE -o value ${rootvol}) in
+        zfs_member)
+            if ! zpool list | grep -q zroot
+            then
+                zpool import -R /mnt zroot -N -d ${rootvol}
+            fi
+            zfs mount zroot/ROOT
+            zfs mount -a
+            ;;
+        btrfs)
+            mount ${rootvol} /mnt
+            ;;
+        ext4)
+            mount ${rootvol} /mnt
+            mount ${bootvol} /mnt/boot
+            if [[ ${arch} = [3567] ]]
+            then
+                mount ${efivol} /mnt/boot/efi
+            fi
+            ;;
+    esac
+}
+
+# Grab disk-id of ${drive}
+installation_disk_id=$(ls -l /dev/disk/by-id/* | grep "${installation_disk}$" | grep 'wwn\|nvme-uuid\|nvme-nvme\|nvme-eui\|QEMU\|virtio\|VBOX' | awk '{print $9}' | head -1)
+external_boot_disk_id=$(ls -l /dev/disk/by-id/* | grep "${external_boot_disk}$" | grep 'wwn\|nvme-uuid\|nvme-nvme\|nvme-eui\|QEMU\|virtio\|VBOX\|usb' | awk '{print $9}' | head -1)
+
+# System configurations
+case ${arch} in
+    1) # Basic
+        export {efivol,bootvol}=${installation_disk_id}-part1
+        export {rootpart,rootvol}=${installation_disk_id}-part2
+        configureVolumes
+        ;;
+
+    2|4) # FDE with EFISTUB
+        export {efivol,bootvol}=${installation_disk_id}-part1
+        rootpart=${installation_disk_id}-part2
+        rootvol=/dev/mapper/cryptroot
+
+        case ${arch} in
+            2)
+                if [[ $(blkid -s TYPE -o value ${rootpart}) = "zfs_member" ]]
+                then
+                    zpool import -R /mnt zroot -N -d ${rootpart}
+                    printf '%s' "${lukspass}" | zfs load-key zroot
+                else
+                    decryptRootLuks
+                fi
+                ;;
+            4) decryptRootYubikey;;
+        esac
+        configureVolumes
+        ;;
+
+    3|5) # FDE with Secured GRUB
+        efivol=${installation_disk_id}-part1
+        bootpart=${installation_disk_id}-part2
+        rootpart=${installation_disk_id}-part3
+        rootvol=/dev/mapper/cryptroot
+        bootvol=/dev/mapper/cryptboot
+
+        case ${arch} in
+            3) decryptRootLuks;;
+            5) decryptRootYubikey;;
+        esac
+
+        decryptBootLuks
+        configureVolumes
+        ;;
+
+    6|7) # FDE with detached luks header
+        efivol=${external_boot_disk_id}-part1
+        bootpart=${external_boot_disk_id}-part2
+        rootpart=${installation_disk_id}
+        rootvol=/dev/mapper/cryptroot
+        bootvol=/dev/mapper/cryptboot
+        luks_header='--header /mnt/header.img'
+
+        decryptBootLuks
+        mount /dev/mapper/cryptboot /mnt
+        case ${arch} in
+            6) decryptRootLuks;;
+            7) decryptRootYubikey;;
+        esac
+        umount /mnt
+        configureVolumes
+        ;;
+
+esac
+
+echo -e '\e[1;92mSystem successfully mounted...\e[0m\n'
+
+if [[ $(blkid -s TYPE -o value ${rootvol}) = "btrfs" ]]
+then
+    install /dev/stdin /usr/local/bin/btrfs-mount <<- BTRFSSU
+#!/usr/bin/env bash
+btrfsopts=$(awk '$2=="/"' /mnt/etc/fstab | awk '{print $4}' | sed 's/,subvol=.*//')
+umount -R /mnt
+echo
+
+# Mount @root subvolume
+mount -o ${btrfsopts},subvol=@ ${rootvol} /mnt
+
+# Mount all subvolumes
+arch-chroot /mnt mount --all
+lsblk
+echo -e '\n\t\e[1;92mBtrfs subvolumes successful mounted!\e[0m\n'
+BTRFSSU
+
+    echo -e '\e[1mRun \e[92mbtrfs-mount\e[0m\e[1m to mount all the btrfs subvolumes\e[0m\n'
+fi
+
+if [[ ${arch} = [3567] ]]
+then
+    install /dev/stdin /usr/local/bin/re-grub <<REGRUB
+#!/usr/bin/env bash
+set -e
+
+if findmnt '/mnt/boot/efi' | grep -q "${efivol}" && findmnt '/mnt/boot' | grep -q "${bootvol}"
+then
+    arch-chroot /mnt /bin/bash <<'GRUB'
+        grub-install --target=x86_64-efi --bootloader-id=GRUB --efi-directory=/boot/efi --recheck
+        grub-mkconfig -o /boot/grub/grub.cfg
+        echo -e '\n\t\e[1;92mGrub successfuly re-installed!\e[0m\n'
+GRUB
+else
+    echo -e '\n\t\e[1;31mEFI partition not mounted, try again\e[0m\n'
+fi
+REGRUB
+
+    echo -e '\e[1mRun \e[92mre-grub\e[0m\e[1m to regenerate grub\e[0m\n'
+fi
diff --git a/virtual-machine.sh b/virtual-machine.sh
new file mode 100755
index 0000000..fb5baa5
--- /dev/null
+++ b/virtual-machine.sh
@@ -0,0 +1,912 @@
+#!/usr/bin/env bash
+set -a
+set -E
+revision=1.0h
+
+## Tput codes
+reset=$(tput sgr0)
+
+bold=$(tput bold)
+italic=$(tput sitm)
+blink=$(tput blink)
+
+black=${reset}$(tput setaf 0)
+red=${reset}$(tput setaf 1)
+green=${reset}$(tput setaf 2)
+yellow=${reset}$(tput setaf 3)
+blue=${reset}$(tput setaf 4)
+magenta=${reset}$(tput setaf 5)
+cyan=${reset}$(tput setaf 6)
+white=${reset}$(tput setaf 7)
+
+# 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}
+}
+
+# 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
+}
+
+# Repeat a command until it exits with code 0
+function repeat
+{
+	until ${@}
+	do
+		sleep 3
+	done
+}
+
+# Read 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
+		;;
+		-? | -h | --help )
+			cat <<- help
+			Parameters:
+			  -s, --ssh-key             Add SSH public key (enclosed in quotes)
+			  -?, -h, --help            This help screen
+			help
+			exit 0
+		;;
+		* ) die "Unknown flag: ${1}"
+		;;
+	esac
+	shift
+done
+
+# Ensure pacman.conf settings are properly set
+if [ ! -f /etc/pacman.conf.bkp ]
+then
+	cp /etc/pacman.conf /etc/pacman.conf.bkp
+fi
+sed -e "/Color/c Color" \
+	-e "/ParallelDownloads/c ParallelDownloads = 10" \
+	-e "/^\[core\]$\|^\[extra\]$/a CacheServer = http://192.168.122.1:9090" \
+	/etc/pacman.conf.bkp >/etc/pacman.conf
+
+# 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
+	curl --fail --silent https://git.myvelabs.com/lab/archlinux/raw/branch/master/mirrorlist -o /etc/pacman.d/mirrorlist || die "Unable to fetch latest mirrorlist"
+else
+	die 'No internet connectivity detected, try again'
+fi
+
+clear
+
+cat <<- title
+$(tput setaf 7 smul dim)## rev ${revision}${reset}
+${blue}${bold}
+    Arch VM
+${reset}
+title
+
+# Default systemd services
+systemd_services=(systemd-networkd.service systemd-resolved.service sshd.service iptables.service wireguard-startup.service)
+
+# Firmware
+if ls -l /dev/disk/* | grep -q VBOX
+then
+	linux_firmware+=(virtualbox-guest-utils)
+	systemd_services+=(vboxservice.service)
+	installation_disk=sda
+else
+	linux_firmware+=(qemu-guest-agent spice-vdagent)
+	systemd_services+=(qemu-guest-agent.service)
+	if ls -l /dev/disk/* | grep -q virtio
+	then
+		installation_disk=vda
+	elif ls -l /dev/disk/* | grep -q QEMU
+	then
+		installation_disk=sda
+	fi
+fi
+if lspci | grep VGA | grep -q QXL
+then
+	linux_firmware+=(xf86-video-qxl)
+fi
+
+# Generate username and hostname
+hostname=$(cat /dev/urandom | tr -d -c 'a-zA-Z' | fold -w 6 | head -n 1)
+
+# Unmount if mounted
+for mount in /mnt/boot /mnt
+do
+    until ! mount | grep -q ${mount}
+    do
+        umount ${mount}
+    done
+done
+
+say "Partitioning /dev/${installation_disk}" yellow bold
+wipefs -f -a /dev/${installation_disk}
+echo
+if [ -d /sys/firmware/efi/efivars/ ]
+then
+	parted --script --align=optimal /dev/${installation_disk} \
+		mklabel gpt \
+		mkpart boot 1MiB 100MiB \
+		mkpart zroot 100MiB 100% \
+		set 1 esp on
+	partprobe /dev/${installation_disk}
+else
+	bios=syslinux
+	parted --script --align=optimal /dev/${installation_disk} \
+		mklabel msdos \
+		mkpart primary 1MiB 100MiB \
+		mkpart primary 100MiB 100% \
+		set 1 boot on
+	partprobe /dev/${installation_disk}
+fi
+
+# Assign partitions
+export {efivol,bootvol}=/dev/$(lsblk -l -o name | grep "${installation_disk}.*1$")
+export {rootpart,rootvol}=/dev/$(lsblk -l -o name | grep "${installation_disk}.*2$")
+
+say "Configuring volumes" yellow bold
+yes | mkfs.ext4 ${rootvol}
+mount -o noatime ${rootvol} /mnt
+
+# Boot partition
+yes | mkfs.fat -F 32 ${efivol}
+mount --mkdir -o uid=0,gid=0,fmask=0077,dmask=0077 ${bootvol} /mnt/boot
+echo
+
+# Temporarily disable mkinitcpio
+ln -s -f /dev/null /etc/pacman.d/hooks/90-mkinitcpio-install.hook
+
+# Install Arch on new root
+say "Installing Arch Linux on /dev/${installation_disk}" yellow bold
+archpkgs="sudo-rs fakeroot \
+	${linux_firmware[@]} ${bios} \
+	reflector openssh vim \
+	git rsync pacman-contrib bash-completion ffmpegthumbs \
+	xorg xorg-xinit \
+	i3-wm i3status dmenu konsole kate dolphin kompare breeze-icons kde-cli-tools ttf-dejavu \
+	firefox firefox-decentraleyes firefox-ublock-origin \
+	chromium \
+	wireguard-tools systemd-resolvconf \
+	noto-fonts-cjk \
+	iptables-nft pipewire-jack \
+	openbsd-netcat debugedit"
+
+# Pacstrap
+repeat pacstrap -K /mnt --ask 4 \
+	linux base mkinitcpio \
+	${archpkgs}
+	echo
+
+# Generate fs table
+genfstab -U -p /mnt >>/mnt/etc/fstab
+
+# Make custom directories
+mkdir -p /mnt/etc/{pacman.d/hooks,makepkg.conf.d,systemd/journald.conf.d}/ \
+	/mnt/opt/local/{bin,hooks}/
+
+# makepkg
+cat >/mnt/etc/makepkg.conf.d/zz-makepkg.conf <<- makepkg
+PKGEXT=".pkg.tar"
+MAKEFLAGS="--jobs=$(nproc)"
+COMPRESSZST=(zstd -c -T0 --auto-threads=logical -)
+makepkg
+
+# pacman.conf hook
+install /dev/stdin /mnt/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\]$\|^\[extra\]$/a CacheServer = http://192.168.122.1:9090' \
+        /etc/pacman.conf.pacnew >/etc/pacman.conf
+    rm /etc/pacman.conf.pacnew
+fi
+hook
+cat >/mnt/etc/pacman.d/hooks/100-pacman.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
+
+# Configure pacman cache server
+sed -e '/ParallelDownloads/c ParallelDownloads = 10' \
+	-e '/Color/c Color' \
+	-e '/^\[core\]$\|^\[extra\]$/a CacheServer = http://192.168.122.1:9090' \
+	-i /mnt/etc/pacman.conf
+
+# Mkinitcpio decompress
+echo 'MODULES_DECOMPRESS="yes"' >/mnt/etc/mkinitcpio.conf.d/zz-modules.conf
+
+# 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
+
+# Networking
+ln -s -f /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf
+rsync -a /etc/systemd/network/20-ethernet.network /mnt/etc/systemd/network/
+
+if [ -d /sys/firmware/efi/efivars/ ]
+then
+	# Systemd-boot
+	say "Configuring systemd-boot" yellow bold
+	arch-chroot -S /mnt bootctl --path=/boot/ install
+	cat >/mnt/boot/loader/entries/arch.conf <<- systemd-boot
+	title Arch Linux
+	linux /vmlinuz-linux
+	initrd /initramfs-linux.img
+	options root=${rootpart} rw quiet
+	systemd-boot
+	cat >/mnt/boot/loader/loader.conf <<- entry
+	default arch
+	timeout 0
+	console-mode max
+	editor no
+	entry
+	systemd_services+=(systemd-boot-update.service)
+else
+	# Syslinux
+	say "Configuring syslinux" yellow bold
+	mkdir /mnt/boot/syslinux/
+	arch-chroot /mnt extlinux --install /boot/syslinux/
+	arch-chroot /mnt syslinux-install_update -i -a -m
+	cat >/mnt/boot/syslinux/syslinux.cfg <<-syslinux
+	DEFAULT arch
+	PROMPT 0
+	TIMEOUT 0
+
+	MENU TITLE Arch Linux
+	MENU COLOR border       30;44   #40ffffff #a0000000 std
+	MENU COLOR title        1;36;44 #9033ccff #a0000000 std
+	MENU COLOR sel          7;37;40 #e0ffffff #20ffffff all
+	MENU COLOR unsel        37;44   #50ffffff #a0000000 std
+	MENU COLOR help         37;40   #c0ffffff #a0000000 std
+	MENU COLOR timeout_msg  37;40   #80ffffff #00000000 std
+	MENU COLOR timeout      1;37;40 #c0ffffff #00000000 std
+	MENU COLOR msg07        37;40   #90ffffff #a0000000 std
+	MENU COLOR tabmsg       31;40   #30ffffff #00000000 std
+
+	LABEL arch
+	    MENU LABEL Arch Linux
+	    LINUX ../vmlinuz-linux
+	    APPEND root=${rootpart} rw quiet
+	    INITRD ../initramfs-linux.img
+	LABEL hdt
+	    MENU LABEL HDT (Hardware Detection Tool)
+	    COM32 hdt.c32
+	LABEL reboot
+	    MENU LABEL Reboot
+	    COM32 reboot.c32
+	LABEL poweroff
+	    MENU LABEL Poweroff
+	    COM32 poweroff.c32
+	syslinux
+fi
+echo
+
+arch-chroot /mnt /usr/bin/bash <<- "CHROOT"
+# Root bashrc
+cat >> /etc/skel/.bashrc <<- 'bashrc'
+
+# Add local functions folder to path
+export PATH=${PATH}:/opt/local/bin
+
+# Source bash functions
+if [ -d ~/.local/functions ]
+then
+    for file in $(find ~/.local/functions -type f)
+    do
+        . ${file}
+    done
+fi
+
+# Disable history
+unset HISTFILE
+rm -f ${HISTFILE}
+history -c -w
+
+# Rsync
+alias rsync='rsync --progress --info=progress2 -v -h'
+bashrc
+cp /etc/skel/.bashrc ~/
+
+# 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 'Locale configured' yellow bold
+
+# Time zone
+ln -s -f $(find /usr/share/zoneinfo/ | shuf -n 1) /etc/localtime
+hwclock --systohc --utc
+say 'Time zone configured' yellow bold
+
+# Hostname
+echo ${hostname} >/etc/hostname
+cat >>/etc/hosts <<- HOSTS
+127.0.0.1		localhost
+127.0.1.1		${hostname}
+HOSTS
+say 'Hostname configured' yellow bold
+
+# User and superuser
+useradd -m -g users -G wheel -s /usr/bin/bash user || die 'User account creation has failed'
+# echo -e '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' >/etc/sudoers.d/zz-NOPASSWD
+# 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
+cat >/etc/pam.d/sudo <<- 'sudo'
+#%PAM-1.0
+auth		include		system-auth
+account		include		system-auth
+session		include		system-auth
+sudo
+ln -s -f /etc/pam.d/sudo /etc/pam.d/sudo-i
+install -m 0440 /dev/stdin /etc/sudoers-rs <<- 'DEFAULTS'
+Defaults!/usr/bin/visudo env_keep += "SUDO_EDITOR EDITOR VISUAL"
+Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/bin"
+root ALL=(ALL:ALL) ALL
+%wheel ALL=(ALL:ALL) NOPASSWD: ALL
+DEFAULTS
+ln -s -f /usr/bin/sudo-rs /usr/local/bin/sudo
+
+# Disable root account
+passwd -dl root >/dev/null 2>&1
+
+mkdir -p /etc/systemd/system/getty@tty1.service.d/
+cat >/etc/systemd/system/getty@tty1.service.d/override.conf <<- 'autologin'
+[Service]
+ExecStart=
+ExecStart=-/usr/bin/agetty --autologin user --noclear %I $TERM
+autologin
+say "Configured root user" yellow bold
+echo
+
+say "Regenerating cpio image" yellow bold
+mkinitcpio -P
+echo
+
+# 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
+
+# Sysctl custom settings
+cat >/etc/sysctl.d/10-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
+vm.swappiness=10
+vm.vfs_cache_pressure=50
+SYSCTL
+
+# iptables
+cat >/etc/iptables/userinput.rules <<- IPTABLES
+
+## User input
+# Accept all connections from hypervisor subnet
+-A INPUT -s 192.168.122.0/24 -j ACCEPT
+
+## Simple Firewall
+IPTABLES
+sed "/OUTPUT ACCEPT/r /etc/iptables/userinput.rules" /etc/iptables/simple_firewall.rules >/etc/iptables/iptables.rules
+
+# iptables hook
+install /dev/stdin /opt/local/hooks/iptables <<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.pacsave
+    mv /etc/iptables/iptables.rules.pacsave /etc/iptables/iptables.rules
+fi
+hook
+cat >/etc/pacman.d/hooks/100-iptables.hook <<iptables
+[Trigger]
+Operation = Install
+Operation = Upgrade
+Type = Package
+Target = iptables-nft
+
+[Action]
+Description = Fixing iptables rules
+When = PostTransaction
+Exec = /opt/local/hooks/iptables
+iptables
+
+# locale.gen.pacnew hook
+install /dev/stdin /opt/local/hooks/localegen <<hook
+#!/usr/bin/env bash
+if [ -f /etc/locale.gen.pacnew ]
+then
+    sed -i '/#en_US.UTF-8 UTF-8/ s/#//' /etc/locale.gen.pacnew
+    mv /etc/locale.gen.pacnew /etc/locale.gen
+    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
+
+# 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
+preset
+install /dev/stdin /opt/local/hooks/mkinitcpio <<'hook'
+#!/usr/bin/env bash
+# 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
+
+# Wireguard
+install /dev/stdin /usr/local/bin/wireguard-startup <<- 'wireguard'
+#!/usr/bin/env bash
+wireguard=$(ls /etc/wireguard | shuf -n 1 | sed 's/.conf//')
+/usr/bin/wg-quick up ${wireguard}
+/usr/bin/wg-quick down ${wireguard}
+/usr/bin/wg-quick up ${wireguard}
+wireguard
+cat >/etc/systemd/system/wireguard-startup.service <<- 'wireguard'
+[Unit]
+Description=Start wireguard on boot
+
+[Service]
+ExecStart=/usr/local/bin/wireguard-startup
+
+[Install]
+WantedBy=multi-user.target
+wireguard
+
+# SSH
+cat >/etc/ssh/sshd_config.d/zz-sshd.conf <<sshd
+PermitRootLogin no
+PasswordAuthentication no
+AuthenticationMethods publickey
+sshd
+
+cat >/etc/ssh/ssh_config.d/zz-ssh.conf <<sshconfig
+# Preferred ciphers
+Ciphers aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com
+
+# Only use ipv4
+AddressFamily inet
+sshconfig
+
+# Polkit
+mkdir -p /etc/polkit-1/rules.d/
+cat >/etc/polkit-1/rules.d/49-nopasswd_global.rules <<'polkit'
+/* Allow members of the wheel group to execute any actions
+ * without password authentication, similar to "sudo NOPASSWD:"
+ */
+polkit.addRule(function(action, subject) {
+    if (subject.isInGroup("wheel")) {
+        return polkit.Result.YES;
+    }
+});
+polkit
+
+# Persistent journal logging
+cat >/etc/systemd/journald.conf.d/zz-journald.conf <<- eof
+[Journal]
+Storage=persistent
+SystemMaxUse=100M
+eof
+
+# Default environment vars
+tee -a /etc/environment >/dev/null <<- environment
+EDITOR=vim
+SUDO_EDITOR=vim
+environment
+
+su user <<- "CHANGEUSER"
+# ssh identity
+ssh-keygen -q \
+    -t ed25519 \
+    -f ~/.ssh/id_ed25519 \
+    -C "user@${hostname}" \
+    -P ""
+
+# Config dirs
+mkdir -p ~/.local/functions/ \
+    ~/.config/{i3,menus}/ \
+    ~/.local/share/konsole/
+
+# i3-config
+curl --fail --silent https://raw.githubusercontent.com/i3/i3/next/etc/config | sed 's/exec i3-config-wizard/# &/' > ~/.config/i3/config
+cat >> ~/.config/i3/config <<- 'i3'
+
+exec xrandr --output $(xrandr -q | grep -w 'connected primary' | awk '{print $1}') --mode 1920x1080
+exec xrandr --dpi 192
+
+exec spice-vdagent
+exec VBoxClient-all
+i3
+
+# Konsole
+cat > ~/.config/konsolerc <<- konsole
+[Desktop Entry]
+DefaultProfile=profile.profile
+
+[General]
+ConfigVersion=1
+
+[UiSettings]
+ColorScheme=
+konsole
+cat >~/.local/share/konsole/profile.profile <<- 'profile'
+[Appearance]
+Font=monospace,16,-1,2,400,0,0,0,0,0,0,0,0,0,0,1
+
+[General]
+Name=profile
+Parent=FALLBACK/
+
+[Scrolling]
+HistoryMode=2
+profile
+
+# 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
+
+# Autostart i3
+cat >> ~/.bash_profile <<- 'autostart'
+
+if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = 1 ]
+then
+    exec startx /usr/bin/i3
+fi
+autostart
+
+cat > ~/.local/functions/i3 <<- 'i3'
+# Edit i3 config
+function i3-config
+{
+    vim ~/.config/i3/config
+}
+i3
+
+cat > ~/.local/functions/alias <<- 'alias'
+#
+# ~/.bash_aliases
+#
+
+# ls
+alias ll='ls -l -a -h'
+
+# Grep color
+alias grep='grep --color=auto'
+alias egrep='egrep --color=auto'
+alias fgrep='fgrep --color=auto'
+
+# Ignore duplicate and whitespace history entries
+export HISTCONTROL=ignoreboth
+
+# Reboot and poweroff
+alias poweroff='sudo poweroff'
+alias reboot='sudo reboot'
+
+# Miscellanous pacman
+alias orphans='sudo pacman -Rcns $(pacman -Qtdq)'
+alias unlockpacmandb='sudo rm /var/lib/pacman/db.lck && sudo pacman -Syy'
+alias
+
+cat > ~/.local/functions/functions <<- 'functions'
+#
+# ~/.bash_functions
+#
+
+# Pacman tools
+function installer
+{
+    sudo pacman -S ${@}
+    echo
+}
+
+function uninstall
+{
+    sudo pacman -Rcns "$@"
+    echo
+}
+
+function syur
+{
+    /opt/local/bin/syu &&
+    reboot
+}
+
+function syup
+{
+    /opt/local/bin/syu &&
+    poweroff
+}
+
+# Update configs
+function update-bash
+{
+    vim ~/.bashrc &&
+    source ~/.bashrc
+}
+functions
+
+cat > ~/.local/functions/wireguard <<- 'wg'
+# Wireguard
+function wglist
+{
+    if [ ${1} ]
+    then
+        sudo ls /etc/wireguard | grep ${1}
+    else
+        sudo ls /etc/wireguard
+    fi
+}
+
+function wgupdate
+{
+    if ls | grep -q ".*\.conf"
+    then
+        sudo find /etc/wireguard -type f -delete
+        sudo cp -v * /etc/wireguard
+        echo -e '\n\t\e[1;32mWireguard profiles updated\e[0m\n'
+    fi
+}
+
+function wd
+{
+    for wireguard in $(sudo wg show | grep interface | awk '{print $2}')
+    do
+        sudo wg-quick down ${wireguard}
+    done
+}
+
+function wu
+{
+    wd
+    if [ ${1} ]
+    then
+        sudo wg-quick up $(sudo ls /etc/wireguard | grep ${1} | shuf -n 1 | sed -e 's/.conf//')
+    else
+        sudo wg-quick up $(sudo ls /etc/wireguard | shuf -n 1 | sed -e 's/.conf//')
+    fi
+}
+wg
+CHANGEUSER
+CHROOT
+
+# System services
+arch-chroot /mnt systemctl enable ${systemd_services[@]} || die "Unable to start systemd services"
+
+# SSH Authentication
+if [ -f sshkeys ]
+then
+    cat sshkeys >/mnt/home/user/.ssh/authorized_keys
+fi
+
+# Custom pacman wrapper
+install /dev/stdin /mnt/opt/local/bin/syu <<- 'syu'
+#!/usr/bin/env bash
+set -e
+
+# Fetch latest mirrors
+sudo reflector --age 24 --latest 10 --protocol https --save /etc/pacman.d/mirrorlist
+
+# Check for new packages and continue if found
+newpkg=($(checkupdates --nocolor | awk '{print $1}'))
+if [ "${newpkg}" ]
+then
+    # Sync pacman dbs
+    sudo pacman --ask 4 -Sy >/dev/null
+
+    # Update archlinux-keyring first
+    if [[ ${newpkg[@]} =~ "archlinux-keyring" ]]
+    then
+        sudo pacman --ask 4 -S archlinux-keyring
+        echo
+    fi
+
+    # Perform update if dependencies are satisfied
+    if sudo pacman --ask 4 -Syu --needed
+    then
+        echo
+        sudo pacdiff
+        exit 0
+    fi
+fi
+syu
+
+# Script for installing desktop environment
+install /dev/stdin /mnt/opt/local/bin/startup <<'DESKTOPINSTALL'
+#!/usr/bin/env bash
+# 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
+    sudo vim /opt/local/bin/startupscript
+    /opt/local/bin/startupscript
+    exit 0
+else
+    echo -e '\n\e[31mNo internet connectivity detected'
+    echo -e 'Connect to a network and try again'
+    echo -e 'Aborting installer...\e[0m\n'
+    exit 1
+fi
+DESKTOPINSTALL
+install /dev/stdin /mnt/opt/local/bin/startupscript <<'DESKTOPINSTALL'
+#!/usr/bin/env bash
+
+# AUR package list
+aur_list=(
+# jdownloader2
+# instaloader
+# czkawka-gui-bin
+# ledger-udev ledger-live-bin
+)
+
+function enable-audio
+{
+    sudo pacman -S --ask 4 pipewire pipewire-audio pipewire-pulse wireplumber
+    echo
+    systemctl --user --quiet enable --now wireplumber.service pipewire-pulse.service pipewire.service
+}
+
+function wireguard-setup
+{
+    if ! sudo wg show | grep -q interface
+    then
+        echo -e '\n\e[31mNo wireguard profile installed\e[0m\n'
+        exit 1
+    fi
+}
+
+function tor-installer
+{
+    sudo pacman -S --ask 4 torbrowser-launcher
+    echo
+    torbrowser-launcher
+}
+
+wireguard-setup
+tor-installer
+# enable-audio # uncomment to enable audio
+
+# AUR packages
+if [ ${aur_list} ]
+then
+    for package in ${aur_list[@]}
+    do
+        cd ~/
+        git clone https://aur.archlinux.org/${package}.git
+        cd ${package}/
+        makepkg -csi
+        echo
+        cd ~
+        rm -rf ${package}/
+    done
+fi
+
+# Add user to shared folder group if in virtualbox guest
+if ls -l /dev/disk/* | grep -q VBOX
+then
+    sudo gpasswd -a user vboxsf >/dev/null
+fi
+
+echo -e '\n\e[1;32mSupplementary installer completed, reboot one last time\e[0m'
+sudo rm -f /opt/local/bin/startup*
+sudo systemctl reboot
+DESKTOPINSTALL
+
+# Reboot only if script succeeded
+if arch-chroot /mnt uname -a | grep -q Linux
+then
+    for mount in /mnt/boot /mnt
+    do
+        until ! mount | grep -q ${mount}
+        do
+            umount ${mount}
+        done
+    done
+    echo -e '\e[1;34mInstaller has completed and system drive has been unmounted
+Boot into the new system, connect to a network and install a DE by running \e[35mdesktop\e[34m in the terminal
+Rebooting...\n\e[0m'
+    reboot
+else
+    die 'Something does not feel right'
+fi