upgrade to 22.11, add extlinux & hetzner-vps:

- disable wip.fs.disks.devices.*.gptOffset (patch broken with 22.11),
- add wip.bootloader.extlinux,
- add wip.hardware.hetzner-vps profile,
- fix wip.services.dropbear.socketActivation,
This commit is contained in:
Niklas Gollenstede 2022-12-27 15:37:27 +01:00
parent df8c451050
commit a4ae2ab551
29 changed files with 476 additions and 180 deletions

22
.vscode/settings.json vendored
View File

@ -1,4 +1,10 @@
{ {
"markdown.validate.ignoredLinks": [
"./modules/",
"./modules/fs/",
"./patches/",
"./example/",
],
"cSpell.diagnosticLevel": "Information", // to find spelling mistakes "cSpell.diagnosticLevel": "Information", // to find spelling mistakes
"cSpell.words": [ "cSpell.words": [
"aarch64", // processor architecture "aarch64", // processor architecture
@ -9,6 +15,7 @@
"askpass", // program "askpass", // program
"attrset", "attrsets", // nix/abbr (attribute set) "attrset", "attrsets", // nix/abbr (attribute set)
"autologin", // agetty "autologin", // agetty
"autotrim", // zpool property
"binfmt", // abbr "binary format" "binfmt", // abbr "binary format"
"blkdiscard", // program "blkdiscard", // program
"blkid", // program / function "blkid", // program / function
@ -28,6 +35,7 @@
"compress_chksum", // f2fs "compress_chksum", // f2fs
"concat", // abbr "concat", // abbr
"controlvm", // virtual box "controlvm", // virtual box
"conv", // dd option
"convertfromraw", // virtual box "convertfromraw", // virtual box
"coreutils", // package "coreutils", // package
"createrawvmdk", // virtual box "createrawvmdk", // virtual box
@ -38,6 +46,7 @@
"dedup", // zfs "dedup", // zfs
"deps", // abbr dependencies "deps", // abbr dependencies
"devs", // abbr (devices) "devs", // abbr (devices)
"diffutils", // package
"dir_nlink", // ext4 option "dir_nlink", // ext4 option
"dmask", // mount "dmask", // mount
"dnodesize", // zfs "dnodesize", // zfs
@ -78,11 +87,15 @@
"gpio", // abbr (general purpose IO) "gpio", // abbr (general purpose IO)
"gptfdisk", // package "gptfdisk", // package
"headlessly", // word "headlessly", // word
"hetzner", // comapny
"HISTCONTROL", // bash option
"hostbus", // cli arg "hostbus", // cli arg
"hostfwd", // cli arg "hostfwd", // cli arg
"hostiocache", // virtual box "hostiocache", // virtual box
"hostport", // cli arg "hostport", // cli arg
"hugepages", // linux "hugepages", // linux
"ignoredups", // bash option
"ignorespace", // bash option
"inetutils", // package "inetutils", // package
"inodes", // plural "inodes", // plural
"internalcommands", // virtual box "internalcommands", // virtual box
@ -98,6 +111,7 @@
"libblockdev", // package "libblockdev", // package
"libraspberrypi", // program "libraspberrypi", // program
"libubootenv", // package "libubootenv", // package
"libutil", // concat
"logbias", // zfs "logbias", // zfs
"losetup", // program / function "losetup", // program / function
"lowerdir", // mount overlay option "lowerdir", // mount overlay option
@ -129,6 +143,7 @@
"noheadings", // cli arg "noheadings", // cli arg
"nohibernate", // kernel param "nohibernate", // kernel param
"nosuid", // mount option "nosuid", // mount option
"notrunc", // dd option
"ondemand", // concat "ondemand", // concat
"oneshot", // systemd "oneshot", // systemd
"optimise", // B/E "optimise", // B/E
@ -136,6 +151,7 @@
"OVMF", // package "OVMF", // package
"partlabel", // linux "partlabel", // linux
"partprobe", // program / function "partprobe", // program / function
"partuuid", // linux
"passthru", // nix "passthru", // nix
"pbkdf", // cli arg "pbkdf", // cli arg
"pflash", // cli arg "pflash", // cli arg
@ -166,9 +182,11 @@
"setsid", // program / function "setsid", // program / function
"setuid", // cli arg "setuid", // cli arg
"sgdisk", // program "sgdisk", // program
"shopt", // bash option
"showvminfo", // virtual box "showvminfo", // virtual box
"sigs", // cli arg "sigs", // cli arg
"socat", // program / function "socat", // program / function
"specialisation", "specialisations", // nixos option
"startvm", // virtual box "startvm", // virtual box
"stdenv", // nix "stdenv", // nix
"storageattach", // virtual box "storageattach", // virtual box
@ -176,6 +194,7 @@
"sublist", // Nix "sublist", // Nix
"swsuspend", // parameter "swsuspend", // parameter
"syncoid", // program "syncoid", // program
"syslinux", // package
"temproot", // abbr (temporary root (FS)) "temproot", // abbr (temporary root (FS))
"timesync", // systemd "timesync", // systemd
"TMPDIR", // env var "TMPDIR", // env var
@ -214,5 +233,6 @@
"yubikey", // program "yubikey", // program
"YubiKeys", // plural "YubiKeys", // plural
"zfsutil", // program / function "zfsutil", // program / function
] "zstd", "zstdcat", // program / function
],
} }

1
example/ssh-login.pub Normal file
View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+y3pbsUopXWSWVz+sowoMPTWv+u9Qj9aEl20NUN1LrKxduUv/fijmOyui92ZdTYJEu1oa5+V5jbxxlqNDn51yuwXXCxnIwFgh/aSl34Mc86HrjH73kZonya26jfCBE/7Mn9rppUmpkTt0Dk13Y1gnKp0OvuukEQ+Fa5ZxPLtyZ9d3zYDKIBbwNhISOHlllj8jgEMgGNNDGS7EdFh9AEnKG9d8s4+zTlHEXTom0srr4GBrRcG8qlV6DEcHB/aS7hhI5lA79H9AFWd1PjTV7ZUvX9sLsfRitcmQy2psicDxlagA15Lm/pLuf11t+IIO6bv9EG1cCAvkrGqnGqHLCPFYIW0rKyxD2IRq1ZG4+sbyQlgJiACw1WPiJkOXK88hmjlvwKGx4i8bk2bkXgcmxEHtd0rl+zsSMaZnNltaaGae7DVPKEYhn/sx+hzPpdpz7nhNs/OmN1Y61Zi8J8NHyBKWJ+lQSpV7AY8f2VNKvTFPdXzZmTYd4xVd7saGCa9235oqHX54rZ2zXZaj24zncnxhsvvKkLHeeYbr8knSZNDVfqCCzrm6FTV8aQ5M+QJwfnjVW+TQ/2hEnM1Jb4qbAylJfGY+LHZC9tysRyMwStvnB2+td4HX4hjO75CWbDsW6RLsXQjuzMNAwcGhftA9rnV8azIVX9PD4FYSadPptwuOsw== gpg_rsa.niklas@gollenstede.net

Binary file not shown.

View File

@ -5,7 +5,7 @@
); inputs = { ); inputs = {
# To update »./flake.lock«: $ nix flake update # To update »./flake.lock«: $ nix flake update
nixpkgs = { url = "github:NixOS/nixpkgs/nixos-22.05"; }; nixpkgs = { url = "github:NixOS/nixpkgs/nixos-22.11"; };
nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; }; nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; };
config = { type = "github"; owner = "NiklasGollenstede"; repo = "nix-wiplib"; dir = "example/defaultConfig"; rev = "5e9cc7ce3440be9ce6aeeaedcc70db9c80489c5f"; }; # Use some previous commit's »./example/defaultConfig/flake.nix« as the default config for this flake. config = { type = "github"; owner = "NiklasGollenstede"; repo = "nix-wiplib"; dir = "example/defaultConfig"; rev = "5e9cc7ce3440be9ce6aeeaedcc70db9c80489c5f"; }; # Use some previous commit's »./example/defaultConfig/flake.nix« as the default config for this flake.
@ -21,7 +21,7 @@
in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to: in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to:
repo # lib.* nixosModules.* overlays.* repo # lib.* nixosModules.* overlays.*
(lib.wip.mkSystemsFlake { inherit inputs; moduleInputs = builtins.removeAttrs inputs [ "nixpkgs" "nixos-hardware" ]; }) # nixosConfigurations.* apps.*-linux.* devShells.*-linux.* packages.*-linux.all-systems (lib.wip.mkSystemsFlake { inherit inputs; }) # nixosConfigurations.* apps.*-linux.* devShells.*-linux.* packages.*-linux.all-systems
(lib.wip.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: { # packages.*-linux.* defaultPackage.*-linux (lib.wip.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: { # packages.*-linux.* defaultPackage.*-linux
packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ]; packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ];
defaultPackage = self.packages.${localSystem}.all-systems; defaultPackage = self.packages.${localSystem}.all-systems;

View File

@ -37,7 +37,7 @@ in { imports = [ ({ ## Hardware
## What follows is a whole bunch of boilerplate-ish stuff, most of which multiple hosts would have in common and which would thus be moved to one or more modules: ## What follows is a whole bunch of boilerplate-ish stuff, most of which multiple hosts would have in common and which would thus be moved to one or more modules:
boot.loader.systemd-boot.enable = true; boot.loader.grub.enable = false; wip.bootloader.extlinux.enable = true;
# Example of adding and/or overwriting setup/maintenance functions: # Example of adding and/or overwriting setup/maintenance functions:
wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; }; wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; };
@ -96,6 +96,9 @@ in { imports = [ ({ ## Hardware
}) (lib.mkIf (name == "example-raidz") { ## Multi-disk ZFS setup }) (lib.mkIf (name == "example-raidz") { ## Multi-disk ZFS setup
wip.bootloader.extlinux.enable = lib.mkForce false; # use UEFI boot this time
boot.loader.systemd-boot.enable = true; boot.loader.grub.enable = false;
wip.fs.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; }); wip.fs.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; });
wip.fs.boot.enable = true; wip.fs.boot.size = "512M"; wip.fs.boot.enable = true; wip.fs.boot.size = "512M";
@ -114,7 +117,7 @@ in { imports = [ ({ ## Hardware
wip.fs.disks.partitions."rpool-arc-${hash}" = { type = "bf00"; }; wip.fs.disks.partitions."rpool-arc-${hash}" = { type = "bf00"; };
}) ({ ## Actual Config }) ({ ## Base Config
# Some base config: # Some base config:
wip.base.enable = true; wip.base.enable = true;
@ -132,7 +135,8 @@ in { imports = [ ({ ## Hardware
boot.kernelParams = [ /* "console=tty1" */ "console=ttyS0" "boot.shell_on_fail" ]; wip.base.panic_on_fail = false; boot.kernelParams = [ /* "console=tty1" */ "console=ttyS0" "boot.shell_on_fail" ]; wip.base.panic_on_fail = false;
wip.services.dropbear.enable = true; wip.services.dropbear.enable = true;
#wip.services.dropbear.rootKeys = [ ''${lib.readFile "${dirname}/....pub"}'' ]; wip.services.dropbear.rootKeys = ''${lib.readFile "${dirname}/../example/ssh-login.pub"}'';
wip.services.dropbear.socketActivation = true;
#wip.fs.disks.devices.primary.gptOffset = 64; #wip.fs.disks.devices.primary.gptOffset = 64;
#wip.fs.disks.devices.primary.size = "250059096K"; # 256GB Intel H10 #wip.fs.disks.devices.primary.size = "250059096K"; # 256GB Intel H10

View File

@ -53,7 +53,9 @@ in rec {
# ]; # ... # ]; # ...
# }; in inputs.wiplib.lib.wip.patchFlakeInputsAndImportRepo inputs patches ./. (inputs@{ self, nixpkgs, ... }: repo@{ nixosModules, overlays, lib, ... }: let ... in [ repo ... ]) # }; in inputs.wiplib.lib.wip.patchFlakeInputsAndImportRepo inputs patches ./. (inputs@{ self, nixpkgs, ... }: repo@{ nixosModules, overlays, lib, ... }: let ... in [ repo ... ])
patchFlakeInputsAndImportRepo = inputs: patches: repoPath: outputs: ( patchFlakeInputsAndImportRepo = inputs: patches: repoPath: outputs: (
patchFlakeInputs inputs patches (inputs: importRepo inputs repoPath (outputs inputs)) patchFlakeInputs inputs patches (inputs: importRepo inputs repoPath (outputs (inputs // {
self = inputs.self // { outPath = builtins.path { path = repoPath; name = "source"; }; }; # If the »flake.nix is in a sub dir of a repo, "${inputs.self}" would otherwise refer to the parent. (?)
})))
); );
# Merges a list of flake output attribute sets. # Merges a list of flake output attribute sets.
@ -82,21 +84,23 @@ in rec {
in let system = { inherit preface; } // (nixosSystem { in let system = { inherit preface; } // (nixosSystem {
system = buildSystem; system = buildSystem;
modules = [ # Anything specific to only this evaluation of the module tree should go here. modules = [ { imports = [ # Anything specific to only this evaluation of the module tree should go here.
(args.config or (importWrapped inputs entryPath).module) (args.config or (importWrapped inputs entryPath).module)
{ _module.args = { inherit name; }; } { _module.args.name = lib.mkOverride 99 name; } # (specialisations can somehow end up with the name »configuration«, which is very incorrect)
{ networking.hostName = name; } { networking.hostName = name; }
]; ]; _file = "${dirname}/flakes.nix#modules"; } ];
extraModules = modules ++ [ ({ # These are passed as »extraModules« module argument and can thus conveniently be reused when defining containers and such (Therefore define as much stuff as possible here). extraModules = modules ++ [ { imports = [ ({ # These are passed as »extraModules« module argument and can thus conveniently be reused when defining containers and such (Therefore define as much stuff as possible here).
}) ({ }) ({ config, ... }: {
nixpkgs = { inherit overlays; } nixpkgs = { inherit overlays; }
// (if buildSystem != targetSystem then { localSystem.system = buildSystem; crossSystem.system = targetSystem; } else { system = targetSystem; }); // (if buildSystem != targetSystem then { localSystem.system = buildSystem; crossSystem.system = targetSystem; } else { system = targetSystem; });
_module.args = builtins.removeAttrs specialArgs [ "name" ]; # (pass the args here, so that they also apply to any other evaluation using »extraModules«) _module.args = builtins.removeAttrs specialArgs [ "name" ]; # (pass the args here, so that they also apply to any other evaluation using »extraModules«)
${prefix}.base.includeInputs = lib.mkDefault inputs; ${prefix}.base.includeInputs = lib.mkDefault (if config.boot.isContainer then inputs // {
self = null; # avoid changing (and thus restarting) the containers on every trivial change
} else inputs);
system.nixos.revision = lib.mkIf (inputs?nixpkgs && inputs.nixpkgs?rev) inputs.nixpkgs.rev; # (evaluating the default value fails under some circumstances) system.nixos.revision = lib.mkIf (inputs?nixpkgs && inputs.nixpkgs?rev) inputs.nixpkgs.rev; # (evaluating the default value fails under some circumstances)
@ -122,7 +126,7 @@ in rec {
default = context: substituteImplicit { inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues config.${prefix}.setup.scripts); context = { inherit config options pkgs inputs; } // context; }; # inherit (builtins) trace; default = context: substituteImplicit { inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues config.${prefix}.setup.scripts); context = { inherit config options pkgs inputs; } // context; }; # inherit (builtins) trace;
}; };
}) ]; }) ]; _file = "${dirname}/flakes.nix#extraModules"; } ];
#specialArgs = specialArgs; # (This is already set during module import, while »_module.args« only becomes available during module evaluation (before that, using it causes infinite recursion). Since it can't be ensured that this is set in every circumstance where »extraModules« are being used, it should not be set at all.) #specialArgs = specialArgs; # (This is already set during module import, while »_module.args« only becomes available during module evaluation (before that, using it causes infinite recursion). Since it can't be ensured that this is set in every circumstance where »extraModules« are being used, it should not be set at all.)
@ -215,6 +219,7 @@ in rec {
for file in ~/.bash_profile ~/.bash_login ~/.profile ; do for file in ~/.bash_profile ~/.bash_login ~/.profile ; do
if [[ -r $file ]] ; then . $file ; break ; fi if [[ -r $file ]] ; then . $file ; break ; fi
done ; unset $file done ; unset $file
declare -A args=( ) ; declare -a argv=( ) # some functions expect these
# add active »hostName« to shell prompt # add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$} PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''}EOS ''}EOS
@ -235,10 +240,12 @@ in rec {
# ... and then call any of the functions in ./utils/setup-scripts/ (in the context of »$(hostname)«, where applicable). # ... and then call any of the functions in ./utils/setup-scripts/ (in the context of »$(hostname)«, where applicable).
# To get an equivalent root shell: $ nix run /etc/nixos/#functions-$(hostname) -- sudo bash # To get an equivalent root shell: $ nix run /etc/nixos/#functions-$(hostname) -- sudo bash
devShells = lib.mapAttrs (name: system: pkgs.mkShell { devShells = lib.mapAttrs (name: system: pkgs.mkShell {
inherit name;
nativeBuildInputs = tools ++ [ pkgs.nixos-install-tools ]; nativeBuildInputs = tools ++ [ pkgs.nixos-install-tools ];
shellHook = '' shellHook = ''
${system.config.${prefix}.setup.appliedScripts { native = pkgs; }} ${system.config.${prefix}.setup.appliedScripts { native = pkgs; }}
# add active »hostName« to shell prompt # add active »hostName« to shell prompt
declare -A args=( ) ; declare -a argv=( ) # some functions expect these
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$} PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''; '';
}) nixosConfigurations; }) nixosConfigurations;

View File

@ -73,9 +73,9 @@ function gen-key-home-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndUser(as
## Generates a reproducible secret by prompting for a pin/password and then challenging slot »$slot« of YubiKey »$serial«. ## 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«) function gen-key-yubikey-pin {( set -eu # 1: usage, 2: serialAndSlot(as »serial:slot«)
usage=$1 ; serialAndSlot=$2 usage=$1 ; serialAndSlot=$2
password=$(prompt-new-password "/ pin as challenge to YubiKey »$serialAndSlot« as key for »@{config.networking.hostName}:$usage«") pin=$( prompt-new-password "/ pin as challenge to YubiKey »$serialAndSlot« as key for »@{config.networking.hostName}:$usage«" )
if [[ ! $password ]] ; then exit 1 ; fi if [[ ! $pin ]] ; then exit 1 ; fi
gen-key-yubikey-challenge "$usage" "$serialAndSlot:$password" true "password / pin as key for »@{config.networking.hostName}:$usage«" gen-key-yubikey-challenge "$usage" "$serialAndSlot:$pin" true "password / pin as key for »@{config.networking.hostName}:$usage«"
)} )}
## Generates a reproducible secret for a certain »$use«case and optionally »$salt« on a »$host« by challenging slot »$slot« of YubiKey »$serial«. ## Generates a reproducible secret for a certain »$use«case and optionally »$salt« on a »$host« by challenging slot »$slot« of YubiKey »$serial«.

View File

