From a4ae2ab55159a718d76896200eedc73acc7acc97 Mon Sep 17 00:00:00 2001 From: Niklas Gollenstede Date: Tue, 27 Dec 2022 15:37:27 +0100 Subject: [PATCH] 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, --- .vscode/settings.json | 22 +++- example/ssh-login.pub | 1 + flake.lock | Bin 1658 -> 1658 bytes flake.nix | 4 +- hosts/example.nix.md | 10 +- lib/flakes.nix | 23 ++-- lib/setup-scripts/add-key.sh | 6 +- lib/setup-scripts/disk.sh | 40 ++++--- lib/setup-scripts/install.sh | 8 +- lib/setup-scripts/maintenance.sh | 60 ++++++----- lib/setup-scripts/zfs.sh | 3 +- lib/vars.nix | 26 +++++ modules/base.nix.md | 97 +++++++---------- modules/bootloader/default.nix | 1 + modules/bootloader/extlinux.nix.md | 100 ++++++++++++++++++ modules/experiments/default.nix | 1 + modules/experiments/noexec.nix.md | 74 +++++++++++++ modules/fs/boot.nix.md | 2 +- modules/fs/temproot.nix.md | 11 ++ modules/fs/zfs.nix.md | 9 +- modules/hardware/hetzner-vps.nix.md | 47 ++++++++ modules/hardware/rpi.nix.md | 7 +- modules/patches/README.md | 7 ++ modules/patches/default.nix | 1 + .../filesystem.nix.md} | 16 +-- modules/patches/profiles.nix.md | 25 +++++ modules/services/dropbear.nix.md | 35 +++--- overlays/gptfdisk.nix.md | 7 +- patches/nixpkgs-add-extlinud-ui.patch | 13 --- 29 files changed, 476 insertions(+), 180 deletions(-) create mode 100644 example/ssh-login.pub create mode 100644 modules/bootloader/default.nix create mode 100644 modules/bootloader/extlinux.nix.md create mode 100644 modules/experiments/default.nix create mode 100644 modules/experiments/noexec.nix.md create mode 100644 modules/hardware/hetzner-vps.nix.md create mode 100644 modules/patches/README.md create mode 100644 modules/patches/default.nix rename modules/{fs/patches.nix.md => patches/filesystem.nix.md} (51%) create mode 100644 modules/patches/profiles.nix.md delete mode 100644 patches/nixpkgs-add-extlinud-ui.patch diff --git a/.vscode/settings.json b/.vscode/settings.json index fa342da..b0a7ffb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,10 @@ { + "markdown.validate.ignoredLinks": [ + "./modules/", + "./modules/fs/", + "./patches/", + "./example/", + ], "cSpell.diagnosticLevel": "Information", // to find spelling mistakes "cSpell.words": [ "aarch64", // processor architecture @@ -9,6 +15,7 @@ "askpass", // program "attrset", "attrsets", // nix/abbr (attribute set) "autologin", // agetty + "autotrim", // zpool property "binfmt", // abbr "binary format" "blkdiscard", // program "blkid", // program / function @@ -28,6 +35,7 @@ "compress_chksum", // f2fs "concat", // abbr "controlvm", // virtual box + "conv", // dd option "convertfromraw", // virtual box "coreutils", // package "createrawvmdk", // virtual box @@ -38,6 +46,7 @@ "dedup", // zfs "deps", // abbr dependencies "devs", // abbr (devices) + "diffutils", // package "dir_nlink", // ext4 option "dmask", // mount "dnodesize", // zfs @@ -78,11 +87,15 @@ "gpio", // abbr (general purpose IO) "gptfdisk", // package "headlessly", // word + "hetzner", // comapny + "HISTCONTROL", // bash option "hostbus", // cli arg "hostfwd", // cli arg "hostiocache", // virtual box "hostport", // cli arg "hugepages", // linux + "ignoredups", // bash option + "ignorespace", // bash option "inetutils", // package "inodes", // plural "internalcommands", // virtual box @@ -98,6 +111,7 @@ "libblockdev", // package "libraspberrypi", // program "libubootenv", // package + "libutil", // concat "logbias", // zfs "losetup", // program / function "lowerdir", // mount overlay option @@ -129,6 +143,7 @@ "noheadings", // cli arg "nohibernate", // kernel param "nosuid", // mount option + "notrunc", // dd option "ondemand", // concat "oneshot", // systemd "optimise", // B/E @@ -136,6 +151,7 @@ "OVMF", // package "partlabel", // linux "partprobe", // program / function + "partuuid", // linux "passthru", // nix "pbkdf", // cli arg "pflash", // cli arg @@ -166,9 +182,11 @@ "setsid", // program / function "setuid", // cli arg "sgdisk", // program + "shopt", // bash option "showvminfo", // virtual box "sigs", // cli arg "socat", // program / function + "specialisation", "specialisations", // nixos option "startvm", // virtual box "stdenv", // nix "storageattach", // virtual box @@ -176,6 +194,7 @@ "sublist", // Nix "swsuspend", // parameter "syncoid", // program + "syslinux", // package "temproot", // abbr (temporary root (FS)) "timesync", // systemd "TMPDIR", // env var @@ -214,5 +233,6 @@ "yubikey", // program "YubiKeys", // plural "zfsutil", // program / function - ] + "zstd", "zstdcat", // program / function + ], } diff --git a/example/ssh-login.pub b/example/ssh-login.pub new file mode 100644 index 0000000..f8a8fe1 --- /dev/null +++ b/example/ssh-login.pub @@ -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 diff --git a/flake.lock b/flake.lock index c92f9ce98ff6c1b5bfd833cd4c826d914b600ea4..588658f15f4707d8a9fbdfe81b6ef8e033605c47 100644 GIT binary patch delta 271 zcmeyx^NVN01SVEXGgAZ0iIws?rR9#nKA~x;DV~9*g$9NpCBEfRF0Nrl9?nrkrg>oj zQCU@C>CW!jE~cKglNU0IPmW*}oP3U1LBA-qOvy??$=t%i7>H5~jZKqNO_R)16H`r7 zl2c8R(h?1gjLeLZEKHLPlTFMfD=;Zf)?;;;yo-4vyQPJ(iKV5{#A-S1yo@M!kMtxH zXX8L~mtgnIVt@VOl4MhZEVm>}H+|nKk5unqll08|l5}M2IVVqG6@WRx)Y2@`BGtmk yBH1)0Ez#7#B+b&$($FHsAT80v(9|d;$v82|)YQNT>I6?FL&M1jn8krcyaE6fRZ%Jc delta 273 zcmYk$K~BOz7(n6JT8#+{U9nOdbO*ut+nMQ%iHQNC0YeltP~3GoGZ1M~HEp%=5|BB8 zH*oFB6L=7hz)BV_yuf?kswNtkivzTDXG>SCWz?Gzmdt6bZkphai zU|fKQk#Vgf({#?wr@h=J6np@_eA{n~?($3&WOAiim*X_2NhGtWeg&9OmDR_|T$VHZ zaMSZ2(f|LBSvb4RKZ8P4NGvE8Ld%GQ46p`27DUAn6d^ diff --git a/flake.nix b/flake.nix index d6e1672..142b142 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ ); inputs = { # 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"; }; 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: 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 packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ]; defaultPackage = self.packages.${localSystem}.all-systems; diff --git a/hosts/example.nix.md b/hosts/example.nix.md index 7bb3de1..5dd2598 100644 --- a/hosts/example.nix.md +++ b/hosts/example.nix.md @@ -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: - 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: 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 + 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.boot.enable = true; wip.fs.boot.size = "512M"; @@ -114,7 +117,7 @@ in { imports = [ ({ ## Hardware wip.fs.disks.partitions."rpool-arc-${hash}" = { type = "bf00"; }; -}) ({ ## Actual Config +}) ({ ## Base Config # Some base config: 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; 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.size = "250059096K"; # 256GB Intel H10 diff --git a/lib/flakes.nix b/lib/flakes.nix index c8557bb..a4c42f8 100644 --- a/lib/flakes.nix +++ b/lib/flakes.nix @@ -53,7 +53,9 @@ in rec { # ]; # ... # }; in inputs.wiplib.lib.wip.patchFlakeInputsAndImportRepo inputs patches ./. (inputs@{ self, nixpkgs, ... }: repo@{ nixosModules, overlays, lib, ... }: let ... in [ repo ... ]) 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. @@ -82,21 +84,23 @@ in rec { in let system = { inherit preface; } // (nixosSystem { 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) - { _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; } - ]; + ]; _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; } // (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«) - ${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) @@ -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; }; - }) ]; + }) ]; _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.) @@ -215,6 +219,7 @@ in rec { for file in ~/.bash_profile ~/.bash_login ~/.profile ; do if [[ -r $file ]] ; then . $file ; break ; fi done ; unset $file + declare -A args=( ) ; declare -a argv=( ) # some functions expect these # add active »hostName« to shell prompt PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$} ''}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). # To get an equivalent root shell: $ nix run /etc/nixos/#functions-$(hostname) -- sudo bash devShells = lib.mapAttrs (name: system: pkgs.mkShell { + inherit name; nativeBuildInputs = tools ++ [ pkgs.nixos-install-tools ]; shellHook = '' ${system.config.${prefix}.setup.appliedScripts { native = pkgs; }} # add active »hostName« to shell prompt + declare -A args=( ) ; declare -a argv=( ) # some functions expect these PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$} ''; }) nixosConfigurations; diff --git a/lib/setup-scripts/add-key.sh b/lib/setup-scripts/add-key.sh index ed1385d..0f54833 100644 --- a/lib/setup-scripts/add-key.sh +++ b/lib/setup-scripts/add-key.sh @@ -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«. 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«") - if [[ ! $password ]] ; then exit 1 ; fi - gen-key-yubikey-challenge "$usage" "$serialAndSlot:$password" true "password / pin 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 [[ ! $pin ]] ; then exit 1 ; fi + 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«. diff --git a/lib/setup-scripts/disk.sh b/lib/setup-scripts/disk.sh index d4e55cc..9128f6a 100644 --- a/lib/setup-scripts/disk.sh +++ b/lib/setup-scripts/disk.sh @@ -97,6 +97,7 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize local -a sgdisk=( --zap-all ) # delete existing part tables 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-backup-table=$(( devSize/${disk[sectorSize]} - 1 - 32 - ${disk[gptOffset]} )) ) 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 »/«). -function mount-system {( set -eu # 1: mnt, 2?: fstabPath - # TODO: »config.system.build.fileSystems« is a dependency-sorted list. Could use that ... - # 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) - mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"} +function mount-system {( set -eu # 1: mnt, 2?: fstabPath, 3?: allowFail + # 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). + # This function depends on the file at »fstabPath« to be sorted like that. + + # 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 - <$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 options=,$options, ; options=${options//,ro,/,} - if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi + if ! mountpoint -q "$mnt"/"$target" ; then ( mkdir -p "$mnt"/"$target" || exit [[ $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 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 - 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 + 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 + lowerdir=$( <<<"$options" grep -o -P ',lowerdir=\K[^,]+' || true ) 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 source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" || exit ; fi fi + 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 )} diff --git a/lib/setup-scripts/install.sh b/lib/setup-scripts/install.sh index fa1dbb2..59b4c3d 100644 --- a/lib/setup-scripts/install.sh +++ b/lib/setup-scripts/install.sh @@ -77,10 +77,10 @@ function install-system-to {( set -u # 1: mnt chmod -R u+w $mnt/@{config.environment.etc.nixos.source} || exit 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 - 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-1-link ]] || ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link || 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) 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: 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 - # 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 # Run the main install command (primarily for the bootloader): diff --git a/lib/setup-scripts/maintenance.sh b/lib/setup-scripts/maintenance.sh index 1f3526f..39c734c 100644 --- a/lib/setup-scripts/maintenance.sh +++ b/lib/setup-scripts/maintenance.sh @@ -18,6 +18,7 @@ function register-vbox {( set -eu # 1: diskImages, 2?: bridgeTo diskImage=${decl/*=/} if [[ ! -e $diskImage.vmdk ]] ; then $VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through + #VBoxManage convertfromraw --format VDI $diskImage $diskImage.vmdk && rm $diskImage # convert fi $VBoxManage storageattach "$vmName" --storagectl SATA --port $(( index++ )) --device 0 --type hdd --medium $diskImage.vmdk 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. 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.) - keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8} && - mkdir -p -- /run/$keystore && - @{native.cryptsetup}/bin/cryptsetup open "$@" /dev/disk/by-partlabel/$keystore $keystore && - mount -o nodev,umask=0077,fmask=0077,dmask=0077,ro /dev/mapper/$keystore /run/$keystore && - prepend_trap "umount /run/$keystore ; @{native.cryptsetup}/bin/cryptsetup close $keystore ; rmdir /run/$keystore" EXIT + local keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8} + 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 && 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 && prepend_trap "umount /run/$keystore" EXIT || return } ## 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. # 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_restoreSystemCommands1writeText_restoreSystemCommands} -# $ install-system-to /tmp/nixos-install-${config_networking_hostName} -# $ nixos-enter --root /tmp/nixos-install-${config_networking_hostName} +# $ install-system-to $mnt +# $ nixos-install --system ${config_system_build_toplevel} --no-root-passwd --no-channel-copy --root $mnt +# $ nixos-enter --root $mnt 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 »;«) 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 decl && for decl in ${diskImages//:/ } ; do - local image=${decl/*=/} && if [[ $image != /dev/* ]] && ! <<<$images grep -xF $image ; then - local blockDev=$( losetup --show -f "$image" ) && prepend_trap "losetup -d '$blockDev'" EXIT && - @{native.parted}/bin/partprobe "$blockDev" && - :;fi && - :;done && - ( @{native.systemd}/bin/udevadm settle -t 15 || true ) && # sometimes partitions aren't quite made available yet + local decl ; for decl in ${diskImages//:/ } ; do + local image=${decl/*=/} ; if [[ $image != /dev/* ]] && ! <<<$images grep -xF $image ; then + local blockDev=$( losetup --show -f "$image" ) && prepend_trap "losetup -d '$blockDev'" EXIT || return + @{native.parted}/bin/partprobe "$blockDev" || return + + fi + 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: 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=<( read -s -p PIN: pin && echo ' touch!' >&2 && ykchalresp -2 "$pin" ) || # TODO: try static yubikey challenge - mount-keystore-luks - fi && + mount-keystore-luks || return + 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 - 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 - zpool import -f -N -R "$mnt" "$poolName" && prepend_trap "zpool export '$poolName'" EXIT && - :;fi && - : | zfs load-key -r "$poolName" || true && - :;done && - ensure-datasets "$mnt" && - :;fi && + zpool import -f -N -R "$mnt" "$poolName" ; prepend_trap "zpool export '$poolName'" EXIT || return + fi + : | zfs load-key -r "$poolName" || true + done + ensure-datasets "$mnt" || return + fi - prepend_trap "unmount-system '$mnt'" EXIT && mount-system "$mnt" && - - true # (success) + prepend_trap "unmount-system '$mnt'" EXIT && mount-system "$mnt" '' 1 || return + df -h | grep $mnt | cat } diff --git a/lib/setup-scripts/zfs.sh b/lib/setup-scripts/zfs.sh index c49558c..b9533f7 100644 --- a/lib/setup-scripts/zfs.sh +++ b/lib/setup-scripts/zfs.sh @@ -32,10 +32,11 @@ function create-zpool { # 1: mnt, 2: poolName fi 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 - @{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 prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || 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). diff --git a/lib/vars.nix b/lib/vars.nix index 8b00bc4..852cf84 100644 --- a/lib/vars.nix +++ b/lib/vars.nix @@ -94,6 +94,32 @@ in rec { 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}"; + ## 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 diff --git a/modules/base.nix.md b/modules/base.nix.md index 6b9f39c..8cad121 100644 --- a/modules/base.nix.md +++ b/modules/base.nix.md @@ -9,26 +9,25 @@ Things that really should be (more like) this by default. ```nix #*/# 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; cfg = config.${prefix}.base; + inputDefinesSystem = cfg.includeInputs?self && cfg.includeInputs.self?nixosConfigurations && cfg.includeInputs.self.nixosConfigurations?${name}; in { options.${prefix} = { base = { 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 = { }; }; - panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; }; # 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; }; + panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; example = false; }; # It's stupidly hard to remove items from lists ... + autoUpgrade = lib.mkEnableOption "automatic NixOS updates and garbage collection" // { default = inputDefinesSystem; defaultText = lib.literalExpression "config.${prefix}.base.includeInputs.self.nixosConfigurations?\${name}"; example = false; }; }; }; # Bugfix: 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 - 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 [ ({ @@ -36,52 +35,7 @@ in { 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) documentation.man.enable = lib.mkDefault config.documentation.enable; - nix.autoOptimiseStore = true; # because why not ... - - - }) (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 - ' - ''; + 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 }) ({ @@ -92,9 +46,7 @@ in { systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot. - }) (let - name = config.networking.hostName; - in lib.mkIf (cfg.includeInputs?self && cfg.includeInputs.self?nixosConfigurations && cfg.includeInputs.self.nixosConfigurations?${name}) { # non-flake + }) (lib.mkIf (inputDefinesSystem) { # non-flake # Importing »« 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 = '' @@ -109,11 +61,11 @@ in { }) (lib.mkIf (cfg.includeInputs != { }) { # flake things # "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 # »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) '' 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" ]); + }) (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: - 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 '' # 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). 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 - 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 ''; @@ -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) ''); + # 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 + ''; + }) ]); } diff --git a/modules/bootloader/default.nix b/modules/bootloader/default.nix new file mode 100644 index 0000000..6bba885 --- /dev/null +++ b/modules/bootloader/default.nix @@ -0,0 +1 @@ +dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { } diff --git a/modules/bootloader/extlinux.nix.md b/modules/bootloader/extlinux.nix.md new file mode 100644 index 0000000..3360de8 --- /dev/null +++ b/modules/bootloader/extlinux.nix.md @@ -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; + + }); +} diff --git a/modules/experiments/default.nix b/modules/experiments/default.nix new file mode 100644 index 0000000..6bba885 --- /dev/null +++ b/modules/experiments/default.nix @@ -0,0 +1 @@ +dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { } diff --git a/modules/experiments/noexec.nix.md b/modules/experiments/noexec.nix.md new file mode 100644 index 0000000..4a36d95 --- /dev/null +++ b/modules/experiments/noexec.nix.md @@ -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//` 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 + ' + ''; + + + }) ]); + +} diff --git a/modules/fs/boot.nix.md b/modules/fs/boot.nix.md index 289cc0f..45d2c61 100644 --- a/modules/fs/boot.nix.md +++ b/modules/fs/boot.nix.md @@ -32,7 +32,7 @@ in { 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"; }; }) ]); diff --git a/modules/fs/temproot.nix.md b/modules/fs/temproot.nix.md index 7bb4535..15dbf22 100644 --- a/modules/fs/temproot.nix.md +++ b/modules/fs/temproot.nix.md @@ -212,13 +212,22 @@ in { "L+ /root/.bash_history - - - - ../${keep}/root/.bash_history" "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" + + # 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 # (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«? # save persistent timer states "/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 @@ -293,6 +302,7 @@ in { compress_algorithm = "lz4"; # compress using lz4 compress_chksum = true; # verify checksums (when decompressing data blocks?) lazytime = true; # update timestamps asynchronously + discard = true; }); } else { formatOptions = (lib.concatStrings [ @@ -305,6 +315,7 @@ in { " -E nodiscard" # do not trim the whole blockdev upon formatting " -e panic" # when (critical?) FS errors are detected, reset the system ]); 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." diff --git a/modules/fs/zfs.nix.md b/modules/fs/zfs.nix.md index 51c3fff..0ce032a 100644 --- a/modules/fs/zfs.nix.md +++ b/modules/fs/zfs.nix.md @@ -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; }; 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 = { + props.autotrim = lib.mkDefault "on"; # These days, there should be no reason not to trim. 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.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 = { }; apply = lib.filterAttrs (k: v: v != null); @@ -40,7 +41,7 @@ in let module = { datasets = lib.mkOption { description = "ZFS datasets managed and mounted on this host."; 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 = { }; }; 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 = { }; }; @@ -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.) services.zfs.autoScrub.enable = true; 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«: fileSystems = lib.wip.mapMerge (path: { props, mount, ... }: if mount != false then { @@ -143,6 +145,7 @@ in let module = { fi ''); #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«: @@ -157,7 +160,7 @@ in let module = { ''; ensure-datasets-for = filterBy: zfsPackage: ''( if [ ! "''${IN_NIXOS_ENTER:-}" ] && [ -e ${zfsPackage}/bin/zfs ] ; then ${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 ${ensure-datasets zfsPackage} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${prefix}:applied-datasets="$expected" ${pool} fi diff --git a/modules/hardware/hetzner-vps.nix.md b/modules/hardware/hetzner-vps.nix.md new file mode 100644 index 0000000..995e9e4 --- /dev/null +++ b/modules/hardware/hetzner-vps.nix.md @@ -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 '.#' -- 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; + + }); +} diff --git a/modules/hardware/rpi.nix.md b/modules/hardware/rpi.nix.md index 41adf4d..c80d828 100644 --- a/modules/hardware/rpi.nix.md +++ b/modules/hardware/rpi.nix.md @@ -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). +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 +* 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`. 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. @@ -24,12 +27,12 @@ in { options.${prefix} = { hardware.raspberry-pi = { 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"; }; }; ## 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 path = "${inputs.nixos-hardware}/raspberry-pi/4/default.nix"; module = import path args; in [ { _file = path; imports = [ { diff --git a/modules/patches/README.md b/modules/patches/README.md new file mode 100644 index 0000000..78c6b7a --- /dev/null +++ b/modules/patches/README.md @@ -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. diff --git a/modules/patches/default.nix b/modules/patches/default.nix new file mode 100644 index 0000000..6bba885 --- /dev/null +++ b/modules/patches/default.nix @@ -0,0 +1 @@ +dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { } diff --git a/modules/fs/patches.nix.md b/modules/patches/filesystem.nix.md similarity index 51% rename from modules/fs/patches.nix.md rename to modules/patches/filesystem.nix.md index b73d983..6c8ba80 100644 --- a/modules/fs/patches.nix.md +++ b/modules/patches/filesystem.nix.md @@ -14,23 +14,23 @@ in { 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 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; - target' = utils.escapeSystemdPath target; + mountPoint' = utils.escapeSystemdPath mountPoint; device' = utils.escapeSystemdPath device; - in { "pre-mount-${target'}" = { - description = "Prepare mounting (to) ${target}"; - wantedBy = [ "${target'}.mount" ]; before = [ "${target'}.mount" ] - ++ (lib.optional isDevice "systemd-fsck@${device'}.service"); # TODO: Does this exist for every device? Does depending on it instantiate the template? + in { "pre-mount-${mountPoint'}" = { + description = "Prepare mounting ${device} at ${mountPoint}"; + wantedBy = [ "${mountPoint'}.mount" ]; before = [ "${mountPoint'}.mount" ]; 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; serviceConfig.Type = "oneshot"; script = preMountCommands; }; } else { }) config.fileSystems; diff --git a/modules/patches/profiles.nix.md b/modules/patches/profiles.nix.md new file mode 100644 index 0000000..8ab1b0f --- /dev/null +++ b/modules/patches/profiles.nix.md @@ -0,0 +1,25 @@ +/* + +# `nixpkgs` Profiles as Options + +The "modules" in `/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 ... + ]; + +} diff --git a/modules/services/dropbear.nix.md b/modules/services/dropbear.nix.md index 0923225..620bb7c 100644 --- a/modules/services/dropbear.nix.md +++ b/modules/services/dropbear.nix.md @@ -16,15 +16,15 @@ in { options.${prefix} = { services.dropbear = { enable = lib.mkEnableOption "dropbear SSH daemon"; - socketActivation = lib.mkEnableOption "socket activation mode for dropbear"; - rootKeys = lib.mkOption { description = "Literal lines to write to »/root/.ssh/authorized_keys«"; default = [ ]; type = lib.types.listOf lib.types.str; }; + port = lib.mkOption { description = "TCP port to listen on and open a firewall rule for."; type = lib.types.port; default = 22; }; + 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; }; }; }; config = let defaultArgs = lib.concatStringsSep "" [ "${pkgs.dropbear}/bin/dropbear" - " -p 22" # listen on TCP/22 (if cfg.hostKeys == [ ] then ( " -R" # generate host keys on connection ) else lib.concatMapStrings (path: ( @@ -33,9 +33,8 @@ in { ]; in lib.mkIf cfg.enable (lib.mkMerge [ ({ - environment.systemPackages = [ pkgs.dropbear ]; - networking.firewall.allowedTCPPorts = [ 22 ]; + networking.firewall.allowedTCPPorts = [ cfg.port ]; }) (lib.mkIf (!cfg.socketActivation) { @@ -43,18 +42,21 @@ in { description = "dropbear SSH server (listening)"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; 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« (?) }; }) (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 conflicts = [ "dropbear.service" ]; - listenStreams = [ "22" ]; - socketConfig.Accept = true; + listenStreams = [ "${toString cfg.port}" ]; + socketConfig.Accept = "yes"; + #socketConfig.Restart = "always"; wantedBy = [ "sockets.target" ]; # (isn't this implicit?) }; systemd.services."dropbear@" = { @@ -62,21 +64,16 @@ in { after = [ "syslog.target" ]; serviceConfig.PreExec = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/"; # or before socket? serviceConfig.ExecStart = lib.concatStringsSep "" [ - "-" # for the most part ignore exit != 0 + "-" # for the most part ignore exit != 0 defaultArgs " -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). - 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} - ''; + systemd.tmpfiles.rules = [ (lib.wip.mkTmpfile { type = "L+"; path = "/root/.ssh/authorized_keys"; argument = pkgs.writeText "root-ssh-authorized_keys" cfg.rootKeys; }) ]; }) ]); diff --git a/overlays/gptfdisk.nix.md b/overlays/gptfdisk.nix.md index 1a10826..910fe98 100644 --- a/overlays/gptfdisk.nix.md +++ b/overlays/gptfdisk.nix.md @@ -13,16 +13,17 @@ dirname: inputs: final: prev: let inherit (final) pkgs; inherit (inputs.self) lib; in { - gptfdisk = prev.gptfdisk.overrideAttrs (old: rec { + gptfdisk = prev.gptfdisk.overrideAttrs (old: let pname = "gptfdisk"; + in rec { version = "1.0.9"; src = builtins.fetchurl { url = "https://downloads.sourceforge.net/gptfdisk/${pname}-${version}.tar.gz"; 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 - ]; + ]; */ }); libblockdev = prev.libblockdev.override { inherit (prev) gptfdisk; }; diff --git a/patches/nixpkgs-add-extlinud-ui.patch b/patches/nixpkgs-add-extlinud-ui.patch deleted file mode 100644 index 9d25e69..0000000 --- a/patches/nixpkgs-add-extlinud-ui.patch +++ /dev/null @@ -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