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