Last active
April 8, 2026 09:08
-
-
Save Ensamisten/490168645794b144183e5ff46fbd645a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ArchISO guided installer for: | |
| # - Encrypted /boot (LUKS1) + encrypted LVM PV (LUKS) | |
| # - LVM: swap/root/home (home=100%FREE) | |
| # - GRUB with cryptodisk | |
| # - mkinitcpio custom install hook to embed keyfiles into initramfs | |
| # | |
| # WARNING: THIS DESTROYS THE SELECTED DISK. | |
| log() { printf "\n==> %s\n" "$*"; } | |
| warn() { printf "\nWARNING: %s\n" "$*" >&2; } | |
| die() { printf "\nERROR: %s\n" "$*" >&2; exit 1; } | |
| require_root() { [[ "$(id -u)" -eq 0 ]] || die "Run as root."; } | |
| require_luks_env() { | |
| if [[ -z "${LUKS_PASSPHRASE:-}" ]]; then | |
| die "LUKS_PASSPHRASE is not set. Run: export LUKS_PASSPHRASE='your-passphrase' then rerun." | |
| fi | |
| } | |
| require_cmds() { | |
| local missing=() | |
| for c in "$@"; do | |
| command -v "$c" >/dev/null 2>&1 || missing+=("$c") | |
| done | |
| if ((${#missing[@]})); then | |
| die "Missing commands: ${missing[*]}" | |
| fi | |
| } | |
| prompt() { | |
| local var="$1"; shift | |
| local text="$1"; shift | |
| local default="${1:-}" | |
| local input="" | |
| if [[ -n "$default" ]]; then | |
| read -r -p "$text [$default]: " input | |
| input="${input:-$default}" | |
| else | |
| read -r -p "$text: " input | |
| fi | |
| printf -v "$var" '%s' "$input" | |
| } | |
| confirm() { | |
| local text="$1" | |
| read -r -p "$text (type YES to continue): " ans | |
| [[ "$ans" == "YES" ]] || die "Aborted." | |
| } | |
| is_uefi() { [[ -d /sys/firmware/efi ]]; } | |
| part_path() { | |
| local disk="$1" num="$2" | |
| if [[ "$disk" =~ nvme|mmcblk ]]; then echo "${disk}p${num}"; else echo "${disk}${num}"; fi | |
| } | |
| pick_disk_menu() { | |
| log "Scanning disks..." | |
| mapfile -t DISK_LINES < <(lsblk -d -p -n -o NAME,SIZE,MODEL,TYPE | awk '$4=="disk"{print $1"|" $2"|" substr($0, index($0,$3))}') | |
| ((${#DISK_LINES[@]})) || die "No disks found." | |
| echo | |
| echo "Select target disk:" | |
| local i=1 | |
| for line in "${DISK_LINES[@]}"; do | |
| IFS='|' read -r name size rest <<<"$line" | |
| printf " %d) %s %s %s\n" "$i" "$name" "$size" "$rest" | |
| ((i++)) | |
| done | |
| echo | |
| local choice | |
| while true; do | |
| prompt choice "Enter choice number" | |
| [[ "$choice" =~ ^[0-9]+$ ]] || { warn "Please enter a number."; continue; } | |
| (( choice>=1 && choice<=${#DISK_LINES[@]} )) || { warn "Out of range."; continue; } | |
| break | |
| done | |
| IFS='|' read -r DISK _ _ <<<"${DISK_LINES[$((choice-1))]}" | |
| [[ -b "$DISK" ]] || die "Disk $DISK not found." | |
| echo | |
| lsblk -p "$DISK" || true | |
| confirm "ALL DATA ON $DISK WILL BE ERASED. Continue?" | |
| } | |
| wipe_disk() { | |
| local disk="$1" | |
| log "Wiping old signatures on $disk" | |
| wipefs -a "$disk" || true | |
| } | |
| partition_disk() { | |
| local disk="$1" mode="$2" # uefi|bios | |
| log "Partitioning $disk for $mode (GPT + gdisk)" | |
| if [[ "$mode" == "uefi" ]]; then | |
| gdisk "$disk" <<'EOF' | |
| o | |
| y | |
| n | |
| +512M | |
| ef00 | |
| n | |
| +1G | |
| 8300 | |
| n | |
| 8e00 | |
| w | |
| y | |
| EOF | |
| else | |
| gdisk "$disk" <<'EOF' | |
| o | |
| y | |
| n | |
| +1M | |
| ef02 | |
| n | |
| +1G | |
| 8300 | |
| n | |
| 8e00 | |
| w | |
| y | |
| EOF | |
| fi | |
| partprobe "$disk" || true | |
| sleep 1 | |
| } | |
| format_esp_if_needed() { | |
| local mode="$1" esp_part="$2" | |
| [[ "$mode" == "uefi" ]] || return 0 | |
| log "Formatting ESP as FAT32: $esp_part" | |
| mkfs.vfat -F 32 "$esp_part" | |
| } | |
| setup_luks_and_lvm() { | |
| local boot_part="$1" lvm_part="$2" | |
| log "Creating LUKS1 container for /boot on $boot_part" | |
| printf '%s' "$LUKS_PASSPHRASE" | cryptsetup -q --batch-mode --key-file=- \ | |
| -v --key-size 512 --type luks1 --hash sha256 --iter-time 5000 --use-random luksFormat "$boot_part" | |
| log "Creating LUKS container for LVM PV on $lvm_part" | |
| printf '%s' "$LUKS_PASSPHRASE" | cryptsetup -q --batch-mode --key-file=- \ | |
| -v --key-size 512 --hash sha256 --iter-time 5000 --use-random luksFormat "$lvm_part" | |
| log "Opening LUKS containers" | |
| cryptsetup open "$boot_part" encrypted-boot | |
| cryptsetup open "$lvm_part" encrypted-lvm | |
| log "Creating LVM PV/VG" | |
| pvcreate /dev/mapper/encrypted-lvm | |
| vgcreate Main /dev/mapper/encrypted-lvm | |
| echo | |
| warn "LV size inputs are passed to 'lvcreate -L'. Examples: 16G, 32768M" | |
| prompt SWAP_SIZE "Swap LV size" "16G" | |
| prompt ROOT_SIZE "Root LV size" "25G" | |
| log "Creating LVs: swap=$SWAP_SIZE root=$ROOT_SIZE home=100%FREE" | |
| lvcreate -L "$SWAP_SIZE" Main -n swap | |
| lvcreate -L "$ROOT_SIZE" Main -n root | |
| lvcreate -l 100%FREE Main -n home | |
| log "Creating filesystems (ext4) and swap" | |
| mkfs.ext4 /dev/mapper/Main-root | |
| mkfs.ext4 /dev/mapper/Main-home | |
| mkswap /dev/mapper/Main-swap | |
| log "Creating filesystem for encrypted /boot (ext4)" | |
| mkfs.ext4 /dev/mapper/encrypted-boot | |
| } | |
| mount_all() { | |
| local mode="$1" esp_part="${2:-}" | |
| log "Mounting filesystems to /mnt" | |
| mount /dev/mapper/Main-root /mnt | |
| mkdir -p /mnt/boot | |
| mount /dev/mapper/encrypted-boot /mnt/boot | |
| if [[ "$mode" == "uefi" ]]; then | |
| mkdir -p /mnt/boot/efi | |
| mount "$esp_part" /mnt/boot/efi | |
| fi | |
| mkdir -p /mnt/home | |
| mount /dev/mapper/Main-home /mnt/home | |
| swapon /dev/mapper/Main-swap | |
| } | |
| collect_i18n_inputs() { | |
| log "Interactive timezone/locale/keymap setup" | |
| prompt TIMEZONE "Timezone (exists under /usr/share/zoneinfo, e.g. Europe/Oslo, America/New_York)" "UTC" | |
| if [[ ! -e "/usr/share/zoneinfo/$TIMEZONE" ]]; then | |
| warn "Timezone '/usr/share/zoneinfo/$TIMEZONE' not found. Falling back to UTC." | |
| TIMEZONE="UTC" | |
| fi | |
| prompt LOCALE "Primary locale (e.g. en_US.UTF-8)" "en_US.UTF-8" | |
| prompt KEYMAP "Console keymap (e.g. us, no, de-latin1)" "us" | |
| } | |
| collect_microcode_choice() { | |
| log "CPU microcode package" | |
| echo "Select microcode to install:" | |
| echo " 1) intel-ucode (recommended for Intel CPUs)" | |
| echo " 2) amd-ucode" | |
| echo " 3) none" | |
| echo | |
| local c | |
| while true; do | |
| prompt c "Enter choice number" "1" | |
| case "$c" in | |
| 1) MICROCODE_PKG="intel-ucode"; break;; | |
| 2) MICROCODE_PKG="amd-ucode"; break;; | |
| 3) MICROCODE_PKG=""; break;; | |
| *) warn "Invalid choice. Pick 1, 2, or 3.";; | |
| esac | |
| done | |
| if [[ -n "${MICROCODE_PKG}" ]]; then | |
| log "Will install: ${MICROCODE_PKG}" | |
| else | |
| log "Will not install microcode package" | |
| fi | |
| } | |
| install_base() { | |
| local mode="$1" | |
| log "Installing base system with pacstrap" | |
| local pkgs=(base base-devel linux linux-firmware lvm2 vim grub mkinitcpio) | |
| if [[ "$mode" == "uefi" ]]; then pkgs+=(efibootmgr); fi | |
| if [[ -n "${MICROCODE_PKG:-}" ]]; then pkgs+=("$MICROCODE_PKG"); fi | |
| pacstrap /mnt "${pkgs[@]}" | |
| log "Generating fstab" | |
| genfstab -U /mnt >> /mnt/etc/fstab | |
| } | |
| configure_system_in_chroot() { | |
| local mode="$1" disk="$2" boot_part="$3" lvm_part="$4" | |
| local timezone="$5" locale="$6" keymap="$7" | |
| log "Entering chroot to configure system" | |
| arch-chroot /mnt /usr/bin/env LUKS_PASSPHRASE="$LUKS_PASSPHRASE" /bin/bash -euo pipefail <<EOF | |
| set -euo pipefail | |
| echo "Configuring timezone/clock" | |
| ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime | |
| hwclock --systohc | |
| echo "Configuring locale" | |
| if grep -qE "^#?$locale[[:space:]]" /etc/locale.gen; then | |
| sed -i "s/^#\\($locale[[:space:]].*\\)/\\1/" /etc/locale.gen | |
| else | |
| echo "$locale UTF-8" >> /etc/locale.gen | |
| fi | |
| locale-gen | |
| cat > /etc/locale.conf <<LC | |
| LANG=$locale | |
| LC | |
| echo "Configuring vconsole keymap" | |
| cat > /etc/vconsole.conf <<VC | |
| KEYMAP=$keymap | |
| VC | |
| echo "Configuring mkinitcpio hooks" | |
| cp -a /etc/mkinitcpio.conf /etc/mkinitcpio.conf.bak | |
| sed -i 's/^HOOKS=.*/HOOKS=(base udev keyboard keymap consolefont autodetect modconf block encrypt lvm2 resume decryption-keys filesystems fsck)/' /etc/mkinitcpio.conf | |
| mkdir -p /etc/initcpio/install | |
| cat > /etc/initcpio/install/decryption-keys <<'HOOK' | |
| #!/bin/bash | |
| build() { | |
| for file in /etc/initcpio/keys/*; do | |
| add_file "\$file" "/\$(basename "\$file")" 0400 | |
| done | |
| } | |
| HOOK | |
| chmod 0755 /etc/initcpio/install/decryption-keys | |
| echo "Creating keyfiles to embed into initramfs" | |
| mkdir -p /etc/initcpio/keys | |
| dd bs=512 count=8 iflag=fullblock if=/dev/urandom of=/etc/initcpio/keys/encrypted-boot.key | |
| dd bs=512 count=8 iflag=fullblock if=/dev/urandom of=/etc/initcpio/keys/encrypted-lvm.key | |
| chmod 0000 /etc/initcpio/keys/* | |
| chattr +i /etc/initcpio/keys/* | |
| echo "Adding keyfiles to LUKS keyslots (non-interactive via LUKS_PASSPHRASE)" | |
| printf '%s' "\$LUKS_PASSPHRASE" | cryptsetup -q --batch-mode --key-file=- luksAddKey "$boot_part" /etc/initcpio/keys/encrypted-boot.key | |
| printf '%s' "\$LUKS_PASSPHRASE" | cryptsetup -q --batch-mode --key-file=- luksAddKey "$lvm_part" /etc/initcpio/keys/encrypted-lvm.key | |
| echo "Building initramfs" | |
| mkinitcpio -p linux | |
| chmod 0400 /boot/initramfs-linux*.img || true | |
| echo "Adding pacman hook to chmod initramfs after kernel/initcpio updates" | |
| mkdir -p /etc/pacman.d/hooks | |
| cat > /etc/pacman.d/hooks/99-initramfs-chmod.hook <<'PAC' | |
| [Trigger] | |
| Type = File | |
| Operation = Install | |
| Operation = Upgrade | |
| Target = boot/vmlinuz-linux | |
| Target = usr/lib/initcpio/* | |
| [Action] | |
| Description = Setting proper permissions for linux initcpios... | |
| When = PostTransaction | |
| Exec = /usr/bin/chmod 0400 /boot/initramfs-linux.img /boot/initramfs-linux-fallback.img | |
| PAC | |
| echo "Configuring GRUB for cryptodisk + kernel cmdline" | |
| if grep -q '^#GRUB_ENABLE_CRYPTODISK=y' /etc/default/grub; then | |
| sed -i 's/^#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/' /etc/default/grub | |
| elif ! grep -q '^GRUB_ENABLE_CRYPTODISK=y' /etc/default/grub; then | |
| echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub | |
| fi | |
| LVM_UUID=\$(blkid -o value -s UUID "$lvm_part") | |
| SWAP_DEV=/dev/mapper/Main-swap | |
| ROOT_DEV=/dev/mapper/Main-root | |
| GRUB_CMDLINE="cryptdevice=UUID=\${LVM_UUID}:encrypted-lvm root=\${ROOT_DEV} resume=\${SWAP_DEV} cryptkey=rootfs:/encrypted-lvm.key" | |
| if grep -q '^GRUB_CMDLINE_LINUX=' /etc/default/grub; then | |
| sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\\"\${GRUB_CMDLINE}\\"|" /etc/default/grub | |
| else | |
| echo "GRUB_CMDLINE_LINUX=\\"\${GRUB_CMDLINE}\\"" >> /etc/default/grub | |
| fi | |
| echo "Installing GRUB bootloader ($mode)" | |
| if [[ "$mode" == "uefi" ]]; then | |
| mountpoint -q /boot/efi || { echo "/boot/efi is not mounted"; exit 1; } | |
| grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=grub --recheck | |
| else | |
| grub-install --target=i386-pc "$disk" | |
| fi | |
| grub-mkconfig -o /boot/grub/grub.cfg | |
| echo "Configuring /etc/crypttab to auto-open encrypted-boot after boot" | |
| BOOT_UUID=\$(blkid -o value -s UUID "$boot_part") | |
| cat > /etc/crypttab <<CT | |
| encrypted-boot UUID=\${BOOT_UUID} /etc/initcpio/keys/encrypted-boot.key luks | |
| CT | |
| echo "Done inside chroot." | |
| EOF | |
| } | |
| post_install_notes() { | |
| cat <<'NOTES' | |
| Next manual steps (not automated here): | |
| - Set hostname: echo myhost > /mnt/etc/hostname | |
| - Set root password: arch-chroot /mnt passwd | |
| - Create user + sudo | |
| - Install and enable networking (e.g. NetworkManager) | |
| Reboot: | |
| umount -R /mnt | |
| swapoff -a | |
| reboot | |
| NOTES | |
| } | |
| main() { | |
| require_root | |
| require_luks_env | |
| require_cmds lsblk gdisk wipefs cryptsetup pvcreate vgcreate lvcreate mkfs.ext4 mkswap mount swapon pacstrap genfstab arch-chroot grub-install grub-mkconfig mkinitcpio blkid sed dd chattr partprobe | |
| local mode="bios" | |
| if is_uefi; then mode="uefi"; fi | |
| log "Detected boot mode: $mode" | |
| pick_disk_menu | |
| collect_i18n_inputs | |
| collect_microcode_choice | |
| wipe_disk "$DISK" | |
| partition_disk "$DISK" "$mode" | |
| local esp_part="" boot_part="" lvm_part="" | |
| if [[ "$mode" == "uefi" ]]; then | |
| esp_part="$(part_path "$DISK" 1)" | |
| boot_part="$(part_path "$DISK" 2)" | |
| lvm_part="$(part_path "$DISK" 3)" | |
| else | |
| boot_part="$(part_path "$DISK" 2)" | |
| lvm_part="$(part_path "$DISK" 3)" | |
| fi | |
| log "Using partitions:" | |
| if [[ "$mode" == "uefi" ]]; then | |
| echo " ESP : $esp_part" | |
| else | |
| echo " BIOS boot: $(part_path "$DISK" 1)" | |
| fi | |
| echo " BOOT: $boot_part" | |
| echo " LVM : $lvm_part" | |
| if [[ "$mode" == "uefi" ]]; then | |
| format_esp_if_needed "$mode" "$esp_part" | |
| fi | |
| setup_luks_and_lvm "$boot_part" "$lvm_part" | |
| if [[ "$mode" == "uefi" ]]; then | |
| mount_all "$mode" "$esp_part" | |
| else | |
| mount_all "$mode" | |
| fi | |
| install_base "$mode" | |
| configure_system_in_chroot "$mode" "$DISK" "$boot_part" "$lvm_part" "$TIMEZONE" "$LOCALE" "$KEYMAP" | |
| log "Install completed." | |
| post_install_notes | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment