nixos/stage-2-init: support nosuid/nodev mount options for /nix/store (#406184)
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
||||
- Create the first release note entry in this section!
|
||||
- The `boot.readOnlyNixStore` has been removed. Control over bind mount options on `/nix/store` is now offered by the `boot.nixStoreMountOpts` option.
|
||||
|
||||
## Other Notable Changes {#sec-nixpkgs-release-25.11-notable-changes}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -38,6 +39,16 @@ let
|
||||
in
|
||||
|
||||
{
|
||||
imports = [
|
||||
(lib.mkRemovedOptionModule
|
||||
[
|
||||
"boot"
|
||||
"readOnlyNixStore"
|
||||
]
|
||||
"Please use the `boot.nixStoreMountOpts' option to define mount options for the Nix store, including 'ro'"
|
||||
)
|
||||
];
|
||||
|
||||
options = {
|
||||
|
||||
boot = {
|
||||
@@ -51,14 +62,20 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
readOnlyNixStore = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
nixStoreMountOpts = mkOption {
|
||||
type = types.listOf types.nonEmptyStr;
|
||||
default = [
|
||||
"ro"
|
||||
"nodev"
|
||||
"nosuid"
|
||||
];
|
||||
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
|
||||
needed.
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -85,6 +102,5 @@ in
|
||||
config = {
|
||||
|
||||
system.build.bootStage2 = bootStage2;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,24 @@ import ./make-test-python.nix (
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# Prints the user's UID. Can't just do a shell script
|
||||
# because setuid is ignored for interpreted programs.
|
||||
uid = pkgs.writeCBin "uid" ''
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
int main(void) {
|
||||
printf("%d\n", geteuid());
|
||||
return 0;
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
users.users.alice = {
|
||||
isNormalUser = true;
|
||||
uid = 1000;
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
emptyDiskImages = [ 256 ];
|
||||
|
||||
@@ -32,6 +49,10 @@ import ./make-test-python.nix (
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.xxd ];
|
||||
|
||||
system.extraDependencies = [ uid ];
|
||||
|
||||
boot = {
|
||||
initrd = {
|
||||
# Format the upper Nix store.
|
||||
@@ -50,6 +71,19 @@ import ./make-test-python.nix (
|
||||
mount -t overlay overlay \
|
||||
-o lowerdir=/mnt-root/nix/store/ro,upperdir=/mnt-root/nix/store/rw,workdir=/mnt-root/nix/store/work \
|
||||
/mnt-root/nix/store
|
||||
|
||||
# Be very rude and try to put suid files and/or devices into the store.
|
||||
evil=/mnt-root/nix/store/evil
|
||||
mkdir -p $evil/bin $evil/dev
|
||||
|
||||
echo "making evil suid..." >&2
|
||||
cp /mnt-root/${builtins.unsafeDiscardStringContext "${uid}"}/bin/uid $evil/bin/suid
|
||||
chmod 4755 $evil/bin/suid
|
||||
[ -u $evil/bin/suid ] || exit 1
|
||||
|
||||
echo "making evil devzero..." >&2
|
||||
mknod -m 666 $evil/dev/zero c 1 5
|
||||
[ -c $evil/dev/zero ] || exit 1
|
||||
'';
|
||||
|
||||
kernelModules = [ "overlay" ];
|
||||
@@ -66,6 +100,32 @@ import ./make-test-python.nix (
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.succeed("test /etc/post-boot-ran")
|
||||
machine.fail("touch /nix/store/should-not-work");
|
||||
|
||||
for opt in ["ro", "nosuid", "nodev"]:
|
||||
with subtest(f"testing store mount option: {opt}"):
|
||||
machine.succeed(f'[[ "$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)" =~ (^|,){opt}(,|$) ]]')
|
||||
|
||||
# should still be suid
|
||||
machine.succeed('[ -u /nix/store/evil/bin/suid ]')
|
||||
# runs as alice and is not root
|
||||
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
|
||||
# can be remounted and runs as root
|
||||
machine.succeed('mount -o remount,suid,bind /nix/store && mount >&2')
|
||||
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 0 ]')
|
||||
# double checking we can undo it
|
||||
machine.succeed('mount -o remount,nosuid,bind /nix/store && mount >&2')
|
||||
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
|
||||
|
||||
# should still be a character device
|
||||
machine.succeed('[ -c /nix/store/evil/dev/zero ]')
|
||||
# should not work
|
||||
machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
|
||||
# can be remounted and works
|
||||
machine.succeed('mount -o remount,dev,bind /nix/store && mount >&2')
|
||||
machine.succeed('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
|
||||
# double checking we can undo it
|
||||
machine.succeed('mount -o remount,nodev,bind /nix/store && mount >&2')
|
||||
machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
|
||||
'';
|
||||
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ numinit ];
|
||||
|
||||
Reference in New Issue
Block a user