@ -97,6 +97,7 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
local -a sgdisk=( --zap-all ) # delete existing part tables local -a sgdisk=( --zap-all ) # delete existing part tables
if [[ ${disk[gptOffset]} != 0 ]] ; then if [[ ${disk[gptOffset]} != 0 ]] ; then
echo 'Setting »gptOffset != 0« is currently not supported, as sgdisk with the patch applied somehow fails to read files' 1>&2 ; return 1
sgdisk+=( --move-main-table=$(( 2 + ${disk[gptOffset]} )) ) # this is incorrectly documented as --adjust-main-table in the man pages (at least versions 1.05 to 1.09 incl) sgdisk+=( --move-main-table=$(( 2 + ${disk[gptOffset]} )) ) # this is incorrectly documented as --adjust-main-table in the man pages (at least versions 1.05 to 1.09 incl)
sgdisk+=( --move-backup-table=$(( devSize/${disk[sectorSize]} - 1 - 32 - ${disk[gptOffset]} )) ) sgdisk+=( --move-backup-table=$(( devSize/${disk[sectorSize]} - 1 - 32 - ${disk[gptOffset]} )) )
fi fi
@ -201,40 +202,37 @@ function fix-grub-install {
## Mounts all file systems as it would happen during boot, but at path prefix »$mnt« (instead of »/«). ## Mounts all file systems as it would happen during boot, but at path prefix »$mnt« (instead of »/«).
function mount-system {( set -eu # 1: mnt, 2?: fstabPath function mount-system {( set -eu # 1: mnt, 2?: fstabPath, 3?: allowFail
# TODO: »config.system.build.fileSystems« is a dependency-sorted list. Could use that ... # While not generally required for fstab, nixos uses the dependency-sorted »config.system.build.fileSystems« list (instead of plain »builtins.attrValues config.fileSystems«) to generate »/etc/fstab« (provided »config.fileSystems.*.depends« is set correctly, e.g. for overlay mounts).
# mount --all --fstab @{config.system.build.toplevel}/etc/fstab --target-prefix "$1" -o X-mount.mkdir # (»--target-prefix« is not supported on Ubuntu 20.04) # This function depends on the file at »fstabPath« to be sorted like that.
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"}
# The following is roughly equivalent to: mount --all --fstab @{config.system.build.toplevel}/etc/fstab --target-prefix "$1" -o X-mount.mkdir # (but »--target-prefix« is not supported with older versions on »mount«, e.g. on Ubuntu 20.04 (but can't we use mount from nixpkgs?))
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"} ; allowFail=${3:-}
PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
<$fstabPath grep -v '^#' | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,} options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then ( if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target" || exit mkdir -p "$mnt"/"$target" || exit
[[ $type == tmpfs || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes) [[ $type == tmpfs || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
) || [[ $options == *,nofail,* ]] || exit ; fi # (actually, nofail already makes mount fail silently)
done || exit
# Since bind mounts may depend on other mounts not only for the target (which the sort takes care of) but also for the source, do all bind mounts last. This would break if there was a different bind mountpoint within a bind-mounted target.
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then : ; else continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target" || exit
if [[ $type == overlay ]] ; then if [[ $type == overlay ]] ; then
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target. options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target.
workdir=$(<<<"$options" grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi workdir=$( <<<"$options" grep -o -P ',workdir=\K[^,]+' || true ) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi
upperdir=$(<<<"$options" grep -o -P ',upperdir=\K[^,]+' || true) ; if [[ $upperdir ]] ; then mkdir -p "$upperdir" ; fi upperdir=$( <<<"$options" grep -o -P ',upperdir=\K[^,]+' || true ) ; if [[ $upperdir ]] ; then mkdir -p "$upperdir" ; fi
lowerdir=$(<<<"$options" grep -o -P ',lowerdir=\K[^,]+' || true) # TODO: test the lowerdir stuff lowerdir=$( <<<"$options" grep -o -P ',lowerdir=\K[^,]+' || true )
options=${options//,lowerdir=$lowerdir,/,lowerdir=$mnt/${lowerdir//:/:$mnt\/},} ; source=overlay options=${options//,lowerdir=$lowerdir,/,lowerdir=$mnt/${lowerdir//:/:$mnt\/},} ; source=overlay
else # TODO: test the lowerdir stuff
elif [[ $options =~ ,r?bind, ]] ; then
if [[ $source == /nix/store/* ]] ; then options=,ro$options ; fi if [[ $source == /nix/store/* ]] ; then options=,ro$options ; fi
source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" || exit ; fi source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" || exit ; fi
fi fi
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
) || [[ $options == *,nofail,* ]] || exit ; fi
) || [[ $options == *,nofail,* || $allowFail ]] || exit ; fi # (actually, nofail already makes mount fail silently)
done || exit done || exit
)} )}

View File

@ -77,10 +77,10 @@ function install-system-to {( set -u # 1: mnt
chmod -R u+w $mnt/@{config.environment.etc.nixos.source} || exit chmod -R u+w $mnt/@{config.environment.etc.nixos.source} || exit
fi fi
# Set this as the initial system generation: # Set this as the initial system generation (just in case »nixos-install-cmd« won't):
mkdir -p -m 755 $mnt/nix/var/nix/profiles || exit mkdir -p -m 755 $mnt/nix/var/nix/profiles || exit
ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link || exit [[ -e $mnt/nix/var/nix/profiles/system-1-link ]] || ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link || exit
ln -sT system-1-link $mnt/nix/var/nix/profiles/system || exit [[ -e $mnt/nix/var/nix/profiles/system ]] || ln -sT system-1-link $mnt/nix/var/nix/profiles/system || exit
# Support cross architecture installation (not sure if this is actually required) # 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 if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.wip.preface.hardware}"-linux ]] ; then
@ -91,7 +91,7 @@ function install-system-to {( set -u # 1: mnt
# Copy system closure to new nix store: # Copy system closure to new nix store:
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var || exit ; fi if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var || exit ; fi
( cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; if [[ ${args[quiet]:-} ]] ; then "${cmd[@]}" --quiet &>/dev/null || exit ; else set -x ; time "${cmd[@]}" || exit ; fi ) || exit ; rm -rf $mnt/nix/var/nix/gcroots || exit ( cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; if [[ ${args[quiet]:-} ]] ; then "${cmd[@]}" --quiet &>/dev/null || exit ; else set -x ; time "${cmd[@]}" || exit ; fi ) || exit ; rm -rf $mnt/nix/var/nix/gcroots || exit
# TODO: if the target has @{config.nix.autoOptimiseStore} and the host doesn't (there is no .links dir?), optimize now # TODO: if the target has @{config.nix.settings.auto-optimise-store} 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 || exit ; chown :30000 $mnt/nix/store || exit ; fi if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var || exit ; chown :30000 $mnt/nix/store || exit ; fi
# Run the main install command (primarily for the bootloader): # Run the main install command (primarily for the bootloader):

View File

@ -18,6 +18,7 @@ function register-vbox {( set -eu # 1: diskImages, 2?: bridgeTo
diskImage=${decl/*=/} diskImage=${decl/*=/}
if [[ ! -e $diskImage.vmdk ]] ; then if [[ ! -e $diskImage.vmdk ]] ; then
$VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through $VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through
#VBoxManage convertfromraw --format VDI $diskImage $diskImage.vmdk && rm $diskImage # convert
fi fi
$VBoxManage storageattach "$vmName" --storagectl SATA --port $(( index++ )) --device 0 --type hdd --medium $diskImage.vmdk $VBoxManage storageattach "$vmName" --storagectl SATA --port $(( index++ )) --device 0 --type hdd --medium $diskImage.vmdk
done done
@ -133,56 +134,57 @@ function add-bootkey-to-keydev {( set -eu # 1: blockDev, 2?: hostHash
# See »open-system«'s implementation for some example calls to this function. # See »open-system«'s implementation for some example calls to this function.
function mount-keystore-luks { # ...: cryptsetupOptions function mount-keystore-luks { # ...: cryptsetupOptions
# (For the traps to work, this can't run in a sub shell. The function therefore can't use »( set -eu ; ... )« internally and instead has to use »&&« after every command and in place of most »;«, and the function can't be called from a pipeline.) # (For the traps to work, this can't run in a sub shell. The function therefore can't use »( set -eu ; ... )« internally and instead has to use »&&« after every command and in place of most »;«, and the function can't be called from a pipeline.)
keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8} && local keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8}
mkdir -p -- /run/$keystore && mkdir -p -- /run/$keystore && prepend_trap "[[ ! -e /run/$keystore ]] || rmdir /run/$keystore" EXIT || return
@{native.cryptsetup}/bin/cryptsetup open "$@" /dev/disk/by-partlabel/$keystore $keystore && @{native.cryptsetup}/bin/cryptsetup open "$@" /dev/disk/by-partlabel/$keystore $keystore && prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $keystore" EXIT || return
mount -o nodev,umask=0077,fmask=0077,dmask=0077,ro /dev/mapper/$keystore /run/$keystore && mount -o nodev,umask=0077,fmask=0077,dmask=0077,ro /dev/mapper/$keystore /run/$keystore && prepend_trap "umount /run/$keystore" EXIT || return
prepend_trap "umount /run/$keystore ; @{native.cryptsetup}/bin/cryptsetup close $keystore ; rmdir /run/$keystore" EXIT
} }
## Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host. ## Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host.
# For any steps taken, it also adds the reaps to undo them on exit from the calling shell, and it always adds the exit trap to do the unmounting itself. # For any steps taken, it also adds the steps to undo them on exit from the calling shell, and it always adds the exit trap to do the unmounting itself.
# »diskImages« may be passed in the same format as to the installer. If so, any image files are ensured to be loop-mounted. # »diskImages« may be passed in the same format as to the installer. If so, any image files are ensured to be loop-mounted.
# Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.: # Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.:
# $ source ${config_wip_fs_disks_initSystemCommands1writeText_initSystemCommands} # $ source ${config_wip_fs_disks_initSystemCommands1writeText_initSystemCommands}
# $ source ${config_wip_fs_disks_restoreSystemCommands1writeText_restoreSystemCommands} # $ source ${config_wip_fs_disks_restoreSystemCommands1writeText_restoreSystemCommands}
# $ install-system-to /tmp/nixos-install-${config_networking_hostName} # $ install-system-to $mnt
# $ nixos-enter --root /tmp/nixos-install-${config_networking_hostName} # $ nixos-install --system ${config_system_build_toplevel} --no-root-passwd --no-channel-copy --root $mnt
# $ nixos-enter --root $mnt
function open-system { # 1?: diskImages function open-system { # 1?: diskImages
# (for the traps to work, this can't run in a sub shell, so also can't »set -eu«, so use »&&« after every command and in place of most »;«) # (for the traps to work, this can't run in a sub shell, so also can't »set -eu«, so use »&&« after every command and in place of most »;«)
local diskImages=${1:-} # If »diskImages« were specified and they point at files that aren't loop-mounted yet, then loop-mount them now: local diskImages=${1:-} # If »diskImages« were specified and they point at files that aren't loop-mounted yet, then loop-mount them now:
local images=$( losetup --list --all --raw --noheadings --output BACK-FILE ) local images=$( losetup --list --all --raw --noheadings --output BACK-FILE )
local decl && for decl in ${diskImages//:/ } ; do local decl ; for decl in ${diskImages//:/ } ; do
local image=${decl/*=/} && if [[ $image != /dev/* ]] && ! <<<$images grep -xF $image ; then local image=${decl/*=/} ; if [[ $image != /dev/* ]] && ! <<<$images grep -xF $image ; then
local blockDev=$( losetup --show -f "$image" ) && prepend_trap "losetup -d '$blockDev'" EXIT && local blockDev=$( losetup --show -f "$image" ) && prepend_trap "losetup -d '$blockDev'" EXIT || return
@{native.parted}/bin/partprobe "$blockDev" && @{native.parted}/bin/partprobe "$blockDev" || return
:;fi &&
:;done && fi
( @{native.systemd}/bin/udevadm settle -t 15 || true ) && # sometimes partitions aren't quite made available yet done
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
if [[ @{config.wip.fs.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore: if [[ @{config.wip.fs.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore:
mount-keystore-luks --key-file=<( printf %s "@{config.networking.hostName}" ) || mount-keystore-luks --key-file=<( printf %s "@{config.networking.hostName}" ) ||
mount-keystore-luks --key-file=/dev/disk/by-partlabel/bootkey-@{config.networking.hostName!hashString.sha256:0:8} || mount-keystore-luks --key-file=/dev/disk/by-partlabel/bootkey-@{config.networking.hostName!hashString.sha256:0:8} ||
mount-keystore-luks --key-file=<( read -s -p PIN: pin && echo ' touch!' >&2 && ykchalresp -2 "$pin" ) || mount-keystore-luks --key-file=<( read -s -p PIN: pin && echo ' touch!' >&2 && ykchalresp -2 "$pin" ) ||
# TODO: try static yubikey challenge # TODO: try static yubikey challenge
mount-keystore-luks mount-keystore-luks || return
fi && fi
local mnt=/tmp/nixos-install-@{config.networking.hostName} && if [[ ! -e $mnt ]] ; then mkdir -p "$mnt" && prepend_trap "rmdir '$mnt'" EXIT ; fi && mnt=/tmp/nixos-install-@{config.networking.hostName} # allow this to leak into the calling scope
if [[ ! -e $mnt ]] ; then mkdir -p "$mnt" && prepend_trap "rmdir '$mnt'" EXIT || return ; fi
open-luks-layers && # Load crypt layers and zfs pools: open-luks-layers || return # Load crypt layers and zfs pools:
if [[ $( LC_ALL=C type -t ensure-datasets ) == 'function' ]] ; then if [[ $( LC_ALL=C type -t ensure-datasets ) == 'function' ]] ; then
local poolName && for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if ! zfs get -o value -H name "$poolName" &>/dev/null ; then if ! zfs get -o value -H name "$poolName" &>/dev/null ; then
zpool import -f -N -R "$mnt" "$poolName" && prepend_trap "zpool export '$poolName'" EXIT && zpool import -f -N -R "$mnt" "$poolName" ; prepend_trap "zpool export '$poolName'" EXIT || return
:;fi && fi
: | zfs load-key -r "$poolName" || true && : | zfs load-key -r "$poolName" || true
:;done && done
ensure-datasets "$mnt" && ensure-datasets "$mnt" || return
:;fi && fi
prepend_trap "unmount-system '$mnt'" EXIT && mount-system "$mnt" && prepend_trap "unmount-system '$mnt'" EXIT && mount-system "$mnt" '' 1 || return
df -h | grep $mnt | cat
true # (success)
} }

View File

@ -32,10 +32,11 @@ function create-zpool { # 1: mnt, 2: poolName
fi fi
done done
<$keySrc tr -dc 0-9a-f | head -c 64 | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" || exit ) || exit <$keySrc tr -dc 0-9a-f | head -c 64 | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" || exit ) || exit
@{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null || true if [[ $keySrc == /dev/urandom ]] ; then @{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null ; fi
) || return ) || return
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
ensure-datasets $mnt '^'"$poolName"'($|[/])' || return ensure-datasets $mnt '^'"$poolName"'($|[/])' || return
if [[ ${args[debug]:-} ]] ; then @{native.zfs}/bin/zfs list -o name,canmount,mounted,mountpoint,keystatus,encryptionroot -r "$poolName" ; fi
} }
## Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined). ## Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined).

View File

@ -94,6 +94,32 @@ in rec {
s = from: to: builtins.substring from (to - from) hash; s = from: to: builtins.substring from (to - from) hash;
in "${s 0 8}-${s 8 12}-5${s 13 16}-8${s 17 20}-${s 20 32}"; in "${s 0 8}-${s 8 12}-5${s 13 16}-8${s 17 20}-${s 20 32}";
## Generate an entry for »systemd.tmpfiles.rules« with named attributes and proper escaping.
# This behaves according to the man page, but contrary to what the man page says/implies:
# * a single »%« is actually interpreted verbatim, as long as it is not followed by a letter (I guess a "specifier" is a »%« followed by a letter or another »%«),
# * »\xDD« escapes don't work in the »type« field (e.g. error: "Unknown modifiers in command 'x66+'" for "\x66+", which should be interpreted as "f+"),
# * none of the »\« I tried (»\n«, »\t«, »\xDD«) worked in the »path« (it simply removed the »\«); Only »\\« correctly results in »\«.
# => I assume the "Fields may contain C-style escapes" isn't technically incorrect, but the implied "... and they are interpreted as such" actually only applies to the »argument« field. The man page also doesn't actually say what consequence quoting has (I assume it prevents splitting at " ", but anything else?).
mkTmpfile = {
type ? "d", # One of [ "f" "f+" "w" "w+" "d" "D" "e" "v" "q" "Q" "p" "p+" "L" "L+" "c" "c+" "b" "b+" "C" "x" "X" "r" "R" "z" "Z" "t" "T" "h" "H" "a" "a+" "A" "A+" ].
path, pathSubstitute ? false, # String starting with "/" or (if »pathSubstitute == true«) also "%"
mode ? "-", # 4 digit octal works. Can be prefixed with "~" (mask with existing mode) or ":" (keep existing mode).
user ? "-", group ? user,
age ? "-",
argument ? "", argumentSubstitute ? false, # Depends on type.
}: let
# »systemd/src/basic/string-util.h« defines »WHITESPACE = " \t\n\r"«. »toJSON« escapes all of these except for " ", but that only matters if it is the first char of the (unquoted) argument.
esc = s: if s == "-" then s else builtins.toJSON s; noSub = builtins.replaceStrings [ "%" ] [ "%%" ];
argument' = builtins.substring 1 ((builtins.stringLength (esc argument)) - 2) (esc argument);
argument'' = if builtins.substring 0 1 argument' != " " then argument'
else ''\x20${builtins.substring 1 ((builtins.stringLength argument') - 1) argument'}'';
in ''${type} ${if pathSubstitute then esc path else noSub (esc path)} ${esc mode} ${esc user} ${esc group} ${esc age} ${if pathSubstitute then argument'' else noSub argument''}'';
/*
systemd.tmpfiles.rules = [
(lib.my.mkTmpfile { type = "f+"; path = "/home/user/t\"e\t%t\n!"; user = "user"; argument = " . foo\nbar\r\n\tba%!\n"; })
''f+ "/home/user/test!\"!\t!%%!\x20! !\n!${"\n"}%!%a!\\!" - "user" "user" - \x20. foo%a!\nbar\r\n\tba%%!\n''
];
*/
## Math ## Math

View File

@ -9,26 +9,25 @@ Things that really should be (more like) this by default.
```nix ```nix
#*/# end of MarkDown, beginning of NixOS module: #*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let dirname: inputs: specialArgs@{ config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
cfg = config.${prefix}.base; cfg = config.${prefix}.base;
inputDefinesSystem = cfg.includeInputs?self && cfg.includeInputs.self?nixosConfigurations && cfg.includeInputs.self.nixosConfigurations?${name};
in { in {
options.${prefix} = { base = { options.${prefix} = { base = {
enable = lib.mkEnableOption "saner defaults"; enable = lib.mkEnableOption "saner defaults";
includeInputs = lib.mkOption { description = "The system's build inputs, to be included in the flake registry, and on the »NIX_PATH« entry, such that they are available for self-rebuilds and e.g. as »pkgs« on the CLI."; type = lib.types.attrsOf lib.types.anything; apply = lib.filterAttrs (k: v: v != null); default = { }; }; includeInputs = lib.mkOption { description = "The system's build inputs, to be included in the flake registry, and on the »NIX_PATH« entry, such that they are available for self-rebuilds and e.g. as »pkgs« on the CLI."; type = lib.types.attrsOf lib.types.anything; apply = lib.filterAttrs (k: v: v != null); default = { }; };
panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; }; # It's stupidly hard to remove items from lists ... panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; example = false; }; # It's stupidly hard to remove items from lists ...
makeNoExec = lib.mkEnableOption "(almost) all filesystems being mounted as »noexec« (and »nosuid« and »nodev«)" // { default = false; }; autoUpgrade = lib.mkEnableOption "automatic NixOS updates and garbage collection" // { default = inputDefinesSystem; defaultText = lib.literalExpression "config.${prefix}.base.includeInputs.self.nixosConfigurations?\${name}"; example = false; };
}; }; }; };
# Bugfix: # Bugfix:
imports = [ (lib.wip.overrideNixpkgsModule ({ inherit inputs; } // specialArgs) "misc/extra-arguments.nix" (old: { config._module.args.utils = old._module.args.utils // { imports = [ (lib.wip.overrideNixpkgsModule ({ inherit inputs; } // specialArgs) "misc/extra-arguments.nix" (old: { config._module.args.utils = old._module.args.utils // {
escapeSystemdPath = s: builtins.replaceStrings [ "/" "-" " " "." ] [ "-" "\\x2d" "\\x20" "\\x2e" ] (lib.removePrefix "/" s); # The original function does not escape ».«, resulting in mismatching names with units generated from paths with ».« in them. escapeSystemdPath = s: builtins.replaceStrings [ "/" "-" " " "." ] [ "-" "\\x2d" "\\x20" "\\x2e" ] (lib.removePrefix "/" s); # BUG(PR): The original function does not escape ».«, resulting in mismatching names with units generated from paths with ».« in them (e.g. overwrites for implicit mount units).
}; })) ]; }; })) ];
config = let config = let
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
implied = true; # some mount points are implied (and forced) to be »neededForBoot« in »specialArgs.utils.pathsNeededForBoot« (this marks those here)
in lib.mkIf cfg.enable (lib.mkMerge [ ({ in lib.mkIf cfg.enable (lib.mkMerge [ ({
@ -36,52 +35,7 @@ in {
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName)); networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName));
environment.etc."machine-id".text = lib.mkDefault (builtins.substring 0 32 (builtins.hashString "sha256" "${config.networking.hostName}:machine-id")); # this works, but it "should be considered "confidential", and must not be exposed in untrusted environments" (not sure _why_ though) environment.etc."machine-id".text = lib.mkDefault (builtins.substring 0 32 (builtins.hashString "sha256" "${config.networking.hostName}:machine-id")); # this works, but it "should be considered "confidential", and must not be exposed in untrusted environments" (not sure _why_ though)
documentation.man.enable = lib.mkDefault config.documentation.enable; documentation.man.enable = lib.mkDefault config.documentation.enable;
nix.autoOptimiseStore = true; # because why not ... nix.settings.auto-optimise-store = lib.mkDefault true; # file deduplication, see https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-store-optimise.html#description
}) (lib.mkIf cfg.makeNoExec { ## Hardening
# This was the only "special" mount that did not have »nosuid« and »nodev« set:
systemd.packages = [ (lib.wip.mkSystemdOverride pkgs "dev-hugepages.mount" "[Mount]\nOptions=nosuid,nodev,noexec\n") ];
# And these were missing »noexec«:
boot.specialFileSystems."/dev".options = [ "noexec" ];
boot.specialFileSystems."/dev/shm".options = [ "noexec" ];
boot.specialFileSystems."/run/keys".options = [ "noexec" ];
# "Exceptions":
# /dev /dev/pts need »dev«
# /run/wrappers needs »exec« »suid«
# /run/binfmt needs »exec«
# /run /run/user/* may need »exec« (TODO: test)
# The Nix build dir (default: /tmp) needs »exec« (TODO)
# Ensure that the /nix/store is not »noexec«, even if the FS it is on is:
boot.initrd.postMountCommands = ''
if ! mountpoint -q $targetRoot/nix/store ; then
mount --bind $targetRoot/nix/store $targetRoot/nix/store
fi
mount -o remount,exec $targetRoot/nix/store
'';
# Nix has no (direct) settings to change where the builders have their »/build« bound to, but many builds will need it to be »exec«:
systemd.services.nix-daemon = { # TODO: while noexec on /tmp is the problem, neither of this solve it:
serviceConfig.PrivateTmp = true;
#serviceConfig.PrivateMounts = true; serviceConfig.ExecStartPre = "/run/wrappers/bin/mount -o remount,exec /tmp";
};
# And now make all "real" FSs »noexec« (if »wip.fs.temproot.enable = true«):
${prefix}.fs.temproot = let
it = { mountOptions = { nosuid = true; noexec = true; nodev = true; }; };
in { temp = it; local = it; remote = it; };
nix.allowedUsers = [ "root" "@wheel" ]; # This goes hand-in-hand with setting mounts as »noexec«. Cases where a user other than root should build stuff are probably fairly rare. A "real" user might want to, but that is either already in the wheel(sudo) group, or explicitly adding that user is pretty reasonable.
boot.postBootCommands = ''
# Make the /nix/store non-iterable, to make it harder for unprivileged programs to search the store for programs they should not have access to:
unshare --fork --mount --uts --mount-proc --pid -- ${pkgs.bash}/bin/bash -euc '
mount --make-rprivate / ; mount --bind /nix/store /nix/store ; mount -o remount,rw /nix/store
chmod -f 1771 /nix/store
chmod -f 751 /nix/store/.links
'
'';
}) ({ }) ({
@ -92,9 +46,7 @@ in {
systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot. systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot.
}) (let }) (lib.mkIf (inputDefinesSystem) { # non-flake
name = config.networking.hostName;
in lib.mkIf (cfg.includeInputs?self && cfg.includeInputs.self?nixosConfigurations && cfg.includeInputs.self.nixosConfigurations?${name}) { # non-flake
# Importing »<nixpkgs>« as non-flake returns a lambda returning the evaluated Nix Package Collection (»pkgs«). The most accurate representation of what that should be on the target host is the »pkgs« constructed when building it: # Importing »<nixpkgs>« as non-flake returns a lambda returning the evaluated Nix Package Collection (»pkgs«). The most accurate representation of what that should be on the target host is the »pkgs« constructed when building it:
system.extraSystemBuilderCmds = '' system.extraSystemBuilderCmds = ''
@ -109,11 +61,11 @@ in {
}) (lib.mkIf (cfg.includeInputs != { }) { # flake things }) (lib.mkIf (cfg.includeInputs != { }) { # flake things
# "input" to the system build is definitely also a nix version that works with flakes: # "input" to the system build is definitely also a nix version that works with flakes:
nix.extraOptions = "experimental-features = nix-command flakes"; # apparently, even nix 2.8 (in nixos-22.05) needs this nix.settings.experimental-features = [ "nix-command" "flakes" ]; # apparently, even nix 2.8 (in nixos-22.05) needs this
environment.systemPackages = [ pkgs.git ]; # necessary as external dependency when working with flakes environment.systemPackages = [ pkgs.git ]; # necessary as external dependency when working with flakes
# »inputs.self« does not have a name (that is known here), so just register it as »/etc/nixos/« system config: # »inputs.self« does not have a name (that is known here), so just register it as »/etc/nixos/« system config:
environment.etc.nixos.source = lib.mkIf (cfg.includeInputs?self) (lib.mkDefault "/run/current-system/config"); # (use this indirection to prevent every change in the config to necessarily also change »/etc«) environment.etc.nixos = lib.mkIf (cfg.includeInputs?self) (lib.mkDefault { source = "/run/current-system/config"; }); # (use this indirection to prevent every change in the config to necessarily also change »/etc«)
system.extraSystemBuilderCmds = lib.mkIf (cfg.includeInputs?self) '' system.extraSystemBuilderCmds = lib.mkIf (cfg.includeInputs?self) ''
ln -sT ${cfg.includeInputs.self} $out/config # (build input for reference) ln -sT ${cfg.includeInputs.self} $out/config # (build input for reference)
''; '';
@ -122,10 +74,28 @@ in {
nix.registry = lib.mapAttrs (name: input: lib.mkDefault { flake = input; }) (builtins.removeAttrs cfg.includeInputs [ "self" ]); nix.registry = lib.mapAttrs (name: input: lib.mkDefault { flake = input; }) (builtins.removeAttrs cfg.includeInputs [ "self" ]);
}) (lib.mkIf (cfg.autoUpgrade) {
nix.gc = { # gc everything older than 30 days, before updating
automatic = lib.mkDefault true; # let's hold back on this for a while
options = lib.mkDefault "--delete-older-than 30d";
dates = lib.mkDefault "Sun *-*-* 03:15:00";
};
nix.settings = { keep-outputs = true; keep-derivations = true; }; # don't GC build-time dependencies
system.autoUpgrade = {
enable = lib.mkDefault true;
flake = config.environment.etc.nixos.source; flags = map (dep: if dep == "self" then "" else "--update-input ${dep}") (builtins.attrNames cfg.includeInputs); # there is no "--update-inputs"?
# (Since all inputs to the system flake are linked as system-level flake registry entries, even "indirect" references that don't really exist on the target can be "updated" (which keeps the same hash but changes the path to point directly to the nix store).)
dates = "05:40"; randomizedDelaySec = "30min";
allowReboot = lib.mkDefault false;
};
}) ({ }) ({
# Free convenience: # Free convenience:
environment.shellAliases = { "with" = ''nix-shell --run "bash --login" -p''; }; # »with« doesn't seem to be a common linux command yet, and it makes sense here: with $package => do stuff in shell environment.shellAliases = { "with" = lib.mkDefault ''nix-shell --run "bash --login" -p''; }; # »with« doesn't seem to be a common linux command yet, and it makes sense here: with $package => do stuff in shell
programs.bash.promptInit = lib.mkDefault '' programs.bash.promptInit = lib.mkDefault ''
# Provide a nice prompt if the terminal supports it. # Provide a nice prompt if the terminal supports it.
@ -145,8 +115,12 @@ in {
''; # The non-interactive version of bash does not remove »\[« and »\]« from PS1, but without those the terminal gets confused about the cursor position after the prompt once one types more than a bit of text there (at least via serial or SSH). ''; # The non-interactive version of bash does not remove »\[« and »\]« from PS1, but without those the terminal gets confused about the cursor position after the prompt once one types more than a bit of text there (at least via serial or SSH).
environment.interactiveShellInit = lib.mkDefault '' environment.interactiveShellInit = lib.mkDefault ''
# In RePl mode: remove duplicates from history; don't save commands with a leading space.
HISTCONTROL=ignoredups:ignorespace
# For shells bound to serial interfaces (which can't detect the size of the screen on the other end), default to a more reasonable screen size than 24x80 blocks/chars:
if [[ "$(realpath /dev/stdin)" != /dev/tty[1-8] && $LINES == 24 && $COLUMNS == 80 ]] ; then if [[ "$(realpath /dev/stdin)" != /dev/tty[1-8] && $LINES == 24 && $COLUMNS == 80 ]] ; then
stty rows 34 cols 145 # Fairly large font on 1080p. Definitely a better default than 24x80. stty rows 34 cols 145 # Fairly large font on 1080p. (Setting this too large for the screen warps the output really badly.)
fi fi
''; '';
@ -154,6 +128,11 @@ in {
ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise) ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise)
''); '');
# deactivate by setting »system.activationScripts.diff-systems = lib.mkForce "";«
system.activationScripts.diff-systems = ''
if [[ -e /run/current-system ]] ; then ${pkgs.nix}/bin/nix --extra-experimental-features nix-command store diff-closures /run/current-system "$systemConfig" ; fi
'';
}) ]); }) ]);
} }

