diff --git a/README.md b/README.md index 6e33c66..572502d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,256 @@ # arch-zbm -Helper script to install Arch Linux with ZFSBootMenu \ No newline at end of file +Helper script to install Arch Linux with ZFSBootMenu from within a running Arch Linux live CD ISO image + +## Prep + +We expect minimal prep on your end. Please make sure that before execution the following conditions are met. + +- Arch Linux live CD ISO image sees exactly one partition with partition type code `BF00` ("Solaris root") +- Arch Linux live CD ISO image sees exactly one partition with partition type code `EF00` ("EFI system partition") +- No ZFS zpool exists + +The script will create a single ZFS zpool `zpool` on the `BF00` partition with dataset child `zpool/root` which itself has one child `zpool/root/archlinux`, that's where Arch Linux gets installed. Parallel to `zpool/root` it'll create `zpool/data` with a `zpool/data/home` child dataset that gets mounted at `/home`. + +The script will use the `EF00` partition to install a ZFSBootMenu EFI executable if `efibootmgr` says that no such `ZFSBootMenu` entry exists. If ZFSBootMenu gets added to the EFI partition it'll become primary boot option. + +## How to run this? + +- Boot an Arch Linux live CD ISO image +- Run: + ``` + export SCRIPT_URL='https://quico.space/quico-os-setup/arch-zbm/raw/branch/main/setup.sh' + curl -s "${SCRIPT_URL}" | bash + ``` + During execution the script will call itself when it changes into its `chroot`, that's why we `export SCRIPT_URL`. Feel free to update `"${SCRIPT_URL}"` with whatever branch or revision you want to use from [quico.space/quico-os-setup/arch-zbm](https://quico.space/quico-os-setup/arch-zbm). Typically `.../branch/main/setup.sh` as shown above is what you want. + +## Steps + +The scripts takes the following installation steps. + +1. Install ZFS tools and kernel module with [github.com/eoli3n/archiso-zfs](https://github.com/eoli3n/archiso-zfs) +1. Create one encrypted ZFS zpool on top of `BF00` partition, password `password` +1. Create dataset for Arch Linux and `/home` +1. Install Arch Linux into pool +1. Add ZFSBootMenu to `EF00` partition if it doesn't exist already +1. Exit into Arch Linux live CD ISO image shell for you to `reboot` and frolick + +## Flavor choices + +We make the following opinionated flavor choices. Feel free to change them to your liking. + +- Arch Linux locale is set to `en_US.UTF-8` +- Keymap is `de-latin1` + - Consult `/etc/vconsole.conf` + - Change `zfs set org.zfsbootmenu:commandline=...` +- No X.Org Server, Wayland compositors or other GUI elements get installed +- Timezone is `Etc/UTC` + - Check `timedatectl set-timezone ` + +## Post-run manual steps + +After installation you're going to want to at least touch these points in your new Arch Linux install: + +- Package manager hook: `pacman` does not have a hook to do ZFS snapshots + - See [this GitHub gist](https://gist.github.com/Soulsuke/6a7d1f09f7fef968a2f32e0ff32a5c4c#file-arch_on_zfs-txt-L238) and [zfs-snapshotter.bash](https://github.com/Soulsuke/arch-zfs-tools/blob/master/zfs-snapshotter.bash) for inspiration +- Hostname: Installation chose a pseudo-randomly generated 8-character string with `pwgen` + - Check `hostnamectl set-hostname ` +- Unprivileged user accounts: The OS was installed with `root` and unprivileged `build` users +- Passwords + - ZFS: The password for all datasets underneath `zpool` is `password`. + - Local `root` account: The local `root` account's password is `password`. +- Arch User Repository (AUR) helper: We installed [paru](https://github.com/Morganamilo/paru) as our AUR helper, we installed from GitHub via `makepkg -si`. + +# Password change + +After installation you're going to want to change your ZFS encryption password. + +## Steps + +In a running OS: + +1. Change password in `keylocation` file, e.g. `/etc/zfs/zpool.key` or whatever other `"${zpool_name}"'.key'` file you used during setup +1. Set this key as the new encryption key: + ``` + zfs change-key -l zpool + ``` + Quoting `man 8 zfs-change-key` from `zfs-utils` version 2.1.9 for the `-l` argument: "Ensures the key is loaded before attempting to change the key." When successful the command will not output data, it'll just silently change your encryption key. +1. Rebuild initramfs: + ``` + mkinitcpio -P + ``` + Here for example with `-P` (`--allpresets`) which processes all presets contained in `/etc/mkinitcpio.d`. This step puts the changed key file into your initramfs. During setup we've adjusted `/etc/mkinitcpio.conf` so that it contains `FILES=(/etc/zfs/zpool.key)` which causes the file to be added to initramfs as-is. + +## Boot flow + +With your password changed in two locations (key file and initramfs) The boot process works as follows. + +At boot time ZFSBootMenu will scan all pools that it can import for a `bootfs` property. If it only finds one pool with that property the dataset given as `bootfs` will be selected for boot with a 10-second countdown allowing manual interaction. With `bootfs` set ZFSBootMenu will not actively search through datasets for valid kernel and initramfs combinations, it'll instead accept `bootfs` as the default boot entry without us entering the pool decryption passphrase. + +Upon loading into a given dataset ZFSBootMenu will attempt to auto-load the matching decryption key. In our setup this will fail because we purposely stored the encryption key inside our `zpool/root/archlinux` dataset. ZFSBootMenu will prompt us to type in the decryption key. + +Lastly ZFSBootMenu loads our OS' kernel and initramfs combination via `kexec`. For this step we don't need to enter the decryption key again. Our initramfs file contains the plain-text `/etc/zfs/zpool.key` file which allows it to seamlessly import the right dataset, load its key and mount it. + +## Caveats in a password change + +ZFS differentiates between user keys - also called wrapping keys - and the master key for any given encryption root. You never interact with the master key, you only pick your personal user key. Subsequently a user key change (in our use case we perceive this simply as a password change) has zero effect on data that's already encrypted. The operation is instant and merely reencrypts the already existing master key, the so-called _wrapped_ master key. + +ZFS generates the master key exactly once when you enable encryption on a dataset - technically when it becomes an encryption root. Among other inputs it uses your user key to encrypt (to _wrap_) the master key. When you change your user key it just means that the master key stays exactly the same and only the encrypted (_wrapped_) key changes. + +`man 8 zfs-change-key` from `zfs-utils` version 2.1.9 adds: +> If the user's key is compromised, `zfs change-key` does not necessarily protect existing or newly-written data from attack. Newly-written data will continue to be encrypted with the same master key as the existing data. The master key is compromised if an attacker obtains a user key and the corresponding wrapped master key. Currently, `zfs change-key` does not overwrite the previous wrapped master key on disk, so it is accessible via forensic analysis for an indeterminate length of time. +> +> In the event of a master key compromise, ideally the drives should be securely erased to remove all the old data (which is readable using the compromised master key), a new pool created, and the data copied back. This can be approximated in place by creating new datasets, copying the data (e.g. using `zfs send | zfs recv`), and then clearing the free space with `zpool trim --secure` if supported by your hardware, otherwise `zpool initialize`. + +On one hand changing the ZFS encryption password is generally a good and useful thing to do. On the other hand changing your password does not currently overwrite previous wrapped master keys on disk. A sufficiently motivated party that gains access to a wrapped master key and the matching user key is able to decrypt the master key and use it to read all data encrypted with it. + +By extension this means after a password change your data remains at risk until you've copied it to a new dataset and erased previously used space thereby erasing any previous wrapped master keys. + +## Changing master key + +In order to generate a new master key after you've changed your user key as mentioned in `man 8 zfs-change-key` from `zfs-utils` version 2.1.9 one example workflow goes like this: + +1. Change user key + - Update `/etc/zfs/zpool.key` + - Update zpool with new key via `zfs change-key -l zpool` + - Generate new initramfs with `mkinitcpio -P` +1. Create a snapshot from current system dataset + ``` + # Assuming current system dataset is zpool/root/archlinux-sxu + # where '-sxu' is a random suffix to differentiate datasets + # and has no real meaning + zfs snapshot zpool/root/archlinux-sxu@rekey + ``` +1. Within same pool `send`/`receive` snapshot + ``` + zfs send \ + --large-block \ + --compressed \ + 'zpool/root/archlinux-sxu@rekey' | \ + + zfs receive \ + -Fvu \ + -o 'encryption=on' \ + -o 'keyformat=passphrase' \ + -o 'keylocation=file:///etc/zfs/zpool.key' \ + -o 'mountpoint=/' \ + -o 'canmount=noauto' \ + -o 'org.zfsbootmenu:commandline=rw nowatchdog rd.vconsole.keymap=de-latin1' \ + 'zpool/root/archlinux-frn' + ``` + Explanation: + - We specifically don't `zfs send -R` (`--replicate`). While it would normally be nice to transfer all of a dataset's children at once such as all of its snapshots the `-R` argument conflicts with the `encryption` property. See [comment by Tom Caputi on GitHub openzfs/zfs issue 10507 from June 2020](https://github.com/openzfs/zfs/issues/10507#issuecomment-651162104) for details. Basically if `encryption` is set then `-R` doesn't work. We could transfer existing encryption properties with `-w`/`--raw` but we don't actually want to transfer encryption properties at all. We want them to change during transfer, see the bullet point four points down from here talking about `encryption`. + - We `zfs receive -F` destroying any target snapshots and file systems beyond the snapshot we're transferring. In this example the target `zpool/root/archlinux-frn` doesn't even exist so `-F` isn't necessary to clean anything up. It's just good practice. + - With `-v` we get verbose progress output + - Argument `-u` makes sure the dataset does not get mounted after transfer. ZFS would mount it into `/` which wouldn't be helpful since we're currently using that filesystem ourselves. + - We set encryption properties `keyformat`, `keylocation` and most importantly `encryption`. The latter will turn our transferred dataset into its own `encryptionroot` which in turn generates a new master key. The auto-generated new master key gets wrapped with our updated passphrase in `keylocation`. This basically reencrypts all data in this dataset during transfer. + - We set `mountpoint` and `canmount` as well as a `org.zfsbootmenu:commandline` as we would for any new system dataset. +1. Change zpool's `bootfs` property to new system dataset + ``` + zpool set bootfs=zpool/root/archlinux-frn zpool + ``` +1. Boot into new system dataset +1. After reboot and now that you're in the new system dataset change its `encryptionroot` by letting it inherit data from its parent: + ``` + zfs change-key -i -l zpool/root/archlinux-frn + ``` + The parent `zpool/root` is inheriting this property from `zpool` which will make sure that `zpool/root/archlinux-frn` essentially gets its key now from `zpool`. Both `zpool/root/archlinux-frn` and `zpool` use the same exact `keylocation` with identical content. This operation is instant. + +## Finishing touches + +Just to confirm that the master key has changed run this command. It takes a moment to output data: + +``` +zfs send --raw zpool/root/archlinux-frn@rekey | zstream dump | sed -n -e '/crypt_keydata/,/end crypt/p; /END/q' +``` + +Repeat for source dataset `zpool/root/archlinux-sxu@rekey`. You're particularly interested in parameters `DSL_CRYPTO_MASTER_KEY_1` and the initialization vector `DSL_CRYPTO_IV`. Notice that they differ between old and new dataset confirming that your new dataset has a new master key. + +Optionally you may want to clean up: + +1. In newly keyed/reencrypted system dataset destroy its snapshot + ``` + zfs destroy zpool/root/archlinux-frn@rekey + ``` +1. Recursively destroy source dataset + ``` + zfs destroy -r zpool/root/archlinux-sxu + ``` + +# ZFS setup explained + +The ZFS pool and dataset setup that makes this tick, explained in plain English. + +1. Create zpool with options: + 1. `-R /mnt` (aka `-o cachefile=none -o altroot=/mnt`). The pool is never cached, i.e. it's considered temporary. All pool and dataset mount paths have `/mnt` prepended. From `man zpoolprops`: + > This can be used when examining an unknown pool where the mount points cannot be trusted, or in an alternate boot environment, where the typical paths are not valid. `altroot` is not a persistent property. It is valid only while the system is up. + 1. `-O canmount=off`: Note the capital `-O` which makes this a file system property, not a pool property. File system cannot be mounted, and is ignored by `zfs mount -a`. This property is not inherited. + 1. `-O mountpoint=none`: What it says on the tin, the pool has no mountpoint configured. + 1. `-O encryption=on`: Makes this our `encryptionroot` and passes the `encryption` setting to all child datasets. Selecting `encryption=on` when creating a dataset indicates that the default encryption suite will be selected, which is currently `aes-256-gcm`. + 1. `-O keylocation=file://...`: This property is only set for encrypted datasets which are encryption roots. Controls where the user's encryption key will be loaded from by default for commands such as `zfs load-key`. + 1. `-O keyformat=passphrase`: Controls what format the user's encryption key will be provided as. Passphrases must be between 8 and 512 bytes long. +1. At this time the newly created zpool is not mounted anywhere. Next we create the "root" dataset, that's an arbitary term for the parent dataset of all boot environments. Boot environments in your case may be for example different operating systems all of which live on separate datasets underneath the root. + 1. `-o mountpoint=none`: Same as above, the root dataset has - just like the pool - no mountpoint configured. + 1. `zfs set org.zfsbootmenu:commandline=...`: Set a common kernel command line for all boot environment such as `"ro quiet"`. +1. Neither the root dataset nor the pool are mounted at this time. We now create one boot environment dataset where we want to install Arch Linux. + 1. `-o mountpoint=/`: Our Arch Linux dataset will be mounted at `/`. + 1. `-o canmount=noauto`: When set to `noauto`, a dataset can only be mounted and unmounted explicitly. The dataset is not mounted automatically when the dataset is created or imported, nor is it mounted by the `zfs mount -a` command or unmounted by the `zfs unmount -a` command. + 1. We then `zpool set bootfs="zpool/root/archlinux" zpool`: ZFSBootMenu uses the `bootfs` property to identify suitable boot environments. If only one pool has it - as is the case here - it identifies the pool's preferred boot dataset that will be booted with a 10-second countdown allowing manual interaction in ZFSBootMenu. + 1. We explicitly mount the boot environment. Since the entire pool is still subject to our initial `-R /mnt` during creation a `zfs mount zpool/root/archlinux` will mount the Arch Linux dataset not into `/` but instead into `/mnt`. +1. We also create a `data` dataset that - at least for now - we use to store only our `/home` data. + 1. For `zpool/data`: + 1. `-o mountpoint=/`: We use the `mountpoint` property here only for inheritance. + 1. `-o canmount=off`: The `zpool/data` dataset itself cannot actually be mounted. + 1. For a `zpool/data/home` child dataset: + 1. We do not specify any properties. Since `canmount` cannot be inherited the parent's `canmount=off` does not apply, it instead defaults to `canmount=on`. The parent's `mountpoint=/` property on the other hand is inherited so for a `home` child dataset it conveniently equals `mountpoint=/home`. + 1. In effect this `zpool/data/home` dataset is subject to `zfs mount -a` and will happily automount into `/home`. +1. We export the zpool once, we then reimport it by scanning only inside `/dev/disk/by-id`, again setting `-R /mnt` as we did during pool creation a moment ago and we do not mount any file systems. +1. We `zfs load-key ` which will load the key from `keylocation` after which the `keystatus` property for `` and all child datasets will change from `unavailable` to `available`. +1. We mount our Arch Linux boot environment dataset. It automatically get prepended with `-R /mnt` since that's how we imported the pool. +1. We `zfs mount -a` which automounts `zpool/data/home` into `/home`, which again gets auto-prepended by `/mnt`. +1. We lastly mount our EFI partition into `/mnt/efi`. +1. We instruct ZFS to save its pool configuration via `zpool set cachefile=/etc/zfs/zpool.cache zpool`. + +The complete ZFS structure now exists and is mounted at `/mnt` ready for any `pacstrap`, [debootstrap](https://wiki.debian.org/Debootstrap), `dnf --installroot` or other bootstrapping action. + +# Development + +## Conventional commits + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for its commit messages. + +### Commit types + +Commit _types_ besides `fix` and `feat` are: + +- `build`: Project structure, directory layout, build instructions for roll-out +- `refactor`: Keeping functionality while streamlining or otherwise improving function flow +- `test`: Working on test coverage +- `docs`: Documentation for project or components + +### Commit scopes + +The following _scopes_ are known for this project. A Conventional Commits commit message may optionally use one of the following scopes or none: + +- `iso`: Changing Arch Linux ISO CD +- `zbm`: Adjusting ZFSBootMenu's behavior +- `zfs`: A change to how ZFS interacts with the system, either a pool or a dataset +- `os`: Getting an perating system set up to correctly work in a ZFS boot environment +- `meta`: Affects the project's repo layout, readme content, file names etc. + +# Credits + +Most of what's here was shamelessly copied and slightly adapted for personal use from Jonathan Kirszling at GitHub. + +Thanks to: + +- Jonathan Kirszling: + - [github.com/eoli3n/arch-config/tree/master/scripts/zfs/install](https://github.com/eoli3n/arch-config/tree/master/scripts/zfs/install) + - [github.com/eoli3n/archiso-zfs](https://github.com/eoli3n/archiso-zfs) +- Maurizio Oliveri: + - [github.com/Soulsuke/arch-zfs-tools](https://github.com/Soulsuke/arch-zfs-tools) + - [gist.github.com/Soulsuke/6a7d1f09f7fef968a2f32e0ff32a5c4c](https://gist.github.com/Soulsuke/6a7d1f09f7fef968a2f32e0ff32a5c4c) +- Zach Dykstra, Andrew J. Hesford and all other [ZFSBootMenu contributors](https://github.com/zbm-dev/zfsbootmenu/graphs/contributors): + - Their [ZFSBootMenu testing helper scripts](https://github.com/zbm-dev/zfsbootmenu/tree/master/testing/helpers) ([chroot-arch.sh](https://github.com/zbm-dev/zfsbootmenu/blob/master/testing/helpers/chroot-arch.sh), [install-arch.sh](https://github.com/zbm-dev/zfsbootmenu/blob/master/testing/helpers/install-arch.sh)) +- [github.com/kongkrit](https://github.com/kongkrit): + - [gist.github.com/kongkrit/a0585e179e33c2adf92db4050ec5171d](https://gist.github.com/kongkrit/a0585e179e33c2adf92db4050ec5171d) diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..e1e7177 --- /dev/null +++ b/setup.sh @@ -0,0 +1,646 @@ +#!/bin/bash + +declare this_script_url +this_script_url="${SCRIPT_URL:?}" + +declare zpool_name zfs_arch_dataset_name +zpool_name='zpool' +zfs_arch_dataset_name='archlinux' + +# https://unix.stackexchange.com/a/48550 +set -E +trap '[ "$?" -ne 77 ] || exit 77' ERR + +declare zpool_drive efi_drive + +function set_ntp () { + timedatectl set-ntp true +} + +function we_are_changerooted () { + if [ "$(stat -c '%d:%i' '/')" != "$(stat -c '%d:%i' '/proc/1/root/.')" ]; then + return 0 + else + return 1 + fi +} + +function update_pacman_db () { + printf -- '%s\n' 'Refreshing mirror list ...' + systemctl start reflector + # In an ISO and for the minimal number of packages we need we do not + # care about partial upgrades + pacman -Sy +} + +function install_pkgs () { + printf -- '%s\n' 'Installing packages ...' + pacman -S --needed --noconfirm "${@}" +} + +function install_zfs () { + declare reset_colors='\033[0m' + curl -s 'https://raw.githubusercontent.com/eoli3n/archiso-zfs/master/init' | bash + printf -- "${reset_colors}" +} + +function get_partitions () { + declare partitions_json + partitions_json="$(lsblk --output PATH,PARTTYPE --json --tree)" || return 1 + printf -- '%s' "${partitions_json}" + return 0 +} + +function get_part_parent () { + local child_partition parent_partition + child_partition="${1:?}" + parent_partition="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.children[].path=="'"${child_partition:?}"'") | .path')" + printf -- '%s' "${parent_partition}" + return 0 +} + +function get_parts () { + local zfs_install_drive + declare parttype parts + parttype="${1:?}" + zfs_install_drive="${2:-}" + case "${parttype}" in + zfs) + parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | .children[] | select(.parttype=="6a85cf4d-1dd2-11b2-99a6-080020736631") | .path')" || exit 1 + ;; + efi) + parts="$(get_partitions | jq --raw-output '.[][] | select(.children | length > 0) | select(.path=="'"${zfs_install_drive:?}"'") | .children[] | select(.parttype=="c12a7328-f81f-11d2-ba4b-00a0c93ec93b") | .path')" || exit 1 + ;; + *) + >2 printf -- '%s\n' 'Unknown partition type '"'"'"${parttype}"'"'"', exiting ...' + exit 77 + ;; + esac + printf -- '%s' "${parts}" + return 0 +} + +function we_have_exactly_one_part () { + local parttype parts_list parts_count + parttype="${1:?}" + parts_list="${2:?}" + parts_count="$(wc -l <<<"${parts_list}")" + if [[ "$(wc -c <<<"${parts_list}")" -gt '1' ]]; then + case "${parts_count}" in + 0) + >2 printf -- '%s\n' 'No '"${parttype^^}"' partition found. Exiting ...' + exit 77 + ;; + 1) + return 0 + ;; + *) + return 1 + ;; + esac + >2 printf -- '%s\n' 'Partition list does not look valid. Cowardly exiting ...' + exit 77 + fi +} + +function get_drive_id () { + local drive_id_list drive_id_single + drive_id_list="$(find -L /dev/disk/by-id -samefile "${1:?}" | sort)" + drive_id_single="$(head -n1 <<<"${drive_id_list}")" + if [[ "$(wc -l <<<"${drive_id_single}")" -eq '1' ]] && [[ "$(wc -c <<<"${drive_id_single}")" -gt '1' ]]; then + printf -- '%s' "${drive_id_single}" + return 0 + fi + >2 printf -- '%s\n' 'Not exactly one '"'${1:?}'"' partition entry in /dev/disk/by-id, exiting ...' + exit 77 +} + +function select_part () { + local parts enriched_parts enriched_parts_count part_number part_type zfs_install_drive + declare part + part_type="${1:?}" # 'efi' or 'zfs' + zfs_install_drive="${2:-}" + if [[ "${zfs_install_drive}" ]]; then + # This is intended to find correct EFI partition + parts="$(get_parts "${part_type}" "${zfs_install_drive}")" + else + parts="$(get_parts "${part_type}")" + fi + + if we_have_exactly_one_part "${part_type}" "${parts}"; then + part="${parts}" + else + 2> printf -- '%s\n' 'More than one '"${part_type^^}"' partition to pick for installation. Cowardly exiting ...' + exit 77 + fi + printf -- '%s' "${part}" + return 0 +} + +function no_zpool_exists () { + declare zpool_list + zpool_list="$(zpool list -H)" + [[ "$(wc -l <<<"${zpool_list}")" -le '1' ]] && [[ "$(wc -c <<<"${zpool_list}")" -le '1' ]] && return 0 + return 1 +} + +function set_zpool_password () { + # May or may not have a newline at the end, ZFS doesn't care + printf -- '%s' 'password' > '/etc/zfs/'"${zpool_name}"'.key' + chmod '000' '/etc/zfs/'"${zpool_name}"'.key' +} + +function import_pool () { + zpool import -d '/dev/disk/by-id' -R '/mnt' "${zpool_name}" -N -f + zfs load-key "${zpool_name}" +} + +function create_pool () { + # Create a temporary pool that is not cached + zpool create -f \ + -o 'ashift=12' \ + -o 'autotrim=on' \ + -O 'acltype=posix' \ + -O 'compression=on' \ + -O 'relatime=on' \ + -O 'xattr=sa' \ + -O 'encryption=on' \ + -O 'keyformat=passphrase' \ + -O 'keylocation=file:///etc/zfs/'"${zpool_name}"'.key' \ + -O 'normalization=formD' \ + -O 'mountpoint=none' \ + -O 'canmount=off' \ + -O 'devices=off' \ + -R '/mnt' \ + "${zpool_name}" "${1:?}" +} + +function create_root_dataset () { + zfs create -o mountpoint=none "${zpool_name}"'/root' + # zfs set org.zfsbootmenu:commandline="ro quiet" "${zpool_name}"'/root' + zfs set org.zfsbootmenu:commandline="ro" "${zpool_name}"'/root' +} + +function create_system_dataset () { + zfs create -o 'mountpoint=/' -o 'canmount=noauto' "${zpool_name}"'/root/'"${zfs_arch_dataset_name}" + zgenhostid + zpool set bootfs="${zpool_name}"'/root/'"${zfs_arch_dataset_name}" "${zpool_name}" + zfs mount "${zpool_name}"'/root/'"${zfs_arch_dataset_name}" +} + +function create_home_dataset () { + zfs create -o 'mountpoint=/' -o 'canmount=off' "${zpool_name}"'/data' + zfs create "${zpool_name}"'/data/home' +} + +function export_pool () { + zpool export "${zpool_name}" +} + +function setup_zpool () { + local drive_by_id + zpool_drive="$(select_part 'zfs')" + drive_by_id="$(get_drive_id "${zpool_drive}")" + + set_zpool_password + if no_zpool_exists; then + create_pool "${drive_by_id}" + create_root_dataset + create_system_dataset + create_home_dataset + export_pool + import_pool + else + printf -- '%s\n' 'A zpool already exists, that is unexpected. Cowardly exiting 1 ...' + exit 1 + fi +} + +function mount_system () { + zfs mount "${zpool_name}"'/root/'"${zfs_arch_dataset_name}" + zfs mount -a + + local zfs_parent efi_part + zfs_parent="$(get_part_parent "${zpool_drive:?}")" + efi_drive="${zfs_parent}" + efi_part="$(select_part 'efi' "${zfs_parent:?}")" + mkdir -p '/mnt/efi' + mount "${efi_part}" '/mnt/efi' +} + +function copy_zpool_cache () { + mkdir -p '/mnt/etc/zfs' + zpool set 'cachefile=/etc/zfs/'"${zpool_name}"'.cache' "${zpool_name}" +} + +function pacman_dl_parallel () { + sed -ri -e 's'$'\x1''^.*?(ParallelDownloads)[^\r\n\f]*'$'\x1''\1 = 5'$'\x1''g' '/etc/pacman.conf' +} + +function install_archlinux () { + pacman_dl_parallel + pacstrap /mnt \ + base \ + base-devel \ + linux \ + linux-headers \ + linux-firmware \ + amd-ucode \ + efibootmgr \ + vim \ + git \ + iwd \ + networkmanager \ + network-manager-applet \ + dialog \ + os-prober \ + reflector \ + bluez \ + bluez-utils \ + man-db \ + xdg-utils \ + xdg-user-dirs +} + +function gen_fstab () { + genfstab -U /mnt | grep -v "${zpool_name}" | tr -s '\n' | sed -r -e 's/\/mnt//' -e '/./,$!d' > '/mnt/etc/fstab' +} + +function configure_hosts_file () { + cat > '/mnt/etc/hosts' < +127.0.0.1 localhost ${1} +::1 localhost ${1} +EOF +} + +function set_hostname () { + declare new_hostname + install_pkgs 'pwgen' + new_hostname="$(pwgen --no-numerals --no-capitalize --ambiguous 8)" + printf -- '%s\n' "${new_hostname}" > '/mnt/etc/hostname' + configure_hosts_file "${new_hostname}" +} + +function set_locale () { + printf -- '%s\n' \ + 'KEYMAP=de-latin1' \ + 'FONT=Lat2-Terminus16' \ + 'FONT_MAP=8859-1' \ + > '/mnt/etc/vconsole.conf' + sed -ri -e 's'$'\x1''^(#)(en_US.UTF-8[^\r\n\f]*)'$'\x1''\2'$'\x1''g' '/mnt/etc/locale.gen' + printf -- '%s\n' 'LANG=en_US.UTF-8' > '/mnt/etc/locale.conf' +} + +function add_zfs_hook_to_initramfs () { + # Add zfs hook, remove fsck hook from initramfs. Also add plain text key + # file into initramfs since it's living inside an encrypted pool anyway. + sed -ri \ + -e 's'$'\x1''^(FILES=)[^\r\n\f]*'$'\x1''\1(/etc/zfs/'"${zpool_name}"'.key)'$'\x1''g' \ + -e 's'$'\x1''(HOOKS=)(.*?[\(| ])(filesystems)([\)| ][^\r\n\f]*)'$'\x1''\1\2zfs \3\4'$'\x1''g' \ + -e 's'$'\x1''((\()(fsck)(\)))'$'\x1''\2\4'$'\x1''g' \ + -e 's'$'\x1''(([[:space:]]+)(fsck)|(fsck)([[:space:]]+))'$'\x1'''$'\x1''g' \ + '/mnt/etc/mkinitcpio.conf' +} + +function set_initramfs_build_list () { + # No need to build fallback initramfs, our new fallback is ZFS snapshots + sed -ri \ + -e '/^#/d' \ + -e '/^$/d' \ + -e '/^fallback/d' \ + -e 's'$'\x1''^(PRESETS=)[^\r\n\f]*'$'\x1''\1('"'"'default'"'"')'$'\x1''g' \ + '/mnt/etc/mkinitcpio.d/linux.preset' + + # Remove any existing fallback initramfs files + find '/mnt/boot' -type f -regextype posix-extended -iregex '^/mnt/boot/initramfs-.*?-fallback.img' -delete +} + +function add_zfs_files_to_new_os () { + for zfs_file in '/etc/hostid' '/etc/zfs/zpool.cache' '/etc/zfs/'"${zpool_name}"'.key'; do + rsync -av --itemize-changes {'','/mnt'}"${zfs_file}" + done +} + +function enter_chroot () { + arch-chroot /mnt /bin/bash -xe <&1)" + if [[ "${from_where}" == 'local' ]] && grep -Piq -- '^error: package .*? was not found' <<<"${pkg_info}"; then + return 1 + else + local_pkg_version="$(awk "${version_search}" <<<"${pkg_info}")" + fi + printf -- '%s' "${local_pkg_version}" + return 0 +} + +function paru_with_zfs_first () { + if [[ "${#}" -eq '0' ]]; then + declare -A local_pkg_info + /usr/bin/paru -Sy + if local_pkg_info['zfs-dkms']="$(get_pkg_info 'local' 'zfs-dkms')" && local_pkg_info['zfs-utils']="$(get_pkg_info 'local' 'zfs-utils')"; then + local_pkg_info['zfs-dkms']="$(get_pkg_info 'local' 'zfs-dkms')" + local_pkg_info['zfs-utils']="$(get_pkg_info 'local' 'zfs-utils')" + + declare -A remote_pkg_info + remote_pkg_info['zfs-dkms']="$(get_pkg_info 'remote' 'zfs-dkms')" + remote_pkg_info['zfs-utils']="$(get_pkg_info 'remote' 'zfs-utils')" + + /usr/bin/paru -S --needed archlinux-keyring + + if [[ "${local_pkg_info['zfs-dkms']}" == "${remote_pkg_info['zfs-dkms']}" ]] && \ + [[ "${local_pkg_info['zfs-utils']}" == "${remote_pkg_info['zfs-utils']}" ]]; then + /usr/bin/paru -Su + else + /usr/bin/paru -Sy 'zfs-dkms' 'zfs-utils' \ + --assume-installed zfs-dkms="${local_pkg_info['zfs-dkms']}" \ + --assume-installed zfs-dkms="${remote_pkg_info['zfs-dkms']}" \ + --assume-installed zfs-utils="${local_pkg_info['zfs-utils']}" \ + --assume-installed zfs-utils="${remote_pkg_info['zfs-utils']}" + /usr/bin/paru -Su + fi + else + /usr/bin/paru -S --needed archlinux-keyring + /usr/bin/paru -Su + fi + else + /usr/bin/paru "${@}" + fi +} + +function create_unpriv_user () { + account_name="${1:?}" + full_name="${2:-${account_name}}" + authorized_keys_abs_path='/home/'"${account_name}"'/.ssh/authorized_keys' + useradd --create-home --shell '/bin/bash' --comment 'Personal account of '"${full_name}" --user-group "${account_name}" + chfn --full-name "${full_name}" "${account_name}" + mkdir -p "$(dirname "${authorized_keys_abs_path}")" + touch "${authorized_keys_abs_path}" + chown -R "${account_name}"':' '/home/'"${account_name}"; chmod -R 'u=rwX,go=' "$(dirname "${authorized_keys_abs_path}")" +} + +function unleash_makepkg () { + local path_prefix + path_prefix='/mnt' + if we_are_changerooted; then + path_prefix='' + fi + sed -ri \ + -e 's'$'\x1''^(#?(MAKEFLAGS=))[^\r\n\f]*'$'\x1''\2"-j$(nproc --ignore 1)"'$'\x1''g' \ + "${path_prefix}"'/etc/makepkg.conf' +} + +function get_aur_helper () { + create_unpriv_user 'build' + usermod --append --groups 'wheel' 'build' + printf -- '%s\n' '%wheel ALL=(ALL:ALL) NOPASSWD: ALL' > '/etc/sudoers.d/10-wheel-group-no-passwd-prompt' + pushd /tmp + git clone 'https://aur.archlinux.org/paru.git' + chown -R 'build:' 'paru' + pushd 'paru' + sudo --user 'build' makepkg -si --noconfirm + popd + rm -rf 'paru' + popd + alias paru='paru_with_zfs_first' +} + +function paru_install () { + sudo --user build paru -S --noconfirm "${@}" +} + +function keep_initiramfs_root_only_rw () { + declare systemd_local_admin_override_path unit_name + systemd_local_admin_override_path='/etc/systemd/system' + unit_name='chmod-initramfs' + path_unit="${systemd_local_admin_override_path%/}"'/'"${unit_name}"'.path' + service_unit="${systemd_local_admin_override_path%/}"'/'"${unit_name}"'.service' + + cat > "${path_unit}" <<"EOF" +[Unit] +Description=chmod initramfs to be root-read-writable only + +[Path] +PathChanged=/boot/initramfs-linux.img + +[Install] +WantedBy=multi-user.target +WantedBy=system-update.target +EOF + + cat > "${service_unit}" <<"EOF" +[Unit] +Description=chmod initramfs to be root-read-writable only + +[Service] +Type=oneshot +ExecStart=/usr/bin/chmod 600 /boot/initramfs-linux.img +EOF + + systemctl enable "${path_unit}" +} + +function add_motd_getting_started_msg () { + cat > '/etc/motd' <<"EOF" + +#################### + +GUI basics: + + paru -S xorg plasma-meta kde-applications-meta sddm + localectl set-x11-keymap de + +#################### + +EOF +} + +function install_os_in_chroot () { + ### Reinit keyring + # As keyring is initialized at boot, and copied to the install dir with pacstrap, and ntp is running + # Time changed after keyring initialization, it leads to malfunction + # Keyring needs to be reinitialised properly to be able to sign keys. + rm -rf '/etc/pacman.d/gnupg' + pacman-key --init + pacman-key --populate archlinux + pacman -S archlinux-keyring --noconfirm + + keep_initiramfs_root_only_rw + pacman_dl_parallel + unleash_makepkg + add_motd_getting_started_msg + get_aur_helper + paru_install 'zfs-dkms' 'zfs-utils' + hwclock --systohc + locale-gen + source /etc/locale.conf + mkinitcpio -P + + # Install ZFSBootMenu and deps + git clone --depth=1 https://github.com/zbm-dev/zfsbootmenu/ '/tmp/zfsbootmenu' + paru_install 'cpanminus' 'kexec-tools' 'fzf' 'util-linux' + pushd '/tmp/zfsbootmenu' + make + make install + cpanm --notest --installdeps . + popd + rm -rf '/tmp/zfsbootmenu' +} + +function set_root_pw () { + printf -- '%s\n' 'root:password' | chpasswd --root '/mnt' +} + +function configure_networking () { + cat > '/mnt/etc/systemd/network/50-wired.network' <<"EOF" +[Match] +Name=en* + +[Network] +DHCP=ipv4 +IPForward=yes + +[DHCP] +UseDNS=yes +RouteMetric=10 +EOF + systemctl enable 'systemd-networkd' --root='/mnt' + systemctl disable 'systemd-networkd-wait-online' --root='/mnt' +} + +function configure_dns () { + rm '/mnt/etc/resolv.conf' + ln -s '/run/systemd/resolve/stub-resolv.conf' '/mnt/etc/resolv.conf' + + # Optionally you may want /etc/systemd/network/50-wired.network to use + # UseDNS=no and hardcode DNS server(s) here: + # sed -i 's/^#DNS=.*/DNS=1.1.1.1/' /mnt/etc/systemd/resolved.conf + systemctl enable 'systemd-resolved' --root='/mnt' +} + +function configure_reflector () { + systemctl enable 'reflector' --root='/mnt' +} + +function configure_zfs () { + systemctl enable 'zfs-import-cache' 'zfs-mount' 'zfs-import.target' 'zfs.target' --root='/mnt' +} + +function configure_zfs_mount_gen () { + mkdir -p '/mnt/etc/zfs/zfs-list.cache' + touch '/mnt/etc/zfs/zfs-list.cache/'"${zpool_name}" + zfs list -H -o name,mountpoint,canmount,atime,relatime,devices,exec,readonly,setuid,nbmand | sed 's/\/mnt//' > '/mnt/etc/zfs/zfs-list.cache/'"${zpool_name}" + systemctl enable 'zfs-zed.service' --root='/mnt' +} + +function configure_zfsbootmenu () { + curl -s 'https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master/etc/zfsbootmenu/mkinitcpio.conf' | sed -r -e '/^#/d' -e '/^$/d' > '/mnt/etc/zfsbootmenu/mkinitcpio.conf' + cat > '/mnt/etc/zfsbootmenu/config.yaml' <