#!/usr/bin/env bash # veilor-os installer — TUI wrapper around anaconda kickstart install. # Runs on tty1 in place of getty (live ISO boot path). # # Flow: # 1. ASCII banner # 2. Menu: Install / Live shell / Reboot / Power off # 3. If Install: collect answers via whiptail (disk, hostname, LUKS pw, # admin pw, locale) # 4. Generate /run/install/veilor-generated.ks from template + answers # 5. Exec anaconda --kickstart=/run/install/veilor-generated.ks # 6. On finish: reboot into installed system # # v0.5.0 — first cut. v0.5.1 swaps whiptail for gum (Go TUI, prettier). set -uo pipefail export TERM="${TERM:-linux}" LOG=/var/log/veilor-installer.log exec > >(tee -a "$LOG") 2>&1 banner() { clear cat << 'EOF' ▌ ▌▙▀▖▌ ▐▌▛▀▖▌ ▖▙▀▖▖▖ ▙▖▌█ ▐▖▟▘▙▄▘▌ ▌▌ ▙▟ ▘ veilor-os installer hardened. branded. yours. EOF echo "──────────────────────────────────────────" echo } require_tty() { if ! [[ -t 0 && -t 1 ]]; then echo "[ERR] veilor-installer must run on a real tty" >&2 exit 1 fi } main_menu() { local choice choice=$(whiptail --title "veilor-os" \ --menu "Welcome. What would you like to do?" 16 60 5 \ "1" "Install veilor-os to disk" \ "2" "Try live — desktop (KDE Plasma)" \ "3" "Try live — shell" \ "4" "Reboot" \ "5" "Power off" \ 3>&1 1>&2 2>&3) echo "$choice" } collect_answers() { local disk hostname luks_pw admin_pw locale local disks_list # ── Disk ── # Build "tag description" pairs for whiptail. Model strings have spaces # (e.g. "WD PC SN740"), so collapse model to underscores for menu. disks_list=$(lsblk -dpno NAME,SIZE,MODEL | grep -E '^/dev/(sd|nvme|vd|mmcblk)' | \ awk '{name=$1; size=$2; $1=""; $2=""; sub(/^ +/,""); gsub(/ /,"_"); model=$0; if(model=="")model="unknown"; print name, size"_"model}') if [[ -z $disks_list ]]; then whiptail --title "veilor-os" --msgbox "No installable disks found." 8 50 return 1 fi disk=$(whiptail --title "Select install disk" \ --menu "WARNING: selected disk will be ERASED." 18 70 8 \ $disks_list 3>&1 1>&2 2>&3) || return 1 # ── Hostname ── hostname=$(whiptail --title "Hostname" \ --inputbox "Set hostname:" 10 60 "veilor" \ 3>&1 1>&2 2>&3) || return 1 # Reject shell-special chars in passwords. Generated kickstart writes # them via heredoc + sed substitution; bare $, ", \, ` would corrupt # the ks line or partially expand. 8-char min for entropy. validate_pw() { local pw=$1 label=$2 if [[ ${#pw} -lt 8 ]]; then whiptail --title "Weak $label" --msgbox "Min 8 chars." 8 40 return 1 fi if [[ $pw =~ [\"\$\\\`] ]]; then whiptail --title "Invalid $label" --msgbox \ "Cannot contain: \" \$ \\ \`" 8 50 return 1 fi return 0 } # ── LUKS passphrase ── luks_pw=$(whiptail --title "Disk encryption" \ --passwordbox "LUKS passphrase (full-disk encryption):" 10 60 \ 3>&1 1>&2 2>&3) || return 1 validate_pw "$luks_pw" "passphrase" || return 1 # ── Admin password ── admin_pw=$(whiptail --title "Admin password" \ --passwordbox "Admin user password (login after install):" 10 60 \ 3>&1 1>&2 2>&3) || return 1 validate_pw "$admin_pw" "password" || return 1 # ── Locale ── locale=$(whiptail --title "Locale" \ --menu "Choose locale:" 14 50 4 \ "en_GB.UTF-8" "English (UK)" \ "en_US.UTF-8" "English (US)" \ "de_DE.UTF-8" "Deutsch" \ "fr_FR.UTF-8" "Francais" \ 3>&1 1>&2 2>&3) || return 1 # ── Confirmation ── whiptail --title "Confirm install" --yesno \ "About to install veilor-os: Disk: $disk (will be ERASED) Hostname: $hostname Locale: $locale LUKS: set Admin pw: set Proceed?" 16 60 || return 1 # Export to caller via globals SEL_DISK=$disk SEL_HOSTNAME=$hostname SEL_LUKS_PW=$luks_pw SEL_ADMIN_PW=$admin_pw SEL_LOCALE=$locale return 0 } generate_ks() { # Build kickstart for actual disk install. # NOTE: passwords go in via --plaintext to avoid storing crypted hash # collisions; anaconda hashes per /etc/login.defs at install time. local out=/run/install/veilor-generated.ks local disk_basename disk_basename=$(basename "$SEL_DISK") mkdir -p /run/install # Single-quoted heredoc → no shell expansion. Substitute placeholders # via sed afterwards. Bulletproof against $/`/" in passwords. cat > "$out" << 'KSEOF' || return 1 # veilor-os installer-generated kickstart # DO NOT commit this file — secrets inline. url --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-43&arch=x86_64" repo --name=fedora --baseurl="https://download.fedoraproject.org/pub/fedora/linux/releases/43/Everything/x86_64/os/" --install repo --name=updates --baseurl="https://download.fedoraproject.org/pub/fedora/linux/updates/43/Everything/x86_64/" --install keyboard --xlayouts='us' lang __LOCALE__ timezone Europe/London --utc firstboot --disable eula --agreed selinux --enforcing services --enabled=sshd,fail2ban,usbguard,tuned,auditd,firewalld,chronyd,sddm network --bootproto=dhcp --device=link --activate --hostname=__HOSTNAME__ firewall --enabled --service=ssh rootpw --lock user --name=admin --groups=wheel --gecos="veilor admin" --password=__ADMIN_PW__ --plaintext # Full hardening cmdline (installed system, not live): # --location=none: anaconda auto-places bootloader (UEFI grub2-efi or BIOS). bootloader --location=none --append="lockdown=integrity slab_nomerge init_on_alloc=1 init_on_free=1 randomize_kstack_offset=on vsyscall=none" # Disk: zero, LUKS2 (argon2id), btrfs subvolumes zerombr clearpart --all --initlabel --drives=__DISK_BASENAME__ part /boot/efi --fstype=efi --size=600 part /boot --fstype=ext4 --size=1024 part pv.veilor --grow --encrypted --luks-version=luks2 --pbkdf=argon2id --passphrase=__LUKS_PW__ volgroup veilor pv.veilor logvol / --vgname=veilor --name=root --fstype=btrfs --size=8192 --grow %packages --excludedocs @^kde-desktop-environment @kde-apps @core @hardware-support @standard fail2ban fail2ban-firewalld usbguard usbguard-tools audit policycoreutils-python-utils tuned chrony firewalld plymouth git vim-enhanced tmux htop podman NetworkManager NetworkManager-wifi fontconfig fira-code-fonts zram-generator -abrt* -snapd -kde-connect -mlocate %end # Reboot when done reboot KSEOF # Substitute placeholders. Use | as sed delimiter (passwords might # contain /). Forbidden chars in passwords (validated upstream): "$\` # — sed safe. sed -i \ -e "s|__LOCALE__|$SEL_LOCALE|" \ -e "s|__HOSTNAME__|$SEL_HOSTNAME|" \ -e "s|__DISK_BASENAME__|$disk_basename|" \ -e "s|__LUKS_PW__|$SEL_LUKS_PW|" \ -e "s|__ADMIN_PW__|$SEL_ADMIN_PW|" \ "$out" echo "[INFO] generated kickstart at $out" return 0 } run_install() { whiptail --title "Installing" --infobox \ "Installing veilor-os to $SEL_DISK ... This will take 10-30 minutes. Logs: /var/log/veilor-installer.log + /tmp/anaconda.log" 10 60 sleep 2 # Hand off to anaconda. --kickstart runs unattended. if anaconda --kickstart=/run/install/veilor-generated.ks; then whiptail --title "Done" --msgbox \ "Install complete. System will reboot. Remove the install media after shutdown." 10 50 sleep 3 systemctl reboot else whiptail --title "Install failed" --msgbox \ "Anaconda exited non-zero. Logs at /tmp/anaconda.log + /var/log/veilor-installer.log. Press OK to drop to shell." 12 60 return 1 fi } drop_to_shell() { clear cat << 'EOF' ═══════════════════════════════════════════════════ veilor-os live shell ═══════════════════════════════════════════════════ You are in a live, in-memory environment. Nothing persists across reboot. Re-run the installer: sudo veilor-installer Reboot: sudo systemctl reboot Power off: sudo systemctl poweroff EOF exec /bin/bash --login } # ── Entry ── require_tty banner launch_desktop() { clear echo "Launching KDE Plasma..." sleep 1 systemctl isolate graphical.target # systemd-isolate switches target; sddm spawns on tty1. # If user logs out, they come back here. Loop continues. } while true; do case "$(main_menu)" in 1) if collect_answers && generate_ks; then run_install || continue fi ;; 2) launch_desktop ;; 3) drop_to_shell ;; 4) systemctl reboot ;; 5) systemctl poweroff ;; *) drop_to_shell ;; esac done