diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh index fa2884d08708..e960f1bbcc39 100755 --- a/nixos/modules/system/boot/stage-2-init.sh +++ b/nixos/modules/system/boot/stage-2-init.sh @@ -61,25 +61,44 @@ else fi -# Make /nix/store a read-only bind mount to enforce immutability of -# the Nix store. Note that we can't use "chown root:nixbld" here +# Give /nix/store the defined mount options. +# Typically, this should be: +# - 'ro' to enforce immutability of the Nix store +# - 'nosuid' to enforce no suid binaries make it into the store and get executed by accident. +# suid-binaries should only exist in /run/wrappers. +# If an attacker can make the nix builder produce suid binaries in the store, those should be useless. +# Another example is tampering with the store from an outside system. +# - 'nodev' to enforce no device files in the store +# Note that we can't use "chown root:nixbld" here # because users/groups might not exist yet. + # Silence chown/chmod to fail gracefully on a readonly filesystem # like squashfs. chown -f 0:30000 /nix/store chmod -f 1775 /nix/store -if [ -n "@readOnlyNixStore@" ]; then - # #375257: Ensure that we pick the "top" (i.e. last) mount so we don't get a false positive for a lower mount. - if ! [[ "$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)" =~ (^|,)ro(,|$) ]]; then - if [ -z "$container" ]; then - mount --bind /nix/store /nix/store - else - mount --rbind /nix/store /nix/store - fi - mount -o remount,ro,bind /nix/store - fi -fi +missing_opts=() # stores the missing mount options that still need to be applied to the nix store +current_opts="$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)" +for mount_opt in @nixStoreMountOpts@ ; do + # #375257: Ensure that we pick the "top" (i.e. last) mount so we don't get a false positive for a lower mount. + # matches '$opt', foo,$opt', '$opt,foo', 'foo,$opt,bar' + # crucially, it does not match 'foo$opt', otherwise e.g. 'errors=remount-ro' would yield false positives for 'ro' + if ! [[ "$current_opts" =~ (^|,)"$mount_opt"(,|$) ]]; then + missing_opts+=("$mount_opt") + fi +done + +# only change the mount options if any need changing +if [[ ${#missing_opts[@]} != 0 ]]; then + if [ -z "$container" ]; then + mount --bind /nix/store /nix/store + else + mount --rbind /nix/store /nix/store + fi + + # apply the missing mount options + mount -o remount,"$(IFS=, ; echo "${missing_opts[*]}")",bind /nix/store +fi if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then # Use /etc/resolv.conf supplied by systemd-nspawn, if applicable. diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix index d72ea225fb24..af90496753d9 100644 --- a/nixos/modules/system/boot/stage-2.nix +++ b/nixos/modules/system/boot/stage-2.nix @@ -17,7 +17,8 @@ let replacements = { shell = "${pkgs.bash}/bin/bash"; systemConfig = null; # replaced in ../activation/top-level.nix - inherit (config.boot) readOnlyNixStore systemdExecutable; + inherit (config.boot) systemdExecutable; + nixStoreMountOpts = lib.concatStringsSep " " (map lib.escapeShellArg config.boot.nixStoreMountOpts); inherit (config.system.nixos) distroName; inherit useHostResolvConf; inherit (config.system.build) earlyMountScript; @@ -57,11 +58,28 @@ in description = '' If set, NixOS will enforce the immutability of the Nix store by making {file}`/nix/store` a read-only bind - mount. Nix will automatically make the store writable when + mount. Nix will automatically make the store writable when needed. ''; }; + nixStoreMountOpts = mkOption { + type = types.listOf types.nonEmptyStr; + default = [ + "ro" + "nodev" + "nosuid" + ]; + description = '' + Defines the mount options used on a bind mount for the {file}`/nix/store`. + This affects the whole system except the nix store daemon, which will undo the bind mount. + + `ro` enforces immutability of the Nix store. + The store daemon should already not put device mappers or suid binaries in the store, + meaning `nosuid` and `nodev` enforce what should already be the case. + ''; + }; + systemdExecutable = mkOption { default = "/run/current-system/systemd/lib/systemd/systemd"; type = types.str; @@ -85,6 +103,9 @@ in config = { system.build.bootStage2 = bootStage2; - + boot.nixStoreMountOpts = [ + "nodev" + "nosuid" + ] ++ lib.optional config.boot.readOnlyNixStore "ro"; }; }