View File

@ -0,0 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -0,0 +1,100 @@
/*
# Extlinux Bootloader
A simple bootloader for legacy-BIOS environments, like (by default) Qemu.
This uses the same implementation as `boot.loader.generic-extlinux-compatible` to generate the bootloader configuration, but then actually also installs `extlinux` itself, instead of relying on something else (like an externally installed u-boot) to read and execute the configuration.
## Issues
* Updating between package versions is not atomic (and I am not sure it can be).
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.bootloader.extlinux;
targetMount = let path = lib.findFirst (path: config.fileSystems?${path}) "/" (lib.wip.parentPaths cfg.targetDir); in config.fileSystems.${path};
supportedFSes = [ "vfat" ]; fsSupported = fs: builtins.elem fs supportedFSes;
in {
options.${prefix} = { bootloader.extlinux = {
enable = lib.mkEnableOption (lib.mdDoc ''
a simple bootloader for legacy-BIOS environments, like (by default) Qemu.
This uses the same implementation as `boot.loader.generic-extlinux-compatible` to generate the bootloader configuration, but then actually also installs `extlinux` itself, instead of relying on something else (like an externally installed u-boot) to read and execute the configuration.
Any options affecting the config file generation by `boot.loader.generic-extlinux-compatible` apply, but `boot.loader.generic-extlinux-compatible.enable` should not be set to `true`.
'');
package = lib.mkOption { description = lib.mdDoc ''
The `syslinux` package to install `extlinux` from use.
''; type = lib.types.package; default = pkgs.syslinux; defaultText = lib.literalExpression "pkgs.syslinux"; };
targetDir = lib.mkOption { description = lib.mdDoc ''
The path in whose `./extlinux` sub dir `extlinux` will be installed to. When `nixos-rebuild boot/switch` gets called, this or a parent path needs to be mounted from {option}`.targetPart`.
''; type = lib.types.strMatching ''^/.*[^/]$''; default = "/boot"; };
targetPart = lib.mkOption { description = lib.mdDoc ''
The `/dev/disk/by-{id,label,partlabel,partuuid,uuid}/*` path of the *disk partition* holding the filesystem that `extlinux` is installed to. This must be formatted with a filesystem that `extlinux` supports and mounted as (a parent of) {option}`.targetDir`. The disk on which the partition lies will have the bootloader section of its MBR replaced by `extlinux`'s.
''; type = lib.types.strMatching ''^/.*[^/]$''; default = targetMount.device; };
allowInstableTargetPart = lib.mkOption { internal = true; type = lib.types.bool; };
showUI = (lib.mkEnableOption (lib.mdDoc "a simple graphical user interface to select the configuration to start during early boot.")) // { default = true; example = false; };
}; };
config = let
confFile = "/nixos/modules/system/boot/loader/generic-extlinux-compatible";
writeConfig = (import "${inputs.nixpkgs}/${confFile}" { inherit config pkgs lib; }).config.content.system.build.installBootLoader;
esc = lib.escapeShellArg;
in lib.mkIf cfg.enable ({
assertions = [ {
assertion = cfg.allowInstableTargetPart || (builtins.match ''^/dev/disk/by-(id|label|partlabel|partuuid|uuid)/.*[^/]$'' cfg.targetPart) != null;
message = ''`config.${prefix}.bootloader.extlinux.targetPart` is not set to a stable path in `/dev/disk/by-{id,label,partlabel,partuuid,uuid}/`. Not using a unique identifier (or even using a path that can unexpectedly change) is very risky.'';
} {
assertion = fsSupported targetMount.fsType;
message = ''`config.${prefix}.bootloader.extlinux.targetPart`'s closest mount (`${targetMount.mountPoint}`) is of type `${targetMount.fsType}`, which is not one of extlinux's supported types (${lib.concatStringsSep ", " supportedFSes}).'';
} ];
${prefix} = {
fs.boot = { enable = lib.mkDefault true; mountpoint = lib.mkDefault cfg.targetDir; };
bootloader.extlinux.allowInstableTargetPart = lib.mkForce false;
};
system.boot.loader.id = "extlinux";
system.build.installBootLoader = "${pkgs.writeShellScript "install-extlinux.sh" ''
${writeConfig} "$1" -d ${esc cfg.targetDir}
partition=${esc cfg.targetPart}
diskDev=$( realpath "$partition" ) || exit ; if [[ $diskDev == /dev/sd* ]] ; then
diskDev=$( shopt -s extglob ; echo "''${diskDev%%+([0-9])}" )
else
diskDev=$( shopt -s extglob ; echo "''${diskDev%%p+([0-9])}" )
fi
if [[ $( cat ${esc cfg.targetDir}/extlinux/installedVersion 2>/dev/null || true ) != ${esc cfg.package} ]] ; then
${esc cfg.package}/bin/extlinux --install ${esc cfg.targetDir}/extlinux || exit
printf '%s\n' ${esc cfg.package} >${esc cfg.targetDir}/extlinux/installedVersion
fi
if ! ${pkgs.diffutils}/bin/cmp --quiet --bytes=440 $diskDev ${esc cfg.package}/share/syslinux/mbr.bin ; then
dd bs=440 conv=notrunc count=1 if=${esc cfg.package}/share/syslinux/mbr.bin of=$diskDev status=none || exit
fi
if [[ '${toString cfg.showUI}' ]] ; then # showUI
for lib in libutil menu ; do
if ! ${pkgs.diffutils}/bin/cmp --quiet ${esc cfg.targetDir}/extlinux/$lib.c32 ${esc cfg.package}/share/syslinux/$lib.c32 ; then
cp ${esc cfg.package}/share/syslinux/$lib.c32 ${esc cfg.targetDir}/extlinux/$lib.c32
fi
done
if ! ${pkgs.gnugrep}/bin/grep -qP '^UI $' ${esc cfg.targetDir}/extlinux/extlinux.conf ; then
${pkgs.perl}/bin/perl -i -pe 's/TIMEOUT/UI menu.c32\nTIMEOUT/' ${esc cfg.targetDir}/extlinux/extlinux.conf
fi
fi
''}";
boot.loader.grub.enable = false;
});
}

View File

@ -0,0 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -0,0 +1,74 @@
/*
# `mount a -o noexec` Experiment
This is a (so far not successful) experiment to mount (almost) all filesystems as `noexec`, `nosuid` and `nodev` -- and to then deal with the consequences.
## Exceptions
* `/dev` and `/dev/pts` need `dev`
* `/run/wrappers` needs `exec` `suid`
* `/run/binfmt` needs `exec`
* `/run` `/run/user/*` may need `exec` (TODO: test)
* The Nix build dir (default: `/tmp`) needs `exec` (TODO!)
* Some parts of `/home/<user>/` will need `exec`
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.experiments.noexec;
in {
options.${prefix} = { experiments.noexec = {
enable = lib.mkEnableOption "(almost) all filesystems being mounted as »noexec« (and »nosuid« and »nodev«)";
}; };
config = let
in lib.mkIf cfg.enable (lib.mkMerge [ ({
# This was the only "special" mount that did not have »nosuid« and »nodev« set:
systemd.packages = [ (lib.wip.mkSystemdOverride pkgs "dev-hugepages.mount" "[Mount]\nOptions=nosuid,nodev,noexec\n") ];
# And these were missing »noexec«:
boot.specialFileSystems."/dev".options = [ "noexec" ];
boot.specialFileSystems."/dev/shm".options = [ "noexec" ];
boot.specialFileSystems."/run/keys".options = [ "noexec" ];
# Make all "real" FSs »noexec« (if »wip.fs.temproot.enable = true«):
${prefix}.fs.temproot = let
it = { mountOptions = { nosuid = true; noexec = true; nodev = true; }; };
in { temp = it; local = it; remote = it; };
# Ensure that the /nix/store is not »noexec«, even if the FS it is on is:
boot.initrd.postMountCommands = ''
if ! mountpoint -q $targetRoot/nix/store ; then
mount --bind $targetRoot/nix/store $targetRoot/nix/store
fi
mount -o remount,exec $targetRoot/nix/store
'';
# Nix has no (direct) settings to change where the builders have their »/build« bound to, but many builds will need it to be »exec«:
systemd.services.nix-daemon = { # TODO: while noexec on /tmp is the problem, neither of this solve it:
serviceConfig.PrivateTmp = true;
#serviceConfig.PrivateMounts = true; serviceConfig.ExecStartPre = "/run/wrappers/bin/mount -o remount,exec /tmp";
};
nix.allowedUsers = [ "root" "@wheel" ]; # This goes hand-in-hand with setting mounts as »noexec«. Cases where a user other than root should build stuff are probably fairly rare. A "real" user might want to, but that is either already in the wheel(sudo) group, or explicitly adding that user is pretty reasonable.
boot.postBootCommands = ''
# Make the /nix/store non-iterable, to make it harder for unprivileged programs to search the store for programs they should not have access to:
unshare --fork --mount --uts --mount-proc --pid -- ${pkgs.bash}/bin/bash -euc '
mount --make-rprivate / ; mount --bind /nix/store /nix/store ; mount -o remount,rw /nix/store
chmod -f 1770 /nix/store
chmod -f 751 /nix/store/.links
'
'';
}) ]);
}

View File

@ -32,7 +32,7 @@ in {
a;1 # active/boot ; part1 a;1 # active/boot ; part1
''; }; }; ''; }; };
}; };
fileSystems.${cfg.mountpoint} = { fsType = "vfat"; device = "/dev/disk/by-partlabel/boot-${hash}"; neededForBoot = true; options = [ "nosuid" "nodev" "noexec" "noatime" "umask=0027" ]; formatOptions = "-F 32"; }; fileSystems.${cfg.mountpoint} = { fsType = "vfat"; device = "/dev/disk/by-partlabel/boot-${hash}"; neededForBoot = true; options = [ "nosuid" "nodev" "noexec" "noatime" "umask=0027" "discard" ]; formatOptions = "-F 32"; };
}) ]); }) ]);

View File

@ -212,13 +212,22 @@ in {
"L+ /root/.bash_history - - - - ../${keep}/root/.bash_history" "L+ /root/.bash_history - - - - ../${keep}/root/.bash_history"
"f /${keep}/root/.local/share/nix/repl-history 0600 root root -" "f /${keep}/root/.local/share/nix/repl-history 0600 root root -"
"L+ /root/.local/share/nix/repl-history - - - - ../../../../${keep}/root/.local/share/nix/repl-history" "L+ /root/.local/share/nix/repl-history - - - - ../../../../${keep}/root/.local/share/nix/repl-history"
# SSHd host keys:
"d /${keep}/etc/ssh/ 0755 root root - -"
"L /etc/ssh/ssh_host_ed25519_key - - - - /${keep}/etc/ssh/ssh_host_ed25519_key"
"L /etc/ssh/ssh_host_ed25519_key.pub - - - - /${keep}/etc/ssh/ssh_host_ed25519_key.pub"
"L /etc/ssh/ssh_host_rsa_key - - - - /${keep}/etc/ssh/ssh_host_rsa_key"
"L /etc/ssh/ssh_host_rsa_key.pub - - - - /${keep}/etc/ssh/ssh_host_rsa_key.pub"
]; ];
fileSystems = { # this does get applied early fileSystems = { # this does get applied early
# (on systems without hardware clock, this allows systemd to provide an at least monolithic time after restarts) # (on systems without hardware clock, this allows systemd to provide an at least monolithic time after restarts)
"/var/lib/systemd/timesync" = { device = "/local/var/lib/systemd/timesync"; options = [ "bind" "nofail" ]; }; # TODO: add »neededForBoot = true«? "/var/lib/systemd/timesync" = { device = "/local/var/lib/systemd/timesync"; options = [ "bind" "nofail" ]; }; # TODO: add »neededForBoot = true«?
# save persistent timer states # save persistent timer states
"/var/lib/systemd/timers" = { device = "/local/var/lib/systemd/timers"; options = [ "bind" "nofail" ]; }; # TODO: add »neededForBoot = true«? "/var/lib/systemd/timers" = { device = "/local/var/lib/systemd/timers"; options = [ "bind" "nofail" ]; }; # TODO: add »neededForBoot = true«?
}; };
security.sudo.extraConfig = "Defaults lecture=never"; # default is »once«, but we'd forget that we did that security.sudo.extraConfig = "Defaults lecture=never"; # default is »once«, but we'd forget that we did that
@ -293,6 +302,7 @@ in {
compress_algorithm = "lz4"; # compress using lz4 compress_algorithm = "lz4"; # compress using lz4
compress_chksum = true; # verify checksums (when decompressing data blocks?) compress_chksum = true; # verify checksums (when decompressing data blocks?)
lazytime = true; # update timestamps asynchronously lazytime = true; # update timestamps asynchronously
discard = true;
}); });
} else { } else {
formatOptions = (lib.concatStrings [ formatOptions = (lib.concatStrings [
@ -305,6 +315,7 @@ in {
" -E nodiscard" # do not trim the whole blockdev upon formatting " -E nodiscard" # do not trim the whole blockdev upon formatting
" -e panic" # when (critical?) FS errors are detected, reset the system " -e panic" # when (critical?) FS errors are detected, reset the system
]); options = optionsToList (cfg.local.mountOptions // { ]); options = optionsToList (cfg.local.mountOptions // {
discard = true;
}); });
}); });
# TODO: "F2FS and its tools support various parameters not only for configuring on-disk layout, but also for selecting allocation and cleaning algorithms." # TODO: "F2FS and its tools support various parameters not only for configuring on-disk layout, but also for selecting allocation and cleaning algorithms."

View File

@ -29,9 +29,10 @@ in let module = {
autoApplyDuringBoot = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets in the initramfs phase during boot for this pool. This can be useful since the keystore is open but no datasets are mounted at that time") // { default = true; }; autoApplyDuringBoot = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets in the initramfs phase during boot for this pool. This can be useful since the keystore is open but no datasets are mounted at that time") // { default = true; };
autoApplyOnActivation = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets on system activation for this pool. This may fail for some changes since datasets may be mounted and the keystore is usually closed at this time. Enable ».autoApplyDuringBoot« and reboot to address this") // { default = true; }; autoApplyOnActivation = (lib.mkEnableOption "automatically re-applying changed dataset properties and create missing datasets on system activation for this pool. This may fail for some changes since datasets may be mounted and the keystore is usually closed at this time. Enable ».autoApplyDuringBoot« and reboot to address this") // { default = true; };
}; config = { }; config = {
props.autotrim = lib.mkDefault "on"; # These days, there should be no reason not to trim.
props.ashift = lib.mkOptionDefault "12"; # be explicit props.ashift = lib.mkOptionDefault "12"; # be explicit
props.comment = lib.mkOptionDefault "hostname=${config.networking.hostName};"; # This is just nice to know without needing to inspect the datasets (»zpool import« shows the comment).
props.cachefile = lib.mkOptionDefault "none"; # If it works on first boot without (stateful) cachefile, then it will also do so later. props.cachefile = lib.mkOptionDefault "none"; # If it works on first boot without (stateful) cachefile, then it will also do so later.
props.comment = lib.mkOptionDefault (if (builtins.stringLength config.networking.hostName <= (32 - 9)) then "hostname=${config.networking.hostName}" else config.networking.hostName); # This is just nice to know which host this pool belongs to, without needing to inspect the datasets (»zpool import« shows the comment). The »comment« is limited to 32 characters.
}; }))); }; })));
default = { }; default = { };
apply = lib.filterAttrs (k: v: v != null); apply = lib.filterAttrs (k: v: v != null);
@ -40,7 +41,7 @@ in let module = {
datasets = lib.mkOption { datasets = lib.mkOption {
description = "ZFS datasets managed and mounted on this host."; description = "ZFS datasets managed and mounted on this host.";
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = { type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = {
name = lib.mkOption { description = "Attribute name as name of the pool."; type = lib.types.str; default = name; readOnly = true; }; name = lib.mkOption { description = "Attribute name as name of the dataset."; type = lib.types.str; default = name; readOnly = true; };
props = lib.mkOption { description = "ZFS properties to set on the dataset."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; }; props = lib.mkOption { description = "ZFS properties to set on the dataset."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; };
mount = lib.mkOption { description = "Whether to create a »fileSystems« entry to mount the dataset. »noauto« creates an entry with that option set."; type = lib.types.enum [ true "noauto" false ]; default = false; }; mount = lib.mkOption { description = "Whether to create a »fileSystems« entry to mount the dataset. »noauto« creates an entry with that option set."; type = lib.types.enum [ true "noauto" false ]; default = false; };
permissions = lib.mkOption { description = ''Permissions to set on the dataset via »zfs allow«. Attribute names should express propagation/who and match »/^[dl]?([ug]\d+|e)$/«, the values are the list of permissions granted.''; type = lib.types.attrsOf lib.types.commas; default = { }; }; permissions = lib.mkOption { description = ''Permissions to set on the dataset via »zfs allow«. Attribute names should express propagation/who and match »/^[dl]?([ug]\d+|e)$/«, the values are the list of permissions granted.''; type = lib.types.attrsOf lib.types.commas; default = { }; };
@ -65,6 +66,7 @@ in let module = {
boot.zfs.devNodes = lib.mkDefault ''/dev/disk/by-partlabel" -d "/dev/mapper''; # Do automatic imports (initrd & systemd) by-partlabel or mapped device, instead of by-id, since that is how the pools were created. (This option is meant to support only a single path, but since it is not properly escaped, this works to pass two paths.) boot.zfs.devNodes = lib.mkDefault ''/dev/disk/by-partlabel" -d "/dev/mapper''; # Do automatic imports (initrd & systemd) by-partlabel or mapped device, instead of by-id, since that is how the pools were created. (This option is meant to support only a single path, but since it is not properly escaped, this works to pass two paths.)
services.zfs.autoScrub.enable = true; services.zfs.autoScrub.enable = true;
services.zfs.trim.enable = true; # (default) services.zfs.trim.enable = true; # (default)
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName)); # ZFS requires one, so might as well set a default.
## Implement »cfg.datasets.*.mount«: ## Implement »cfg.datasets.*.mount«:
fileSystems = lib.wip.mapMerge (path: { props, mount, ... }: if mount != false then { fileSystems = lib.wip.mapMerge (path: { props, mount, ... }: if mount != false then {
@ -143,6 +145,7 @@ in let module = {
fi fi
''); '');
#setsid ${extraUtils}/bin/ash -c "exec ${extraUtils}/bin/ash < /dev/$console >/dev/$console 2>/dev/$console" #setsid ${extraUtils}/bin/ash -c "exec ${extraUtils}/bin/ash < /dev/$console >/dev/$console 2>/dev/$console"
boot.zfs = if (builtins.substring 0 5 inputs.nixpkgs.lib.version) == "22.05" then { } else { allowHibernation = true; };
}) (let ## Implement »cfg.pools.*.autoApplyDuringBoot« and »cfg.pools.*.autoApplyOnActivation«: }) (let ## Implement »cfg.pools.*.autoApplyDuringBoot« and »cfg.pools.*.autoApplyOnActivation«:
@ -157,7 +160,7 @@ in let module = {
''; '';
ensure-datasets-for = filterBy: zfsPackage: ''( if [ ! "''${IN_NIXOS_ENTER:-}" ] && [ -e ${zfsPackage}/bin/zfs ] ; then ensure-datasets-for = filterBy: zfsPackage: ''( if [ ! "''${IN_NIXOS_ENTER:-}" ] && [ -e ${zfsPackage}/bin/zfs ] ; then
${lib.concatStrings (map (pool: '' ${lib.concatStrings (map (pool: ''
expected=${lib.escapeShellArg (builtins.toJSON (lib.mapAttrs (n: v: v.props) (lib.filterAttrs (path: _: path == pool || lib.wip.startsWith "${pool}/" path) cfg.datasets)))} expected=${lib.escapeShellArg (builtins.toJSON (lib.mapAttrs (n: v: v.props // (if v.permissions != { } then { ":permissions" = v.permissions; } else { })) (lib.filterAttrs (path: _: path == pool || lib.wip.startsWith "${pool}/" path) cfg.datasets)))}
if [ "$(${zfsPackage}/bin/zfs get -H -o value nixos-${prefix}:applied-datasets ${pool})" != "$expected" ] ; then if [ "$(${zfsPackage}/bin/zfs get -H -o value nixos-${prefix}:applied-datasets ${pool})" != "$expected" ] ; then
${ensure-datasets zfsPackage} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${prefix}:applied-datasets="$expected" ${pool} ${ensure-datasets zfsPackage} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${prefix}:applied-datasets="$expected" ${pool}
fi fi

View File

@ -0,0 +1,47 @@
/*
# Hetzner Cloud VPS Base Config
This is "device" type specific configuration for Hetzner's cloud VPS VMs.
## Installation / Testing
Hetzner Cloud unfortunately doesn't let one directly upload complete images to be deployed on a new server.
Since the VPSes are Qemu VMs, [installed](../../lib/setup-scripts/README.md#install-system-documentation) images can be tested locally in qemu:
```bash
nix run '.#<hostname>' -- sudo run-qemu $image
```
Once the system works locally, one can (for example) create a new server instance, boot it into rescue mode, and:
```bash
cat $image | zstd | ssh $newServerIP 'zstdcat >/dev/sda && sync'
```
If the image is very large, even if it is mostly empty and with compression, this can take quite a while.
Declaring a smaller image size and expanding it on boot may be a workaround, but (since it depends on the disk partitioning and filesystems used) is out of scope here.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.hardware.hetzner-vps;
in {
options.${prefix} = { hardware.hetzner-vps = {
enable = lib.mkEnableOption "the core hardware configuration for Hetzner VPS (virtual) hardware";
}; };
config = lib.mkIf cfg.enable ({
${prefix}.bootloader.extlinux.enable = true;
networking.interfaces.eth0.useDHCP = true;
networking.interfaces.eth0.ipv6.routes = [ { address = "::"; prefixLength = 0; via = "fe80::1"; } ];
networking.timeServers = [ "ntp1.hetzner.de" "ntp2.hetzner.com" "ntp3.hetzner.net" ]; # overwrite NTP
profiles.qemu-guest.enable = true;
});
}

View File

@ -4,9 +4,12 @@
Device specific config for any 64bit capable Raspberry PI (esp. rPI4 and CM4, but also 3B(+) and Zero 2, maybe others). Device specific config for any 64bit capable Raspberry PI (esp. rPI4 and CM4, but also 3B(+) and Zero 2, maybe others).
To set up a rPI host, connect it's boot medium (USB SSD or microSD card) to some other host, and run [`install-system`](../../lib/setup-scripts/README.md#install-system-documentation), directly targeting the future boot medium.
## Notes ## Notes
* Importing this module also makes the [`hardware.raspberry-pi."4".*`](https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/) options from [`nixos-hardware`](https://github.com/NixOS/nixos-hardware/) available (but disabled by default).
* All the `boot.loader.raspberryPi.*` stuff (and maybe also `boot.kernelParams`) seems to be effectively disabled if `boot.loader.generic-extlinux-compatible.enable == true`. * All the `boot.loader.raspberryPi.*` stuff (and maybe also `boot.kernelParams`) seems to be effectively disabled if `boot.loader.generic-extlinux-compatible.enable == true`.
The odd thing is that various online sources, including `nixos-hardware`, enable extlinux (this may simply be because `nixos-generate-config` sets this by default, purely based on the presence of `/boot/extlinux` in the (derived from more generic) default images). The odd thing is that various online sources, including `nixos-hardware`, enable extlinux (this may simply be because `nixos-generate-config` sets this by default, purely based on the presence of `/boot/extlinux` in the (derived from more generic) default images).
Without extlinux, u-boot is also disabled, which means that (on an rPI4) there is no way to get a generation selection menu, and generations would need to be restored by moving files in the `/boot` partition manually. Without extlinux, u-boot is also disabled, which means that (on an rPI4) there is no way to get a generation selection menu, and generations would need to be restored by moving files in the `/boot` partition manually.
@ -24,12 +27,12 @@ in {
options.${prefix} = { hardware.raspberry-pi = { options.${prefix} = { hardware.raspberry-pi = {
enable = lib.mkEnableOption "base configuration for Raspberry Pi 64bit hardware"; enable = lib.mkEnableOption "base configuration for Raspberry Pi 64bit hardware";
i2c = lib.mkEnableOption "the ARM i²c /dev/i2c-1 on pins 3+5 / GPIO2+3 (/ SDA+SCL)"; i2c = lib.mkEnableOption ''the ARM i²c /dev/i2c-1 on pins 3+5 / GPIO2+3 (/ SDA+SCL). Also see `hardware.raspberry-pi."4".i2c{0,1}.enable`'';
lightless = lib.mkEnableOption "operation without any activity lights"; lightless = lib.mkEnableOption "operation without any activity lights";
}; }; }; };
## Import the rPI4 config from nixos-hardware, but have it disabled by default. ## Import the rPI4 config from nixos-hardware, but have it disabled by default.
# This provides some options for additional onboard hardware components as »hardware.raspberry-pi."4".*«, see: https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/ # This provides some options for additional onboard hardware components as »hardware.raspberry-pi."4".*«, see: https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/
imports = let imports = let
path = "${inputs.nixos-hardware}/raspberry-pi/4/default.nix"; module = import path args; path = "${inputs.nixos-hardware}/raspberry-pi/4/default.nix"; module = import path args;
in [ { _file = path; imports = [ { in [ { _file = path; imports = [ {

View File

@ -0,0 +1,7 @@
# NixOS Module Patches
This directory contains not self-sufficient modules, but modules that are in fact only "patches" to existing modules in NixOS.
While other modules should have an `enable` option, these don't. They define options in the namespace of some existing module, and become active as soon as those options are assigned by some other module.
If there are conflicts in the defined options, then the modules will have to be not imported.

View File

@ -0,0 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -14,23 +14,23 @@ in {
options = { options = {
fileSystems = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule [ { options = { fileSystems = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule [ { options = {
preMountCommands = lib.mkOption { description = ""; type = lib.types.nullOr lib.types.str; default = null; }; preMountCommands = lib.mkOption { description = "Commands to be run as root every time before mounting this filesystem, but after all its dependents were mounted (TODO: or does this run just once per boot?). This does not order itself before or after `systemd-fsck@\${utils.escapeSystemdPath device}.service`."; type = lib.types.lines; default = ""; };
}; } ]); }; } ]);
}; }; }; };
config = let config = let
in ({ in ({
systemd.services = lib.wip.mapMerge (target: { device, preMountCommands, depends, ... }: if (preMountCommands != null) then let # The implementation is derived from the "mkfs-${device'}" service in nixpkgs.
systemd.services = lib.wip.mapMergeUnique (_: { mountPoint, device, preMountCommands, depends, ... }: if (preMountCommands != "") then let
isDevice = lib.wip.startsWith "/dev/" device; isDevice = lib.wip.startsWith "/dev/" device;
target' = utils.escapeSystemdPath target; mountPoint' = utils.escapeSystemdPath mountPoint;
device' = utils.escapeSystemdPath device; device' = utils.escapeSystemdPath device;
in { "pre-mount-${target'}" = { in { "pre-mount-${mountPoint'}" = {
description = "Prepare mounting (to) ${target}"; description = "Prepare mounting ${device} at ${mountPoint}";
wantedBy = [ "${target'}.mount" ]; before = [ "${target'}.mount" ] wantedBy = [ "${mountPoint'}.mount" ]; before = [ "${mountPoint'}.mount" ];
++ (lib.optional isDevice "systemd-fsck@${device'}.service"); # TODO: Does this exist for every device? Does depending on it instantiate the template?
requires = lib.optional isDevice "${device'}.device"; after = lib.optional isDevice "${device'}.device"; requires = lib.optional isDevice "${device'}.device"; after = lib.optional isDevice "${device'}.device";
unitConfig.RequiresMountsFor = depends ++ [ (builtins.dirOf device) (builtins.dirOf target) ]; unitConfig.RequiresMountsFor = depends ++ [ (builtins.dirOf device) (builtins.dirOf mountPoint) ];
unitConfig.DefaultDependencies = false; unitConfig.DefaultDependencies = false;
serviceConfig.Type = "oneshot"; script = preMountCommands; serviceConfig.Type = "oneshot"; script = preMountCommands;
}; } else { }) config.fileSystems; }; } else { }) config.fileSystems;

View File

@ -0,0 +1,25 @@
/*
# `nixpkgs` Profiles as Options
The "modules" in `<nixpkgs>/nixos/modules/profile/` define sets of option defaults to be used in certain contexts.
Unfortunately, they apply their options unconditionally once included, and NixOS' module system does not allow conditional imports.
This wrapper makes it possible to apply a profile based on some option's values.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module patch:
dirname: inputs: specialArgs@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
in {
imports = [
(lib.wip.overrideNixpkgsModule ({ inherit inputs; } // specialArgs) "profiles/qemu-guest.nix" (module: {
options.profiles.qemu-guest.enable = (lib.mkEnableOption "qemu-guest profile");
config = lib.mkIf config.profiles.qemu-guest.enable module;
}))
# Could do this automatically for all files in the directory ...
];
}

View File

@ -16,15 +16,15 @@ in {
options.${prefix} = { services.dropbear = { options.${prefix} = { services.dropbear = {
enable = lib.mkEnableOption "dropbear SSH daemon"; enable = lib.mkEnableOption "dropbear SSH daemon";
socketActivation = lib.mkEnableOption "socket activation mode for dropbear"; port = lib.mkOption { description = "TCP port to listen on and open a firewall rule for."; type = lib.types.port; default = 22; };
rootKeys = lib.mkOption { description = "Literal lines to write to »/root/.ssh/authorized_keys«"; default = [ ]; type = lib.types.listOf lib.types.str; }; socketActivation = lib.mkEnableOption "socket activation mode for dropbear, where systemd launches dropbear on incoming TCP connections, instead of dropbear permanently running and listening on its TCP port";
rootKeys = lib.mkOption { description = "Literal lines to write to »/root/.ssh/authorized_keys«"; default = ""; type = lib.types.lines; };
hostKeys = lib.mkOption { description = "Location of the host key(s) to use. If empty, then a key(s) will be generated at »/etc/dropbear/dropbear_(ecdsa/rsa)_host_key« on first access to the server."; default = [ ]; type = lib.types.listOf lib.types.path; }; hostKeys = lib.mkOption { description = "Location of the host key(s) to use. If empty, then a key(s) will be generated at »/etc/dropbear/dropbear_(ecdsa/rsa)_host_key« on first access to the server."; default = [ ]; type = lib.types.listOf lib.types.path; };
}; }; }; };
config = let config = let
defaultArgs = lib.concatStringsSep "" [ defaultArgs = lib.concatStringsSep "" [
"${pkgs.dropbear}/bin/dropbear" "${pkgs.dropbear}/bin/dropbear"
" -p 22" # listen on TCP/22
(if cfg.hostKeys == [ ] then ( (if cfg.hostKeys == [ ] then (
" -R" # generate host keys on connection " -R" # generate host keys on connection
) else lib.concatMapStrings (path: ( ) else lib.concatMapStrings (path: (
@ -33,9 +33,8 @@ in {
]; ];
in lib.mkIf cfg.enable (lib.mkMerge [ ({ in lib.mkIf cfg.enable (lib.mkMerge [ ({
environment.systemPackages = [ pkgs.dropbear ];
networking.firewall.allowedTCPPorts = [ 22 ]; networking.firewall.allowedTCPPorts = [ cfg.port ];
}) (lib.mkIf (!cfg.socketActivation) { }) (lib.mkIf (!cfg.socketActivation) {
@ -43,18 +42,21 @@ in {
description = "dropbear SSH server (listening)"; description = "dropbear SSH server (listening)";
wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ];
serviceConfig.ExecStartPre = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/"; serviceConfig.ExecStartPre = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/";
serviceConfig.ExecStart = defaultArgs + " -F -E"; # don't fork, use stderr serviceConfig.ExecStart = lib.concatStringsSep "" [
defaultArgs
" -p ${toString cfg.port}" # listen on TCP/${port}
" -F -E" # don't fork, use stderr
];
#serviceConfig.PIDFile = "/var/run/dropbear.pid"; serviceConfig.Type = "forking"; after = [ "network.target" ]; # alternative to »-E -F« (?) #serviceConfig.PIDFile = "/var/run/dropbear.pid"; serviceConfig.Type = "forking"; after = [ "network.target" ]; # alternative to »-E -F« (?)
}; };
}) (lib.mkIf (cfg.socketActivation) { }) (lib.mkIf (cfg.socketActivation) {
# This did not work: dropbear errors out with "socket operation on non-socket".
systemd.sockets.dropbear = { # start a »dropbear@.service« on any number of TCP connections on port 22 systemd.sockets.dropbear = { # start a »dropbear@.service« on any number of TCP connections on port 22
conflicts = [ "dropbear.service" ]; conflicts = [ "dropbear.service" ];
listenStreams = [ "22" ]; listenStreams = [ "${toString cfg.port}" ];
socketConfig.Accept = true; socketConfig.Accept = "yes";
#socketConfig.Restart = "always";
wantedBy = [ "sockets.target" ]; # (isn't this implicit?) wantedBy = [ "sockets.target" ]; # (isn't this implicit?)
}; };
systemd.services."dropbear@" = { systemd.services."dropbear@" = {
@ -62,21 +64,16 @@ in {
after = [ "syslog.target" ]; after = [ "syslog.target" ];
serviceConfig.PreExec = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/"; # or before socket? serviceConfig.PreExec = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/"; # or before socket?
serviceConfig.ExecStart = lib.concatStringsSep "" [ serviceConfig.ExecStart = lib.concatStringsSep "" [
"-" # for the most part ignore exit != 0 "-" # for the most part ignore exit != 0
defaultArgs defaultArgs
" -i" # handle a single connection on stdio " -i" # handle a single connection on stdio
]; ];
serviceConfig.StandardInput = "socket";
}; };
}) (lib.mkIf (cfg.rootKeys != [ ]) { }) (lib.mkIf (cfg.rootKeys != "") {
# TODO: This is suboptimal when the system gets activated more than once. Could use a »tmpfiles« rule, or simply »>« (instead of »>>« here). systemd.tmpfiles.rules = [ (lib.wip.mkTmpfile { type = "L+"; path = "/root/.ssh/authorized_keys"; argument = pkgs.writeText "root-ssh-authorized_keys" cfg.rootKeys; }) ];
system.activationScripts.root-authorized_keys = ''
mkdir -pm 700 /root/.ssh/
[ -e /root/.ssh/authorized_keys ] || install -m 600 -T /dev/null /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
${lib.concatMapStringsSep "\n" (key: "printf %s ${lib.escapeShellArg key} >>/root/.ssh/authorized_keys") cfg.rootKeys}
'';
}) ]); }) ]);

View File

@ -13,16 +13,17 @@ dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib; inherit (final) pkgs; inherit (inputs.self) lib;
in { in {
gptfdisk = prev.gptfdisk.overrideAttrs (old: rec { gptfdisk = prev.gptfdisk.overrideAttrs (old: let
pname = "gptfdisk"; pname = "gptfdisk";
in rec {
version = "1.0.9"; version = "1.0.9";
src = builtins.fetchurl { src = builtins.fetchurl {
url = "https://downloads.sourceforge.net/gptfdisk/${pname}-${version}.tar.gz"; url = "https://downloads.sourceforge.net/gptfdisk/${pname}-${version}.tar.gz";
sha256 = "1hjh5m77fmfq5m44yy61kchv7mbfgx026aw3jy5qxszsjckavzns"; sha256 = "1hjh5m77fmfq5m44yy61kchv7mbfgx026aw3jy5qxszsjckavzns";
}; };
patches = [ # (don't include »old.patches«, as the only one was upstreamed) /* patches = [ # (don't include »old.patches«, as the only one was upstreamed)
../patches/gptfdisk-move-secondary-table.patch ../patches/gptfdisk-move-secondary-table.patch
]; ]; */
}); });
libblockdev = prev.libblockdev.override { inherit (prev) gptfdisk; }; libblockdev = prev.libblockdev.override { inherit (prev) gptfdisk; };

View File

@ -1,13 +0,0 @@
diff --git a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
index 1a0da005029..7a82bbeeb19 100644
--- a/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
+++ b/nixos/modules/system/boot/loader/generic-extlinux-compatible/extlinux-conf-builder.sh
@@ -128,6 +128,8 @@ DEFAULT nixos-default
MENU TITLE ------------------------------------------------------------
TIMEOUT $timeout
+# TODO: Check whether this is an issue with uboot
+UI menu.c32
EOF
addEntry $default default >> $tmpFile