modularize setup scripts, make hibernation safe with ZFS, apply noexec FS option everywhere

This commit is contained in:
Niklas Gollenstede
2022-07-29 12:49:55 +02:00
parent 8b0e200c73
commit 7ab9215b0a
28 changed files with 521 additions and 266 deletions

View File

@ -3,16 +3,15 @@
This is a library of bash functions, mostly for NixOS system installation.
The (paths to these) scripts are meant to be passed in the `scripts` argument to [`mkSystemsFlake`](../flakes.nix#mkSystemsFlake) (see [`flake.nix`](../../flake.nix) for an example), which makes their functions available in the per-host [`devShells`/`apps`](../flakes.nix#mkSystemsFlake).
The (paths to these) scripts are meant to be (and by default are) set as `config.wip.setup.scripts.*` (see [`../flakes.nix`](../flakes.nix)), which makes their functions available in the per-host [`devShells`/`apps`](../flakes.nix#mkSystemsFlake).
Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context.
Any script passed later in `scripts` can overwrite the functions of these (earlier) default scripts.
With the functions from here, [a simple four-liner](../install.sh) is enough to do a completely automated NixOS installation:
With the functions from here, [a simple three-liner](../install.sh) is enough to do a completely automated NixOS installation:
```bash
function install-system {( set -eu # 1: diskPaths
prepare-installer "$@"
do-disk-setup "${argv[0]}"
init-or-restore-system
install-system-to $mnt
)}
```

View File

@ -2,109 +2,114 @@
##
# Key Generation Methods
# See »../../modules/fs/keystore.nix.md« for more documentation.
# It is probably generally advisable that these functions output ASCII strings.
# Keys used as ZFS encryption keys (with the implicit »keyformat = hex«) must be 64 (lowercase?) hex digits.
##
## Puts an empty key in the keystore, causing that ZFS dataset to be unencrypted, even if it's parent is encrypted.
function add-key-unencrypted {( set -eu # 1: usage
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
: | write-secret "$keystore"/"$usage".key
## Outputs nothing (/ an empty key), causing that ZFS dataset to be unencrypted, even if it's parent is encrypted.
function gen-key-unencrypted {( set -eu # 1: usage
:
)}
## Adds a key by copying the hostname to the keystore.
function add-key-hostname {( set -eu # 1: usage
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
if [[ ! "$usage" =~ ^(luks/keystore-@{config.networking.hostName!hashString.sha256:0:8}/.*)$ ]] ; then printf '»trivial« key mode is only available for the keystore itself.\n' ; exit 1 ; fi
printf %s "@{config.networking.hostName}" | write-secret "$keystore"/"$usage".key
## Uses the hostname as a trivial key.
function gen-key-hostname {( set -eu # 1: usage
usage=$1
if [[ ! "$usage" =~ ^(luks/keystore-@{config.networking.hostName!hashString.sha256:0:8}/.*)$ ]] ; then printf '»trivial« key mode is only available for the keystore itself.\n' 1>&2 ; exit 1 ; fi
printf %s "@{config.networking.hostName}"
)}
## Adds a key by copying it from a bootkey partition (see »add-bootkey-to-keydev«) to the keystore.
function add-key-usb-part {( set -eu # 1: usage
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
if [[ ! "$usage" =~ ^(luks/keystore-[^/]+/[1-8])$ ]] ; then printf '»usb-part« key mode is only available for the keystore itself.\n' ; exit 1 ; fi
## Obtains a key by reading it from a bootkey partition (see »add-bootkey-to-keydev«).
function gen-key-usb-part {( set -eu # 1: usage
usage=$1
if [[ ! "$usage" =~ ^(luks/keystore-[^/]+/[1-8])$ ]] ; then printf '»usb-part« key mode is only available for the keystore itself.\n' 1>&2 ; exit 1 ; fi
bootkeyPartlabel=bootkey-"@{config.networking.hostName!hashString.sha256:0:8}"
cat /dev/disk/by-partlabel/"$bootkeyPartlabel" | write-secret "$keystore"/"$usage".key
cat /dev/disk/by-partlabel/"$bootkeyPartlabel"
)}
## Adds a key by copying a different key from the keystore to the keystore.
function add-key-copy {( set -eu # 1: usage, 2: source
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; source=$2
cat "$keystore"/"$source".key | write-secret "$keystore"/"$usage".key
## Outputs a key by simply printing an different keystore entry (that must have been generated before).
function gen-key-copy {( set -eu # 1: _, 2: source
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; source=$2
cat "$keystore"/"$source".key
)}
## Adds a key by writing a constant value to the keystore.
function add-key-constant {( set -eu # 1: usage, 2: value
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; value=$2
printf %s "$value" | write-secret "$keystore"/"$usage".key
## Outputs a key by simply using the constant »$value« passed in.
function gen-key-constant {( set -eu # 1: _, 2: value
value=$2
printf %s "$value"
)}
## Adds a key by prompting for a password and saving it to the keystore.
function add-key-password {( set -eu # 1: usage
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
(prompt-new-password "as key for @{config.networking.hostName}/$usage" || exit 1) \
| write-secret "$keystore"/"$usage".key
## Obtains a key by prompting for a password.
function gen-key-password {( set -eu # 1: usage
usage=$1
( prompt-new-password "as key for @{config.networking.hostName}/$usage" || exit 1 )
)}
## Generates a key by prompting for a password, combining it with »$keystore/home/$user.key«, and saving it to the keystore.
function add-key-home-pw {( set -eu # 1: usage, 2: user
## Generates a key by prompting for (or reusing) a »$user«'s password, combining it with »$keystore/home/$user.key«.
function gen-key-home-composite {( set -eu # 1: usage, 2: user
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; user=$2
if [[ ${!userPasswords[@]} && ${userPasswords[$user]:-} ]] ; then
if [[ ${!userPasswords[@]} && ${userPasswords[$user]:-} ]] ; then
password=${userPasswords[$user]}
else
password=$(prompt-new-password "that will be used as component of the key for @{config.networking.hostName}/$usage")
fi
( cat "$keystore"/home/"$user".key && cat <<<"$password" ) | sha256sum | head -c 64 \
| write-secret "$keystore"/"$usage".key
( cat "$keystore"/home/"$user".key && cat <<<"$password" ) | sha256sum | head -c 64
)}
## Generates a reproducible secret for a certain »$use«case by prompting for a pin/password and then challenging slot »$slot« of YubiKey »$serial«, and saves it to the »$keystore«.
function add-key-yubikey-pin {( set -eu # 1: usage, 2: serialAndSlot(as »serial:slot«)
## Generates a reproducible, host-independent key by challenging slot »$slot« of YubiKey »$serial« with »$user«'s password.
function gen-key-home-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndUser(as »serial:slot:user«)
usage=$1 ; args=$2
serial=$( <<<"$args" cut -d: -f1 ) ; slot=$( <<<"$args" cut -d: -f2 )
user=${args/$serial:$slot:/}
if [[ ${!userPasswords[@]} && ${userPasswords[$user]:-} ]] ; then
password=${userPasswords[$user]}
else
password=$(prompt-new-password "as YubiKey challenge for @{config.networking.hostName}/$usage")
fi
gen-key-yubikey-challenge "$usage" "$serial:$slot:home-$password" true "${user}'s password for ${usage}"
)}
## Generates a reproducible secret by prompting for a pin/password and then challenging slot »$slot« of YubiKey »$serial«.
function gen-key-yubikey-pin {( set -eu # 1: usage, 2: serialAndSlot(as »serial:slot«)
usage=$1 ; serialAndSlot=$2
password=$(prompt-new-password "/ pin as challenge to YubiKey »$serialAndSlot« as key for @{config.networking.hostName}/$usage")
add-key-yubikey-challenge "$usage" "$serialAndSlot:$password" true "pin for ${usage}"
gen-key-yubikey-challenge "$usage" "$serialAndSlot:$password" true "pin for ${usage}"
)}
## Generates a reproducible secret for a certain »$use«case on a »$host« by challenging slot »$slot« of YubiKey »$serial«, and saves it to the »$keystore«.
function add-key-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndSalt(as »serial:slot:salt«)
usage=$1 ; IFS=':' read -ra serialAndSlotAndSalt <<< "$2"
usage_="$usage" ; if [[ "$usage" =~ ^(luks/.*/[0-8])$ ]] ; then usage_="${usage:0:(-2)}" ; fi # produce the same secret, regardless of the target luks slot
challenge="@{config.networking.hostName}:$usage_${serialAndSlotAndSalt[2]:+:${serialAndSlotAndSalt[2]:-}}"
add-key-yubikey-challenge "$usage" "${serialAndSlotAndSalt[0]}:${serialAndSlotAndSalt[1]}:$challenge"
## Generates a reproducible secret for a certain »$use«case and optionally »$salt« on a »$host« by challenging slot »$slot« of YubiKey »$serial«.
function gen-key-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndSalt(as »serial:slot:salt«)
usage=$1 ; args=$2
serial=$( <<<"$args" cut -d: -f1 ) ; slot=$( <<<"$args" cut -d: -f2 )
salt=${args/$serial:$slot:/}
usagE="$usage" ; if [[ "$usage" =~ ^(luks/.*/[0-8])$ ]] ; then usagE="${usage:0:(-2)}" ; fi # produce the same secret, regardless of the target luks slot
challenge="@{config.networking.hostName}:$usagE${salt:+:$salt}"
gen-key-yubikey-challenge "$usage" "$serial:$slot:$challenge"
)}
## Generates a reproducible secret for a certain »$use«case by challenging slot »$slot« of YubiKey »$serial« with the fixed »$challenge«, and saves it to the »$keystore«.
# If »$sshArgs« is set as (env) var, generate the secret locally, then use »ssh $sshArgs« to write the secret on the other end.
# E.g.: # sshArgs='installerIP' add-key-yubikey /run/keystore/ zfs/rpool/remote 1234567:2:customChallenge
function add-key-yubikey-challenge {( set -eu # 1: usage, 2: serialAndSlotAndChallenge(as »$serial:$slot:$challenge«), 3?: onlyOnce, 4?: message
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; args=$2 ; message=${4:-}
serial=$(<<<"$args" cut -d: -f1)
slot=$(<<<"$args" cut -d: -f2)
## Generates a reproducible secret by challenging slot »$slot« of YubiKey »$serial« with the fixed »$challenge«.
function gen-key-yubikey-challenge {( set -eu # 1: _, 2: serialAndSlotAndChallenge(as »$serial:$slot:$challenge«), 3?: onlyOnce, 4?: message
args=$2 ; message=${4:-}
serial=$( <<<"$args" cut -d: -f1 ) ; slot=$( <<<"$args" cut -d: -f2 )
challenge=${args/$serial:$slot:/}
if [[ "$serial" != "$(@{native.yubikey-personalization}/bin/ykinfo -sq)" ]] ; then printf 'Please insert / change to YubiKey with serial %s!\n' "$serial" ; fi
if [[ "$serial" != "$(@{native.yubikey-personalization}/bin/ykinfo -sq)" ]] ; then printf 'Please insert / change to YubiKey with serial %s!\n' "$serial" 1>&2 ; fi
if [[ ! "${3:-}" ]] ; then
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' twice with '"${message:-challenge »"$challenge":1/2«}"'. Enter to continue, or Ctrl+C to abort:'
else
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' once with '"${message:-challenge »"$challenge"«}"'. Enter to continue, or Ctrl+C to abort:'
fi
if [[ "$serial" != "$(@{native.yubikey-personalization}/bin/ykinfo -sq)" ]] ; then printf 'YubiKey with serial %s not present, aborting.\n' "$serial" ; exit 1 ; fi
if [[ "$serial" != "$(@{native.yubikey-personalization}/bin/ykinfo -sq)" ]] ; then printf 'YubiKey with serial %s not present, aborting.\n' "$serial" 1>&2 ; exit 1 ; fi
if [[ ! "${3:-}" ]] ; then
secret="$(@{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge":1)""$(@{native.yubikey-personalization}/bin/ykchalresp -2 "$challenge":2)"
if [[ ${#secret} != 80 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" ; exit 1 ; fi
if [[ ${#secret} != 80 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" 1>&2 ; exit 1 ; fi
else
secret="$(@{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge")"
if [[ ${#secret} != 40 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" ; exit 1 ; fi
fi
if [[ ! "${sshArgs:-}" ]] ; then
printf %s "$secret" | ( head -c 64 | write-secret "$keystore"/"$usage".key )
else
read -p 'Uploading secret with »ssh '"$sshArgs"'«. Enter to continue, or Ctrl+C to abort:'
printf %s "$secret" | ( head -c 64 | ssh $sshArgs /etc/nixos/utils/functions.sh write-secret "$keystore"/"$usage".key )
if [[ ${#secret} != 40 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" 1>&2 ; exit 1 ; fi
fi
printf %s "$secret" | head -c 64
)}
## Generates a random secret key and saves it to the keystore.
function add-key-random {( set -eu # 1: usage
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
</dev/urandom tr -dc 0-9a-f | head -c 64 | write-secret "$keystore"/"$usage".key
## Generates a random secret key.
function gen-key-random {( set -eu # 1: usage
</dev/urandom tr -dc 0-9a-f | head -c 64
)}

View File

@ -52,9 +52,11 @@ function partition-disks { { # 1: diskPaths
install -o root -g root -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" &&
blockDevs[$name]=$(losetup --show -f "$outFile") && prepend_trap "losetup -d '${blockDevs[$name]}'" EXIT # NOTE: this must not be inside a sub-shell!
else
local size=$( blockdev --getsize64 "${blockDevs[$name]}" || : )
local size=$( blockdev --getsize64 "${blockDevs[$name]}" || : ) ; local waste=$(( size - ${disk[size]} ))
if [[ ! $size ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" ; exit 1 ; fi
if [[ $size != "${disk[size]}" ]] ; then echo "Block device ${blockDevs[$name]}'s size $size does not match the size ${disk[size]} declared for $name" ; exit 1 ; fi
if (( waste < 0 )) ; then echo "Block device ${blockDevs[$name]}'s size $size is smaller than the size ${disk[size]} declared for $name" ; exit 1 ; fi
if (( waste > 0 )) && [[ ! ${disk[allowLarger]:-} ]] ; then echo "Block device ${blockDevs[$name]}'s size $size is bigger than the size ${disk[size]} declared for $name" ; exit 1 ; fi
if (( waste > 0 )) ; then echo "Wasting $(( waste / 1024))K of ${blockDevs[$name]} due to the size declared for $name (should be ${size}b)" ; fi
blockDevs[$name]=$(realpath "${blockDevs[$name]}")
fi
done
@ -73,7 +75,7 @@ function partition-disks { { # 1: diskPaths
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev's serial ($actual) does not match the serial (${disk[serial]}) declared for ${disk[name]}" ; exit 1 ; fi
fi
# can (and probably should) restore the backup:
( PATH=@{native.gptfdisk}/bin ; set -x ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup "${blockDevs[${disk[name]}]}" >$beQuiet )
( PATH=@{native.gptfdisk}/bin ; set -x ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beQuiet )
#partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}"
done
@{native.parted}/bin/partprobe "${blockDevs[@]}"

View File

@ -7,11 +7,10 @@
function install-system {( set -eu # 1: blockDev
prepare-installer "$@"
do-disk-setup "${argv[0]}"
init-or-restore-system
install-system-to $mnt
)}
## Does very simple argument passing and validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
## Does very simple argument paring and validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
function prepare-installer { # ...
generic-arg-parse "$@"
@ -30,32 +29,21 @@ function prepare-installer { # ...
if [[ -e "/dev/mapper/$luksName" ]] ; then echo "LUKS device mapping »$luksName« is already open. Close it before running the installer." ; exit 1 ; fi
done
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." ; exit 1 ; fi
if @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." ; exit 1 ; fi
done
if [[ ${SUDO_USER:-} ]] ; then function nix {( set +x ; declare -a args=("$@") ; PATH=$hostPath su - "$SUDO_USER" -c "$(declare -p args)"' ; nix "${args[@]}"' )} ; fi
if [[ ${args[debug]:-} ]] ; then set +e ; set -E ; trap 'code= ; cat 2>/dev/null || true ; @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} || code=$? ; if [[ $code ]] ; then exit $code ; fi' ERR ; fi # On error, instead of exiting straight away, open a shell to allow diagnosing/fixing the issue. Only exit if that shell reports failure (e.g. CtrlC + CtrlD). Unfortunately, the exiting has to be repeated for level of each nested sub-shells. The cat eats anything lined up on stdin, which would otherwise be run in the shell (TODO: but it blocks if there is nothing on stdin, requiring Ctrl+D to be pressed).
}
export PATH=$PATH:@{native.util-linux}/bin # Doing a system installation requires a lot of stuff from »util-linux«. This should probably be moved into the individual functions that actually use the tools ...
## Depending on the presence or absence of the »--restore« CLI flag, either runs the system's initialization or restore commands.
# The initialization commands are expected to create files that can't be stored in the (host's or target system's) nix store (i.e. secrets).
# The restore commands are expected to pull in a backup of the systems secrets and state from somewhere, and need to acknowledge that something happened by running »restore-supported-callback«.
function init-or-restore-system {( set -eu # (void)
if [[ ! ${args[restore]:-} ]] ; then
run-hook-script 'System Initialization' @{config.wip.fs.disks.initSystemCommands!writeText.initSystemCommands} # TODO: Do this later inside the chroot?
return # usually, this would be it ...
fi
requiresRestoration=$(mktemp) ; trap "rm -f '$requiresRestoration'" EXIT ; function restore-supported-callback {( rm -f "$requiresRestoration" )}
run-hook-script 'System Restoration' @{config.wip.fs.disks.restoreSystemCommands!writeText.restoreSystemCommands}
if [[ -e $requiresRestoration ]] ; then echo 'The »restoreSystemCommands« did not call »restore-supported-callback« to mark backup restoration as supported for this system. Assuming incomplete configuration.' 1>&2 ; exit 1 ; fi
)}
}
## The default command that will activate the system and install the bootloader. In a separate function to make it easy to replace.
function nixos-install-cmd {( set -eu # 1: mnt, 2: topLevel
# »nixos-install« by default does some stateful things (see the »--no« options below), builds and copies the system config (but that's already done), and then calls »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- $topLevel/bin/switch-to-configuration boot«, which is essentially the same as »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- @{config.system.build.installBootLoader} $targetSystem«, i.e. the side effects of »nixos-enter« and then calling the bootloader-installer.
PATH=@{config.systemd.package}/bin:$PATH TMPDIR=/tmp LC_ALL=C nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" #--debug
PATH=@{config.systemd.package}/bin:@{native.nix}/bin:$PATH TMPDIR=/tmp LC_ALL=C @{native.nixos-install-tools}/bin/nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" #--debug
)}
## Copies the system's dependencies to the disks mounted at »$mnt« and installs the bootloader. If »$inspect« is set, a root shell will be opened in »$mnt« afterwards.
@ -65,14 +53,8 @@ function install-system-to {( set -eu # 1: mnt, 2?: topLevel
targetSystem=@{config.system.build.toplevel}
trap - EXIT # start with empty traps for sub-shell
# Copy system closure to new nix store:
mkdir -p -m 755 $mnt/nix/var/nix ; mkdir -p -m 1775 $mnt/nix/store
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi
( set -x ; time nix copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; rm -rf $mnt/nix/var/nix/gcroots
# TODO: if the target has @{config.nix.autoOptimiseStore} and the host doesn't (there is no .links dir?), optimize now
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var ; chown :30000 $mnt/nix/store ; fi
# Link/create files that some tooling expects:
mkdir -p -m 755 $mnt/nix/var/nix ; mkdir -p -m 1775 $mnt/nix/store
mkdir -p $mnt/etc $mnt/run ; mkdir -p -m 1777 $mnt/tmp
mount tmpfs -t tmpfs $mnt/run ; prepend_trap "umount -l $mnt/run" EXIT # If there isn't anything mounted here, »activate« will mount a tmpfs (inside »nixos-enter«'s private mount namespace). That would hide the additions below.
[[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS # for »switch-to-configuration«
@ -80,19 +62,32 @@ function install-system-to {( set -eu # 1: mnt, 2?: topLevel
ln -sT $(realpath $targetSystem) $mnt/run/current-system
#mkdir -p /nix/var/nix/db # »nixos-containers« requires this but nothing creates it before nix is used. BUT »nixos-enter« screams: »/nix/var/nix/db exists and is not a regular file.«
# If the system configuration is supposed to be somewhere on the system, might as well initialize that:
if [[ @{config.environment.etc.nixos.source:-} && @{config.environment.etc.nixos.source} != /nix/store/* && @{config.environment.etc.nixos.source} != /run/current-system/config && ! -e $mnt/@{config.environment.etc.nixos.source} && -e $targetSystem/config ]] ; then
mkdir -p -- $mnt/@{config.environment.etc.nixos.source} ; cp -at $mnt/@{config.environment.etc.nixos.source} -- $targetSystem/config/*
chown -R 0:0 $mnt/@{config.environment.etc.nixos.source} ; chmod -R u+w $mnt/@{config.environment.etc.nixos.source}
fi
# Set this as the initial system generation:
mkdir -p -m 755 $mnt/nix/var/nix/profiles ; ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link ; ln -sT system-1-link $mnt/nix/var/nix/profiles/system
# Support cross architecture installation (not sure if this is actually required)
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.wip.preface.hardware}"-linux ]] ; then
mkdir -p $mnt/run/binfmt ; cp -a {,$mnt}/run/binfmt/"@{config.wip.preface.hardware}"-linux || true
mkdir -p $mnt/run/binfmt ; [[ ! -e /run/binfmt/"@{config.wip.preface.hardware}"-linux ]] || cp -a {,$mnt}/run/binfmt/"@{config.wip.preface.hardware}"-linux
# Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.wip.preface.hardware}-static«.
fi
# Copy system closure to new nix store:
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi
( set -x ; time nix --extra-experimental-features nix-command copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; rm -rf $mnt/nix/var/nix/gcroots
# TODO: if the target has @{config.nix.autoOptimiseStore} and the host doesn't (there is no .links dir?), optimize now
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var ; chown :30000 $mnt/nix/store ; fi
# Run the main install command (primarily for the bootloader):
mount -o bind,ro /nix/store $mnt/nix/store ; prepend_trap '! mountpoint -q $mnt/nix/store || umount -l $mnt/nix/store' EXIT # all the things required to _run_ the system are copied, but (may) need some more things to initially install it
mount -o bind,ro /nix/store $mnt/nix/store ; prepend_trap '! mountpoint -q $mnt/nix/store || umount -l $mnt/nix/store' EXIT # all the things required to _run_ the system are copied, but (may) need some more things to initially install it and/or enter the chroot (like qemu, see above)
run-hook-script 'Pre Installation' @{config.wip.fs.disks.preInstallCommands!writeText.preInstallCommands}
code=0 ; nixos-install-cmd $mnt "${topLevel:-$targetSystem}" || code=$?
#umount -l $mnt/nix/store # »nixos-enter« below still needs the bind mount, if installing cross-arch
run-hook-script 'post Installation' @{config.wip.fs.disks.postInstallCommands!writeText.postInstallCommands}
# Done!
if [[ ! ${args[no-inspect]:-} ]] ; then
@ -101,7 +96,7 @@ function install-system-to {( set -eu # 1: mnt, 2?: topLevel
else
( set +x ; echo "Installation done! This shell is in a chroot in the mounted system for inspection. Exiting the shell will unmount the system." )
fi
PATH=@{config.systemd.package}/bin:$PATH nixos-enter --root $mnt
PATH=@{config.systemd.package}/bin:$PATH @{native.nixos-install-tools}/bin/nixos-enter --root $mnt # TODO: construct path as it would be at login
#( cd $mnt ; mnt=$mnt @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} )
elif (( code != 0 )) ; then
exit $code

View File

@ -34,16 +34,16 @@ function populate-keystore { { # (void)
methods[$usage]=${methods[$from]} ; options[$usage]=${options[$from]}
done
for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" == home-pw || "${methods[$usage]}" == copy ]] ; then continue ; fi
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}" || return 1
if [[ "${methods[$usage]}" == home-composite || "${methods[$usage]}" == copy ]] ; then continue ; fi
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || return 1
done
for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" != home-pw ]] ; then continue ; fi
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}" || return 1
if [[ "${methods[$usage]}" != home-composite ]] ; then continue ; fi
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || return 1
done
for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" != copy ]] ; then continue ; fi
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}" || return 1
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || return 1
done
)}

View File

@ -26,7 +26,7 @@ function register-vbox {( set -eu # 1: diskImages, 2?: bridgeTo
$VBoxManage modifyvm "$vmName" --nic2 bridged --bridgeadapter2 $bridgeTo
fi
# TODO: The serial settings between qemu and vBox seem incompatible. With a simple »console=ttyS0«, vBox hangs on start. So just disable this for now an use qemu for headless setups. The UX here is awful anyway.
# The serial settings between qemu and vBox seem incompatible. With a simple »console=ttyS0«, vBox hangs on start. So just disable this for now an use qemu for headless setups. The UX here is awful anyway.
#$VBoxManage modifyvm "$vmName" --uart1 0x3F8 4 --uartmode1 server /run/user/$(id -u)/$vmName.socket # (guest sets speed)
set +x # avoid double-echoing
@ -63,8 +63,14 @@ function run-qemu {( set -eu # 1: diskImages
#qemu+=( -M virt -m 1024 -smp 4 -cpu cortex-a53 ) ; args[no-nat]=1
fi # else things are going to be quite slow
for decl in ${diskImages//:/ } ; do
qemu+=( -drive format=raw,file="${decl/*=/}" ) #,if=none,index=0,media=disk,id=disk0 -device "virtio-blk-pci,drive=disk0,disable-modern=on,disable-legacy=off" )
disks=( ${diskImages//:/ } ) ; for index in ${!disks[@]} ; do
# qemu+=( -drive format=raw,if=ide,file="${disks[$index]/*=/}" ) # »if=ide« is the default, which these days isn't great for driver support inside the VM
qemu+=( # not sure how correct the interpretations if the command are, and whether this works for more than one disk
-drive format=raw,file="${disks[$index]/*=/}",media=disk,if=none,index=${index},id=drive${index} # create the disk drive, without attaching it, name it driveX
-device ahci,acpi-index=${index},id=ahci${index} # create an (ich9-)AHCI bus named »ahciX«
-device ide-hd,drive=drive${index},bus=ahci${index}.${index} # attach IDE?! disk driveX as device X on bus »ahciX«
#-device virtio-blk-pci,drive=drive${index},disable-modern=on,disable-legacy=off # alternative to the two lines above (implies to be faster, but seems to require guest drivers)
)
done
if [[ @{config.boot.loader.systemd-boot.enable} || ${args[efi]:-} ]] ; then # UEFI. Otherwise it boots something much like a classic BIOS?
@ -78,11 +84,18 @@ function run-qemu {( set -eu # 1: diskImages
qemu+=( -kernel @{config.system.build.kernel}/Image -initrd @{config.system.build.initialRamdisk}/initrd -append "$(echo -n "@{config.boot.kernelParams[@]}")" )
fi
for param in "@{config.boot.kernelParams[@]}" ; do if [[ $param == 'console=ttyS0' || $param == 'console=ttyS0',* ]] ; then
qemu+=( -nographic ) # »-nographic« by default only shows output once th system reaches the login prompt. Add »config.boot.kernelParams = [ "console=tty1" "console=ttyS0" ]« to log to serial (»-nographic«) and the display (if there is one), preferring the last »console« option for the initrd shell (if enabled and requested).
fi ; done
# Add »config.boot.kernelParams = [ "console=tty1" "console=ttyS0" ]« to log to serial (»ttyS0«) and/or the display (»tty1«), preferring the last »console« option for the initrd shell (if enabled and requested).
logSerial= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=ttyS0'@( |,)* ]] ; then logSerial=1 ; fi
logScreen= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=tty1 '* ]] ; then logScreen=1 ; fi
if [[ $logSerial ]] ; then
if [[ $logScreen || ${args[graphic]:-} ]] ; then
qemu+=( -serial mon:stdio )
else
qemu+=( -nographic ) # Without »console=tty1« or no »console=...« parameter, boot messages won't be on the screen.
fi
fi
if [[ ! ${args[no-nat]:-} ]] ; then
if [[ ! ${args[no-nat]:-} ]] ; then # e.g. --nat-fw=8000-:8000,8001-:8001
qemu+=( -nic user,model=virtio-net-pci${args[nat-fw]:+,hostfwd=tcp::${args[nat-fw]//,/,hostfwd=tcp::}} ) # NATed, IPs: 10.0.2.15+/32, gateway: 10.0.2.2
fi

View File

@ -33,6 +33,11 @@ function prepend_trap { # 1: command, ...: trapNames
}
declare -f -t prepend_trap # required to modify DEBUG or RETURN traps
## Given the name to an existing bash function, this creates a copy of that function with a new name (in the current scope).
function copy-function { # 1: existingName, 2: newName
local original=$(declare -f "${1?existingName not provided}") ; if [[ ! $original ]] ; then echo "Function $1 is not defined" ; return 1 ; fi
eval "${original/$1/${2?newName not provided}}" # run the code declaring the function again, replacing only the first occurrence of the name
}
## Writes a »$name«d secret from stdin to »$targetDir«, ensuring proper file permissions.
function write-secret {( set -eu # 1: path, 2?: owner[:[group]], 3?: mode

View File

@ -1,6 +1,4 @@
# These functions have »pkgs.zfs« as undeclared dependency (so that they can alternatively use initramfs' »extraUtils«).
## Creates the system's ZFS pools and their datasets.
function create-zpools { # 1: mnt
local mnt=$1 ; local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do ( set -eu
@ -22,9 +20,9 @@ function create-zpools { # 1: mnt
if ! is-partition-on-disks "$part" "${blockDevs[@]}" ; then echo "Partition alias $part used by zpool ${pool[name]} does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
fi
done
( set -x ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" )
( PATH=@{native.zfs}/bin ; set -x ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" )
) && {
prepend_trap "zpool export '$poolName'" EXIT
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT
} ; done &&
ensure-datasets $mnt
@ -38,6 +36,7 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
mnt=$1 ; while [[ "$mnt" == */ ]] ; do mnt=${mnt:0:(-1)} ; done # (remove any tailing slashes)
filterExp=${2:-'^'}
tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT
zfs=@{native.zfs}/bin/zfs
: 'Step-through is very verbose and breaks the loop, disabling it for this function' ; trap - debug
printf '%s\0' "@{!config.wip.fs.zfs.datasets[@]}" | LC_ALL=C sort -z | while IFS= read -r -d $'\0' name ; do
@ -49,10 +48,10 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
explicitKeylocation=${props[keylocation]:-}
get-zfs-crypt-props "${dataset[name]}" props cryptKey cryptRoot
if zfs get -o value -H name "${dataset[name]}" &>/dev/null ; then # dataset exists: check its properties
if $zfs get -o value -H name "${dataset[name]}" &>/dev/null ; then # dataset exists: check its properties
if [[ ${props[mountpoint]:-} ]] ; then # don't set the current mount point again (no-op), cuz that fails if the dataset is mounted
current=$(zfs get -o value -H mountpoint "${dataset[name]}") ; current=${current/$mnt/}
current=$($zfs get -o value -H mountpoint "${dataset[name]}") ; current=${current/$mnt/}
if [[ ${props[mountpoint]} == "${current:-/}" ]] ; then unset props[mountpoint] ; fi
fi
if [[ ${props[keyformat]:-} == ephemeral ]] ; then
@ -61,54 +60,54 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
if [[ $explicitKeylocation ]] ; then props[keylocation]=$explicitKeylocation ; fi
unset props[encryption] ; unset props[keyformat] # can't change these anyway
names=$(IFS=, ; echo "${!props[*]}") ; values=$(IFS=$'\n' ; echo "${props[*]}")
if [[ $values != "$(zfs get -o value -H "$names" "${dataset[name]}")" ]] ; then (
if [[ $values != "$($zfs get -o value -H "$names" "${dataset[name]}")" ]] ; then (
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( "${name}=${props[$name]}" ) ; done
( set -x ; zfs set "${args[@]}" "${dataset[name]}" )
( PATH=@{native.zfs}/bin ; set -x ; zfs set "${args[@]}" "${dataset[name]}" )
) ; fi
if [[ $cryptRoot && $(zfs get -o value -H encryptionroot "${dataset[name]}") != "$cryptRoot" ]] ; then ( # inherit key from parent (which the parent would also already have done if necessary)
if [[ $(zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "zfs unload-key $cryptRoot || true" EXIT
if [[ $cryptRoot && $($zfs get -o value -H encryptionroot "${dataset[name]}") != "$cryptRoot" ]] ; then ( # inherit key from parent (which the parent would also already have done if necessary)
if [[ $($zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
$zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "$zfs unload-key $cryptRoot || true" EXIT
fi
if [[ $(zfs get -o value -H keystatus "${dataset[name]}") != available ]] ; then
zfs load-key -L file://"$cryptKey" "${dataset[name]}" # will unload with cryptRoot
if [[ $($zfs get -o value -H keystatus "${dataset[name]}") != available ]] ; then
$zfs load-key -L file://"$cryptKey" "${dataset[name]}" # will unload with cryptRoot
fi
( set -x ; zfs change-key -i "${dataset[name]}" )
( PATH=@{native.zfs}/bin ; set -x ; zfs change-key -i "${dataset[name]}" )
) ; fi
else ( # create dataset
if [[ ${props[keyformat]:-} == ephemeral ]] ; then
props[encryption]=aes-256-gcm ; props[keyformat]=hex ; props[keylocation]=file:///dev/stdin ; explicitKeylocation=file:///dev/null
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done
</dev/urandom tr -dc 0-9a-f | head -c 64 | ( set -x ; zfs create "${args[@]}" "${dataset[name]}" )
zfs unload-key "${dataset[name]}"
</dev/urandom tr -dc 0-9a-f | head -c 64 | ( PATH=@{native.zfs}/bin ; set -x ; zfs create "${args[@]}" "${dataset[name]}" )
$zfs unload-key "${dataset[name]}"
else
if [[ $cryptRoot && $cryptRoot != ${dataset[name]} && $(zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "zfs unload-key $cryptRoot || true" EXIT
if [[ $cryptRoot && $cryptRoot != ${dataset[name]} && $($zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
$zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "$zfs unload-key $cryptRoot || true" EXIT
fi
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done
( set -x ; zfs create "${args[@]}" "${dataset[name]}" )
( PATH=@{native.zfs}/bin ; set -x ; zfs create "${args[@]}" "${dataset[name]}" )
fi
if [[ ${props[canmount]} != off ]] ; then (
mount -t zfs -o zfsutil "${dataset[name]}" $tmpMnt ; trap "umount '${dataset[name]}'" EXIT
chmod 000 "$tmpMnt" ; ( chown "${dataset[uid]}:${dataset[gid]}" -- "$tmpMnt" ; chmod "${dataset[mode]}" -- "$tmpMnt" )
) ; fi
if [[ $explicitKeylocation && $explicitKeylocation != "${props[keylocation]:-}" ]] ; then
( set -x ; zfs set keylocation="$explicitKeylocation" "${dataset[name]}" )
( PATH=@{native.zfs}/bin ; set -x ; zfs set keylocation="$explicitKeylocation" "${dataset[name]}" )
fi
zfs snapshot -r "${dataset[name]}"@empty
$zfs snapshot -r "${dataset[name]}"@empty
) ; fi
eval 'declare -A allows='"${dataset[permissions]}"
for who in "${!allows[@]}" ; do
# »zfs allow $dataset« seems to be the only way to view permissions, and that is not very parsable -.-
( set -x ; zfs allow -$who "${allows[$who]}" "${dataset[name]}" >&2 )
( PATH=@{native.zfs}/bin ; set -x ; zfs allow -$who "${allows[$who]}" "${dataset[name]}" >&2 )
done
done
)}
## TODO
## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.wip.fs.keystore.keys."zfs/..."«).
function get-zfs-crypt-props { # 1: datasetPath, 2: name_cryptProps, 3: name_cryptKey, 4: name_cryptRoot
local hash=@{config.networking.hostName!hashString.sha256:0:8}
local keystore=/run/keystore-$hash