From 4b128008c5d9fde881ce1b0a25e60ae0415a14d5 Mon Sep 17 00:00:00 2001 From: nikstur Date: Sun, 19 Nov 2023 01:25:52 +0100 Subject: nixos/test-instrumentation: use file to set root password --- nixos/modules/testing/test-instrumentation.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'nixos') diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix index 9ee77cd79a9b..6aa718c1975d 100644 --- a/nixos/modules/testing/test-instrumentation.nix +++ b/nixos/modules/testing/test-instrumentation.nix @@ -207,7 +207,10 @@ in networking.usePredictableInterfaceNames = false; # Make it easy to log in as root when running the test interactively. - users.users.root.initialHashedPassword = mkOverride 150 ""; + # This needs to be a file because of a quirk in systemd credentials, + # where you cannot specify an empty string as a value. systemd-sysusers + # uses credentials to set passwords on users. + users.users.root.hashedPasswordFile = mkOverride 150 "${pkgs.writeText "hashed-password.root" ""}"; services.xserver.displayManager.job.logToJournal = true; -- cgit 1.4.1 From be2a4f37af10d36cc02ab66fd955f07b2ed7d53e Mon Sep 17 00:00:00 2001 From: Julian Stecklina Date: Tue, 17 Oct 2023 12:09:12 +0200 Subject: nixos/dbus: explicitly set homeMode for dbus Otherwise /run/dbus is created with the wrong permissions. For some reason our recent changes made /run/dbus be 0700 instead of 0755. This is actually the default of homeMode. So something worked by accident before? --- nixos/modules/services/system/dbus.nix | 1 + 1 file changed, 1 insertion(+) (limited to 'nixos') diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix index b47ebc92f93a..e8f8b48d0337 100644 --- a/nixos/modules/services/system/dbus.nix +++ b/nixos/modules/services/system/dbus.nix @@ -95,6 +95,7 @@ in uid = config.ids.uids.messagebus; description = "D-Bus system message bus daemon user"; home = homeDir; + homeMode = "0755"; group = "messagebus"; }; -- cgit 1.4.1 From eec1845744299b8be930029c58edde6d58957207 Mon Sep 17 00:00:00 2001 From: nikstur Date: Sun, 19 Nov 2023 23:16:14 +0100 Subject: nixos/systemd-sysusers: init --- nixos/modules/config/users-groups.nix | 10 +- nixos/modules/module-list.nix | 1 + nixos/modules/system/boot/systemd/sysusers.nix | 169 +++++++++++++++++++++++++ nixos/tests/all-tests.nix | 2 + nixos/tests/systemd-sysusers-immutable.nix | 64 ++++++++++ nixos/tests/systemd-sysusers-mutable.nix | 71 +++++++++++ 6 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 nixos/modules/system/boot/systemd/sysusers.nix create mode 100644 nixos/tests/systemd-sysusers-immutable.nix create mode 100644 nixos/tests/systemd-sysusers-mutable.nix (limited to 'nixos') diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 2aed620eb154..967ad0846d75 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -685,7 +685,7 @@ in { shadow.gid = ids.gids.shadow; }; - system.activationScripts.users = { + system.activationScripts.users = if !config.systemd.sysusers.enable then { supportsDryActivation = true; text = '' install -m 0700 -d /root @@ -694,7 +694,7 @@ in { ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \ -w ${./update-users-groups.pl} ${spec} ''; - }; + } else ""; # keep around for backwards compatibility system.activationScripts.update-lingering = let lingerDir = "/var/lib/systemd/linger"; @@ -711,7 +711,9 @@ in { ''; # Warn about user accounts with deprecated password hashing schemes - system.activationScripts.hashes = { + # This does not work when the users and groups are created by + # systemd-sysusers because the users are created too late then. + system.activationScripts.hashes = if !config.systemd.sysusers.enable then { deps = [ "users" ]; text = '' users=() @@ -729,7 +731,7 @@ in { printf ' - %s\n' "''${users[@]}" fi ''; - }; + } else ""; # keep around for backwards compatibility # for backwards compatibility system.activationScripts.groups = stringAfter [ "users" ] ""; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index e6fffd4716de..698514410560 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1486,6 +1486,7 @@ ./system/boot/systemd/repart.nix ./system/boot/systemd/shutdown.nix ./system/boot/systemd/sysupdate.nix + ./system/boot/systemd/sysusers.nix ./system/boot/systemd/tmpfiles.nix ./system/boot/systemd/user.nix ./system/boot/systemd/userdbd.nix diff --git a/nixos/modules/system/boot/systemd/sysusers.nix b/nixos/modules/system/boot/systemd/sysusers.nix new file mode 100644 index 000000000000..c619c2d91eb0 --- /dev/null +++ b/nixos/modules/system/boot/systemd/sysusers.nix @@ -0,0 +1,169 @@ +{ config, lib, pkgs, utils, ... }: + +let + + cfg = config.systemd.sysusers; + userCfg = config.users; + + sysusersConfig = pkgs.writeTextDir "00-nixos.conf" '' + # Type Name ID GECOS Home directory Shell + + # Users + ${lib.concatLines (lib.mapAttrsToList + (username: opts: + let + uid = if opts.uid == null then "-" else toString opts.uid; + in + ''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}'' + ) + userCfg.users) + } + + # Groups + ${lib.concatLines (lib.mapAttrsToList + (groupname: opts: ''g ${groupname} ${if opts.gid == null then "-" else toString opts.gid}'') userCfg.groups) + } + + # Group membership + ${lib.concatStrings (lib.mapAttrsToList + (groupname: opts: (lib.concatMapStrings (username: "m ${username} ${groupname}\n")) opts.members ) userCfg.groups) + } + ''; + + staticSysusersCredentials = pkgs.runCommand "static-sysusers-credentials" { } '' + mkdir $out; cd $out + ${lib.concatLines ( + (lib.mapAttrsToList + (username: opts: "echo -n '${opts.initialHashedPassword}' > 'passwd.hashed-password.${username}'") + (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users)) + ++ + (lib.mapAttrsToList + (username: opts: "echo -n '${opts.initialPassword}' > 'passwd.plaintext-password.${username}'") + (lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users)) + ++ + (lib.mapAttrsToList + (username: opts: "cat '${opts.hashedPasswordFile}' > 'passwd.hashed-password.${username}'") + (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users)) + ) + } + ''; + + staticSysusers = pkgs.runCommand "static-sysusers" + { + nativeBuildInputs = [ pkgs.systemd ]; + } '' + mkdir $out + export CREDENTIALS_DIRECTORY=${staticSysusersCredentials} + systemd-sysusers --root $out ${sysusersConfig}/00-nixos.conf + ''; + +in + +{ + + options = { + + # This module doesn't set it's own user options but reuses the ones from + # users-groups.nix + + systemd.sysusers = { + enable = lib.mkEnableOption (lib.mdDoc "systemd-sysusers") // { + description = lib.mdDoc '' + If enabled, users are created with systemd-sysusers instead of with + the custom `update-users-groups.pl` script. + + Note: This is experimental. + ''; + }; + }; + + }; + + config = lib.mkIf cfg.enable { + + assertions = [ + { + assertion = config.system.activationScripts.users == ""; + message = "system.activationScripts.users has to be empty to use systemd-sysusers"; + } + { + assertion = config.users.mutableUsers -> config.system.etc.overlay.enable; + message = "config.users.mutableUsers requires config.system.etc.overlay.enable."; + } + ]; + + systemd = lib.mkMerge [ + ({ + + # Create home directories, do not create /var/empty even if that's a user's + # home. + tmpfiles.settings.home-directories = lib.mapAttrs' + (username: opts: lib.nameValuePair opts.home { + d = { + mode = opts.homeMode; + user = username; + group = opts.group; + }; + }) + (lib.filterAttrs (_username: opts: opts.home != "/var/empty") userCfg.users); + }) + + (lib.mkIf config.users.mutableUsers { + additionalUpstreamSystemUnits = [ + "systemd-sysusers.service" + ]; + + services.systemd-sysusers = { + # Enable switch-to-configuration to restart the service. + unitConfig.ConditionNeedsUpdate = [ "" ]; + requiredBy = [ "sysinit-reactivation.target" ]; + before = [ "sysinit-reactivation.target" ]; + restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ]; + + serviceConfig = { + LoadCredential = lib.mapAttrsToList + (username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}") + (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users); + SetCredential = (lib.mapAttrsToList + (username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}") + (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users)) + ++ + (lib.mapAttrsToList + (username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}") + (lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users)) + ; + }; + }; + }) + ]; + + environment.etc = lib.mkMerge [ + (lib.mkIf (!userCfg.mutableUsers) { + "passwd" = { + source = "${staticSysusers}/etc/passwd"; + mode = "0644"; + }; + "group" = { + source = "${staticSysusers}/etc/group"; + mode = "0644"; + }; + "shadow" = { + source = "${staticSysusers}/etc/shadow"; + mode = "0000"; + }; + "gshadow" = { + source = "${staticSysusers}/etc/gshadow"; + mode = "0000"; + }; + }) + + (lib.mkIf userCfg.mutableUsers { + "sysusers.d".source = sysusersConfig; + }) + ]; + + }; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 25ee587e8f7a..5abb85c224bb 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -864,6 +864,8 @@ in { systemd-repart = handleTest ./systemd-repart.nix {}; systemd-shutdown = handleTest ./systemd-shutdown.nix {}; systemd-sysupdate = runTest ./systemd-sysupdate.nix; + systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix; + systemd-sysusers-immutable = runTest ./systemd-sysusers-immutable.nix; systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-timesyncd-nscd-dnssec = handleTest ./systemd-timesyncd-nscd-dnssec.nix {}; systemd-user-tmpfiles-rules = handleTest ./systemd-user-tmpfiles-rules.nix {}; diff --git a/nixos/tests/systemd-sysusers-immutable.nix b/nixos/tests/systemd-sysusers-immutable.nix new file mode 100644 index 000000000000..42cbf84d175e --- /dev/null +++ b/nixos/tests/systemd-sysusers-immutable.nix @@ -0,0 +1,64 @@ +{ lib, ... }: + +let + rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6"; + normaloPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639"; + newNormaloPassword = "mellow"; +in + +{ + + name = "activation-sysusers-immutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { + systemd.sysusers.enable = true; + users.mutableUsers = false; + + # Override the empty root password set by the test instrumentation + users.users.root.hashedPasswordFile = lib.mkForce null; + users.users.root.initialHashedPassword = rootPassword; + users.users.normalo = { + isNormalUser = true; + initialHashedPassword = normaloPassword; + }; + + specialisation.new-generation.configuration = { + users.users.new-normalo = { + isNormalUser = true; + initialPassword = newNormaloPassword; + }; + }; + }; + + testScript = '' + with subtest("Users are not created with systemd-sysusers"): + machine.fail("systemctl status systemd-sysusers.service") + machine.fail("ls /etc/sysusers.d") + + with subtest("Correct mode on the password files"): + assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n" + assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n" + + with subtest("root user has correct password"): + print(machine.succeed("getent passwd root")) + assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct" + + with subtest("normalo user is created"): + print(machine.succeed("getent passwd normalo")) + assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n" + assert "${normaloPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct" + + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + + with subtest("new-normalo user is created after switching to new generation"): + print(machine.succeed("getent passwd new-normalo")) + print(machine.succeed("getent shadow new-normalo")) + assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n" + ''; +} diff --git a/nixos/tests/systemd-sysusers-mutable.nix b/nixos/tests/systemd-sysusers-mutable.nix new file mode 100644 index 000000000000..e69cfe23a59a --- /dev/null +++ b/nixos/tests/systemd-sysusers-mutable.nix @@ -0,0 +1,71 @@ +{ lib, ... }: + +let + rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6"; + normaloPassword = "hello"; + newNormaloPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6"; +in + +{ + + name = "activation-sysusers-mutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, ... }: { + systemd.sysusers.enable = true; + users.mutableUsers = true; + + # Prerequisites + system.etc.overlay.enable = true; + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + # Override the empty root password set by the test instrumentation + users.users.root.hashedPasswordFile = lib.mkForce null; + users.users.root.initialHashedPassword = rootPassword; + users.users.normalo = { + isNormalUser = true; + initialPassword = normaloPassword; + }; + + specialisation.new-generation.configuration = { + users.users.new-normalo = { + isNormalUser = true; + initialHashedPassword = newNormaloPassword; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("systemd-sysusers.service") + + with subtest("systemd-sysusers.service contains the credentials"): + sysusers_service = machine.succeed("systemctl cat systemd-sysusers.service") + print(sysusers_service) + assert "SetCredential=passwd.plaintext-password.normalo:${normaloPassword}" in sysusers_service + + with subtest("Correct mode on the password files"): + assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n" + assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n" + + with subtest("root user has correct password"): + print(machine.succeed("getent passwd root")) + assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct" + + with subtest("normalo user is created"): + print(machine.succeed("getent passwd normalo")) + assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n" + + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + + with subtest("new-normalo user is created after switching to new generation"): + print(machine.succeed("getent passwd new-normalo")) + assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n" + assert "${newNormaloPassword}" in machine.succeed("getent shadow new-normalo"), "new-normalo user password is not correct" + ''; +} -- cgit 1.4.1 From d4a8fe24c235f63a68b9aeaebeea8b00d6b9f9a7 Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 28 Nov 2023 01:32:24 +0100 Subject: nixos/systemd-sysusers: add manual section --- nixos/doc/manual/configuration/user-mgmt.chapter.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'nixos') diff --git a/nixos/doc/manual/configuration/user-mgmt.chapter.md b/nixos/doc/manual/configuration/user-mgmt.chapter.md index b35b38f6e964..71d61ce4c641 100644 --- a/nixos/doc/manual/configuration/user-mgmt.chapter.md +++ b/nixos/doc/manual/configuration/user-mgmt.chapter.md @@ -89,3 +89,18 @@ A user can be deleted using `userdel`: The flag `-r` deletes the user's home directory. Accounts can be modified using `usermod`. Unix groups can be managed using `groupadd`, `groupmod` and `groupdel`. + +## Create users and groups with `systemd-sysusers` {#sec-systemd-sysusers} + +::: {.note} +This is experimental. +::: + +Instead of using a custom perl script to create users and groups, you can use +systemd-sysusers: + +```nix +systemd.sysusers.enable = true; +``` + +The primary benefit of this is to remove a dependency on perl. -- cgit 1.4.1 From 094f6cca9ab9dc1aa886be936b9e57f58a9457ab Mon Sep 17 00:00:00 2001 From: nikstur Date: Mon, 27 Nov 2023 20:20:31 +0100 Subject: nixos/systemd-sysusers: add release-note --- nixos/doc/manual/release-notes/rl-2405.section.md | 3 +++ 1 file changed, 3 insertions(+) (limited to 'nixos') diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 9e8ef49783ca..abc7c18f20fc 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -18,6 +18,9 @@ In addition to numerous new and upgraded packages, this release has the followin - Julia environments can now be built with arbitrary packages from the ecosystem using the `.withPackages` function. For example: `julia.withPackages ["Plots"]`. +- A new option `systemd.sysusers.enable` was added. If enabled, users and + groups are created with systemd-sysusers instead of with a custom perl script. + ## New Services {#sec-release-24.05-new-services} -- cgit 1.4.1 From 60f529fc8277f1e49082a8c240cdfba6d5a5c794 Mon Sep 17 00:00:00 2001 From: nikstur Date: Fri, 13 Oct 2023 00:02:48 +0200 Subject: nixos/etc: optionally mount etc as an overlay --- nixos/modules/system/etc/build-composefs-dump.py | 209 +++++++++++++++++++++ .../system/etc/check-build-composefs-dump.sh | 8 + nixos/modules/system/etc/etc-activation.nix | 98 +++++++++- nixos/modules/system/etc/etc.nix | 116 +++++++++++- nixos/tests/activation/etc-overlay-immutable.nix | 30 +++ nixos/tests/activation/etc-overlay-mutable.nix | 30 +++ nixos/tests/all-tests.nix | 2 + 7 files changed, 481 insertions(+), 12 deletions(-) create mode 100644 nixos/modules/system/etc/build-composefs-dump.py create mode 100755 nixos/modules/system/etc/check-build-composefs-dump.sh create mode 100644 nixos/tests/activation/etc-overlay-immutable.nix create mode 100644 nixos/tests/activation/etc-overlay-mutable.nix (limited to 'nixos') diff --git a/nixos/modules/system/etc/build-composefs-dump.py b/nixos/modules/system/etc/build-composefs-dump.py new file mode 100644 index 000000000000..923d40008b63 --- /dev/null +++ b/nixos/modules/system/etc/build-composefs-dump.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 + +"""Build a composefs dump from a Json config + +See the man page of composefs-dump for details about the format: +https://github.com/containers/composefs/blob/main/man/composefs-dump.md + +Ensure to check the file with the check script when you make changes to it: + +./check-build-composefs-dump.sh ./build-composefs_dump.py +""" + +import glob +import json +import os +import sys +from enum import Enum +from pathlib import Path +from typing import Any + +Attrs = dict[str, Any] + + +class FileType(Enum): + """The filetype as defined by the `st_mode` stat field in octal + + You can check the st_mode stat field of a path in Python with + `oct(os.stat("/path/").st_mode)` + """ + + directory = "4" + file = "10" + symlink = "12" + + +class ComposefsPath: + path: str + size: int + filetype: FileType + mode: str + uid: str + gid: str + payload: str + rdev: str = "0" + nlink: int = 1 + mtime: str = "1.0" + content: str = "-" + digest: str = "-" + + def __init__( + self, + attrs: Attrs, + size: int, + filetype: FileType, + mode: str, + payload: str, + path: str | None = None, + ): + if path is None: + path = attrs["target"] + self.path = "/" + path + self.size = size + self.filetype = filetype + self.mode = mode + self.uid = attrs["uid"] + self.gid = attrs["gid"] + self.payload = payload + + def write_line(self) -> str: + line_list = [ + str(self.path), + str(self.size), + f"{self.filetype.value}{self.mode}", + str(self.nlink), + str(self.uid), + str(self.gid), + str(self.rdev), + str(self.mtime), + str(self.payload), + str(self.content), + str(self.digest), + ] + return " ".join(line_list) + + +def eprint(*args, **kwargs) -> None: + print(args, **kwargs, file=sys.stderr) + + +def leading_directories(path: str) -> list[str]: + """Return the leading directories of path + + Given the path "alsa/conf.d/50-pipewire.conf", for example, this function + returns `[ "alsa", "alsa/conf.d" ]`. + """ + parents = list(Path(path).parents) + parents.reverse() + # remove the implicit `.` from the start of a relative path or `/` from an + # absolute path + del parents[0] + return [str(i) for i in parents] + + +def add_leading_directories( + target: str, attrs: Attrs, paths: dict[str, ComposefsPath] +) -> None: + """Add the leading directories of a target path to the composefs paths + + mkcomposefs expects that all leading directories are explicitly listed in + the dump file. Given the path "alsa/conf.d/50-pipewire.conf", for example, + this function adds "alsa" and "alsa/conf.d" to the composefs paths. + """ + path_components = leading_directories(target) + for component in path_components: + composefs_path = ComposefsPath( + attrs, + path=component, + size=4096, + filetype=FileType.directory, + mode="0755", + payload="-", + ) + paths[component] = composefs_path + + +def main() -> None: + """Build a composefs dump from a Json config + + This config describes the files that the final composefs image is supposed + to contain. + """ + config_file = sys.argv[1] + if not config_file: + eprint("No config file was supplied.") + sys.exit(1) + + with open(config_file, "rb") as f: + config = json.load(f) + + if not config: + eprint("Config is empty.") + sys.exit(1) + + eprint("Building composefs dump...") + + paths: dict[str, ComposefsPath] = {} + for attrs in config: + target = attrs["target"] + source = attrs["source"] + mode = attrs["mode"] + + if "*" in source: # Path with globbing + glob_sources = glob.glob(source) + for glob_source in glob_sources: + basename = os.path.basename(glob_source) + glob_target = f"{target}/{basename}" + + composefs_path = ComposefsPath( + attrs, + path=glob_target, + size=100, + filetype=FileType.symlink, + mode="0777", + payload=glob_source, + ) + + paths[glob_target] = composefs_path + add_leading_directories(glob_target, attrs, paths) + else: # Without globbing + if mode == "symlink": + composefs_path = ComposefsPath( + attrs, + # A high approximation of the size of a symlink + size=100, + filetype=FileType.symlink, + mode="0777", + payload=source, + ) + else: + if os.path.isdir(source): + composefs_path = ComposefsPath( + attrs, + size=4096, + filetype=FileType.directory, + mode=mode, + payload=source, + ) + else: + composefs_path = ComposefsPath( + attrs, + size=os.stat(source).st_size, + filetype=FileType.file, + mode=mode, + payload=target, + ) + paths[target] = composefs_path + add_leading_directories(target, attrs, paths) + + composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory + for key in sorted(paths): + composefs_path = paths[key] + eprint(composefs_path.path) + composefs_dump.append(composefs_path.write_line()) + + print("\n".join(composefs_dump)) + + +if __name__ == "__main__": + main() diff --git a/nixos/modules/system/etc/check-build-composefs-dump.sh b/nixos/modules/system/etc/check-build-composefs-dump.sh new file mode 100755 index 000000000000..da61651d1a5d --- /dev/null +++ b/nixos/modules/system/etc/check-build-composefs-dump.sh @@ -0,0 +1,8 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash -p black ruff mypy + +file=$1 + +black --check --diff $file +ruff --line-length 88 $file +mypy --strict $file diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix index 780104950186..f47fd771c659 100644 --- a/nixos/modules/system/etc/etc-activation.nix +++ b/nixos/modules/system/etc/etc-activation.nix @@ -1,12 +1,96 @@ { config, lib, ... }: -let - inherit (lib) stringAfter; -in { + +{ imports = [ ./etc.nix ]; - config = { - system.activationScripts.etc = - stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands; - }; + config = lib.mkMerge [ + + { + system.activationScripts.etc = + lib.stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands; + } + + (lib.mkIf config.system.etc.overlay.enable { + + assertions = [ + { + assertion = config.boot.initrd.systemd.enable; + message = "`system.etc.overlay.enable` requires `boot.initrd.systemd.enable`"; + } + { + assertion = (!config.system.etc.overlay.mutable) -> config.systemd.sysusers.enable; + message = "`system.etc.overlay.mutable = false` requires `systemd.sysusers.enable`"; + } + { + assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.6"; + message = "`system.etc.overlay.enable requires a newer kernel, at least version 6.6"; + } + { + assertion = config.systemd.sysusers.enable -> (config.users.mutableUsers == config.system.etc.overlay.mutable); + message = '' + When using systemd-sysusers and mounting `/etc` via an overlay, users + can only be mutable when `/etc` is mutable and vice versa. + ''; + } + ]; + + boot.initrd.availableKernelModules = [ "loop" "erofs" "overlay" ]; + + boot.initrd.systemd = { + mounts = [ + { + where = "/run/etc-metadata"; + what = "/sysroot${config.system.build.etcMetadataImage}"; + type = "erofs"; + options = "loop"; + unitConfig.RequiresMountsFor = [ + "/sysroot/nix/store" + ]; + } + { + where = "/sysroot/etc"; + what = "overlay"; + type = "overlay"; + options = lib.concatStringsSep "," ([ + "relatime" + "redirect_dir=on" + "metacopy=on" + "lowerdir=/run/etc-metadata::/sysroot${config.system.build.etcBasedir}" + ] ++ lib.optionals config.system.etc.overlay.mutable [ + "rw" + "upperdir=/sysroot/.rw-etc/upper" + "workdir=/sysroot/.rw-etc/work" + ] ++ lib.optionals (!config.system.etc.overlay.mutable) [ + "ro" + ]); + wantedBy = [ "initrd-fs.target" ]; + before = [ "initrd-fs.target" ]; + requires = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ]; + after = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ]; + unitConfig.RequiresMountsFor = [ + "/sysroot/nix/store" + "/run/etc-metadata" + ]; + } + ]; + services = lib.mkIf config.system.etc.overlay.mutable { + rw-etc = { + unitConfig = { + DefaultDependencies = false; + RequiresMountsFor = "/sysroot"; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = '' + /bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work + ''; + }; + }; + }; + }; + + }) + + ]; } diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix index ea61e7384e60..baf37ba6def3 100644 --- a/nixos/modules/system/etc/etc.nix +++ b/nixos/modules/system/etc/etc.nix @@ -62,6 +62,16 @@ let ]) etc'} ''; + etcHardlinks = filter (f: f.mode != "symlink") etc'; + + build-composefs-dump = pkgs.runCommand "build-composefs-dump.py" + { + buildInputs = [ pkgs.python3 ]; + } '' + install ${./build-composefs-dump.py} $out + patchShebangs --host $out + ''; + in { @@ -72,6 +82,30 @@ in options = { + system.etc.overlay = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Mount `/etc` as an overlayfs instead of generating it via a perl script. + + Note: This is currently experimental. Only enable this option if you're + confident that you can recover your system if it breaks. + ''; + }; + + mutable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only). + + If this is false, only the immutable lowerdir is mounted. If it is + true, a writable upperdir is mounted on top. + ''; + }; + }; + environment.etc = mkOption { default = {}; example = literalExpression '' @@ -190,12 +224,84 @@ in config = { system.build.etc = etc; - system.build.etcActivationCommands = - '' - # Set up the statically computed bits of /etc. - echo "setting up /etc..." - ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc + system.build.etcActivationCommands = let + etcOverlayOptions = lib.concatStringsSep "," ([ + "relatime" + "redirect_dir=on" + "metacopy=on" + ] ++ lib.optionals config.system.etc.overlay.mutable [ + "upperdir=/.rw-etc/upper" + "workdir=/.rw-etc/work" + ]); + in if config.system.etc.overlay.enable then '' + # This script atomically remounts /etc when switching configuration. On a (re-)boot + # this should not run because /etc is mounted via a systemd mount unit + # instead. To a large extent this mimics what composefs does. Because + # it's relatively simple, however, we avoid the composefs dependency. + if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]]; then + echo "remounting /etc..." + + tmpMetadataMount=$(mktemp --directory) + mount --type erofs ${config.system.build.etcMetadataImage} $tmpMetadataMount + + # Mount the new /etc overlay to a temporary private mount. + # This needs the indirection via a private bind mount because you + # cannot move shared mounts. + tmpEtcMount=$(mktemp --directory) + mount --bind --make-private $tmpEtcMount $tmpEtcMount + mount --type overlay overlay \ + --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \ + $tmpEtcMount + + # Move the new temporary /etc mount underneath the current /etc mount. + # + # This should eventually use util-linux to perform this move beneath, + # however, this functionality is not yet in util-linux. See this + # tracking issue: https://github.com/util-linux/util-linux/issues/2604 + ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc + + # Unmount the top /etc mount to atomically reveal the new mount. + umount /etc + + fi + '' else '' + # Set up the statically computed bits of /etc. + echo "setting up /etc..." + ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc + ''; + + system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } '' + set -euo pipefail + + makeEtcEntry() { + src="$1" + target="$2" + + mkdir -p "$out/$(dirname "$target")" + cp "$src" "$out/$target" + } + + mkdir -p "$out" + ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [ + "makeEtcEntry" + # Force local source paths to be added to the store + "${etcEntry.source}" + etcEntry.target + ]) etcHardlinks} + ''; + + system.build.etcMetadataImage = + let + etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc'); + etcDump = pkgs.runCommand "etc-dump" { } "${build-composefs-dump} ${etcJson} > $out"; + in + pkgs.runCommand "etc-metadata.erofs" { + nativeBuildInputs = [ pkgs.composefs pkgs.erofs-utils ]; + } '' + mkcomposefs --from-file ${etcDump} $out + fsck.erofs $out ''; + }; } diff --git a/nixos/tests/activation/etc-overlay-immutable.nix b/nixos/tests/activation/etc-overlay-immutable.nix new file mode 100644 index 000000000000..70c3623b929c --- /dev/null +++ b/nixos/tests/activation/etc-overlay-immutable.nix @@ -0,0 +1,30 @@ +{ lib, ... }: { + + name = "activation-etc-overlay-immutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, ... }: { + system.etc.overlay.enable = true; + system.etc.overlay.mutable = false; + + # Prerequisites + systemd.sysusers.enable = true; + users.mutableUsers = false; + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + specialisation.new-generation.configuration = { + environment.etc."newgen".text = "newgen"; + }; + }; + + testScript = '' + machine.succeed("findmnt --kernel --type overlay /etc") + machine.fail("stat /etc/newgen") + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + assert machine.succeed("cat /etc/newgen") == "newgen" + ''; +} diff --git a/nixos/tests/activation/etc-overlay-mutable.nix b/nixos/tests/activation/etc-overlay-mutable.nix new file mode 100644 index 000000000000..cfe7604fceb8 --- /dev/null +++ b/nixos/tests/activation/etc-overlay-mutable.nix @@ -0,0 +1,30 @@ +{ lib, ... }: { + + name = "activation-etc-overlay-mutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, ... }: { + system.etc.overlay.enable = true; + system.etc.overlay.mutable = true; + + # Prerequisites + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + specialisation.new-generation.configuration = { + environment.etc."newgen".text = "newgen"; + }; + }; + + testScript = '' + machine.succeed("findmnt --kernel --type overlay /etc") + machine.fail("stat /etc/newgen") + machine.succeed("echo -n 'mutable' > /etc/mutable") + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + assert machine.succeed("cat /etc/newgen") == "newgen" + assert machine.succeed("cat /etc/mutable") == "mutable" + ''; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5abb85c224bb..ab2cf8c4e2f5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -285,6 +285,8 @@ in { activation = pkgs.callPackage ../modules/system/activation/test.nix { }; activation-var = runTest ./activation/var.nix; activation-nix-channel = runTest ./activation/nix-channel.nix; + activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix; + activation-etc-overlay-immutable = runTest ./activation/etc-overlay-immutable.nix; etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {}; etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {}; etebase-server = handleTest ./etebase-server.nix {}; -- cgit 1.4.1 From dbd8cb9d460aefddce0afd09ffdce5271a6f36bd Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 28 Nov 2023 01:32:53 +0100 Subject: nixos/etc: add manual section --- .../doc/manual/development/etc-overlay.section.md | 36 ++++++++++++++++++++++ .../what-happens-during-a-system-switch.chapter.md | 1 + 2 files changed, 37 insertions(+) create mode 100644 nixos/doc/manual/development/etc-overlay.section.md (limited to 'nixos') diff --git a/nixos/doc/manual/development/etc-overlay.section.md b/nixos/doc/manual/development/etc-overlay.section.md new file mode 100644 index 000000000000..e6f6d8d4ca1e --- /dev/null +++ b/nixos/doc/manual/development/etc-overlay.section.md @@ -0,0 +1,36 @@ +# `/etc` via overlay filesystem {#sec-etc-overlay} + +::: {.note} +This is experimental and requires a kernel version >= 6.6 because it uses +new overlay features and relies on the new mount API. +::: + +Instead of using a custom perl script to activate `/etc`, you activate it via an +overlay filesystem: + +```nix +system.etc.overlay.enable = true; +``` + +Using an overlay has two benefits: + +1. it removes a dependency on perl +2. it makes activation faster (up to a few seconds) + +By default, the `/etc` overlay is mounted writable (i.e. there is a writable +upper layer). However, you can also mount `/etc` immutably (i.e. read-only) by +setting: + +```nix +system.etc.overlay.mutable = false; +``` + +The overlay is atomically replaced during system switch. However, files that +have been modified will NOT be overwritten. This is the biggest change compared +to the perl-based system. + +If you manually make changes to `/etc` on your system and then switch to a new +configuration where `system.etc.overlay.mutable = false;`, you will not be able +to see the previously made changes in `/etc` anymore. However the changes are +not completely gone, they are still in the upperdir of the previous overlay in +`/.rw-etc/upper`. diff --git a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md index 5d17a9c98514..28c06f999dac 100644 --- a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md +++ b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md @@ -56,4 +56,5 @@ explained in the next sections. unit-handling.section.md activation-script.section.md non-switchable-systems.section.md +etc-overlay.section.md ``` -- cgit 1.4.1 From 7763ea212ab5359e19e447ac2cf24afb5c4b9a04 Mon Sep 17 00:00:00 2001 From: nikstur Date: Mon, 27 Nov 2023 20:00:56 +0100 Subject: nixos/etc: add release-note about overlay --- nixos/doc/manual/release-notes/rl-2405.section.md | 3 +++ 1 file changed, 3 insertions(+) (limited to 'nixos') diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index abc7c18f20fc..1d557d14c611 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -21,6 +21,9 @@ In addition to numerous new and upgraded packages, this release has the followin - A new option `systemd.sysusers.enable` was added. If enabled, users and groups are created with systemd-sysusers instead of with a custom perl script. +- A new option `system.etc.overlay.enable` was added. If enabled, `/etc` is + mounted via an overlayfs instead of being created by a custom perl script. + ## New Services {#sec-release-24.05-new-services} -- cgit 1.4.1 From 8710a27bc6dc56a77808757b49b9bc5ad165d7ce Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 17 Oct 2023 16:40:26 +0200 Subject: nixos/profiles/perlless: init --- nixos/modules/profiles/perlless.nix | 31 +++++++++++++++++++++++++++++++ nixos/tests/activation/perlless.nix | 24 ++++++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + 3 files changed, 56 insertions(+) create mode 100644 nixos/modules/profiles/perlless.nix create mode 100644 nixos/tests/activation/perlless.nix (limited to 'nixos') diff --git a/nixos/modules/profiles/perlless.nix b/nixos/modules/profiles/perlless.nix new file mode 100644 index 000000000000..90abd14f077e --- /dev/null +++ b/nixos/modules/profiles/perlless.nix @@ -0,0 +1,31 @@ +# WARNING: If you enable this profile, you will NOT be able to switch to a new +# configuration and thus you will not be able to rebuild your system with +# nixos-rebuild! + +{ lib, ... }: + +{ + + # Disable switching to a new configuration. This is not a necessary + # limitation of a perlless system but just a current one. In the future, + # perlless switching might be possible. + system.switch.enable = lib.mkDefault false; + + # Remove perl from activation + boot.initrd.systemd.enable = lib.mkDefault true; + system.etc.overlay.enable = lib.mkDefault true; + systemd.sysusers.enable = lib.mkDefault true; + + # Random perl remnants + system.disableInstallerTools = lib.mkDefault true; + programs.less.lessopen = lib.mkDefault null; + programs.command-not-found.enable = lib.mkDefault false; + boot.enableContainers = lib.mkDefault false; + environment.defaultPackages = lib.mkDefault [ ]; + documentation.info.enable = lib.mkDefault false; + + # Check that the system does not contain a Nix store path that contains the + # string "perl". + system.forbiddenDependenciesRegex = "perl"; + +} diff --git a/nixos/tests/activation/perlless.nix b/nixos/tests/activation/perlless.nix new file mode 100644 index 000000000000..4d784b4542f4 --- /dev/null +++ b/nixos/tests/activation/perlless.nix @@ -0,0 +1,24 @@ +{ lib, ... }: + +{ + + name = "activation-perlless"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, modulesPath, ... }: { + imports = [ "${modulesPath}/profiles/perlless.nix" ]; + + boot.kernelPackages = pkgs.linuxPackages_latest; + + virtualisation.mountHostNixStore = false; + virtualisation.useNixStoreImage = true; + }; + + testScript = '' + perl_store_paths = machine.succeed("ls /nix/store | grep perl || true") + print(perl_store_paths) + assert len(perl_store_paths) == 0 + ''; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ab2cf8c4e2f5..9615036c1f85 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -287,6 +287,7 @@ in { activation-nix-channel = runTest ./activation/nix-channel.nix; activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix; activation-etc-overlay-immutable = runTest ./activation/etc-overlay-immutable.nix; + activation-perlless = runTest ./activation/perlless.nix; etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {}; etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {}; etebase-server = handleTest ./etebase-server.nix {}; -- cgit 1.4.1 From 3df9c5ef3e7a50aa43916bb69a6a42fd3354ecf7 Mon Sep 17 00:00:00 2001 From: nikstur Date: Tue, 28 Nov 2023 01:52:39 +0100 Subject: nixos/profiles/perlless: add manual section --- nixos/doc/manual/configuration/profiles.chapter.md | 1 + nixos/doc/manual/configuration/profiles/perlless.section.md | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 nixos/doc/manual/configuration/profiles/perlless.section.md (limited to 'nixos') diff --git a/nixos/doc/manual/configuration/profiles.chapter.md b/nixos/doc/manual/configuration/profiles.chapter.md index 9f1f48f742ac..9f6c11b0d59d 100644 --- a/nixos/doc/manual/configuration/profiles.chapter.md +++ b/nixos/doc/manual/configuration/profiles.chapter.md @@ -29,6 +29,7 @@ profiles/graphical.section.md profiles/hardened.section.md profiles/headless.section.md profiles/installation-device.section.md +profiles/perlless.section.md profiles/minimal.section.md profiles/qemu-guest.section.md ``` diff --git a/nixos/doc/manual/configuration/profiles/perlless.section.md b/nixos/doc/manual/configuration/profiles/perlless.section.md new file mode 100644 index 000000000000..bf055971cfc4 --- /dev/null +++ b/nixos/doc/manual/configuration/profiles/perlless.section.md @@ -0,0 +1,11 @@ +# Perlless {#sec-perlless} + +::: {.warning} +If you enable this profile, you will NOT be able to switch to a new +configuration and thus you will not be able to rebuild your system with +nixos-rebuild! +::: + +Render your system completely perlless (i.e. without the perl interpreter). This +includes a mechanism so that your build fails if it contains a Nix store path +that references the string "perl". -- cgit 1.4.1 From 1f65b34e999760470208af94cd208ea3f9da0c47 Mon Sep 17 00:00:00 2001 From: nikstur Date: Mon, 27 Nov 2023 19:52:31 +0100 Subject: nixos/profiles/perlless: add release-note --- nixos/doc/manual/release-notes/rl-2405.section.md | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'nixos') diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 1d557d14c611..0b313326fac8 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -24,6 +24,16 @@ In addition to numerous new and upgraded packages, this release has the followin - A new option `system.etc.overlay.enable` was added. If enabled, `/etc` is mounted via an overlayfs instead of being created by a custom perl script. +- It is now possible to have a completely perlless system (i.e. a system + without perl). Previously, the NixOS activation depended on two perl scripts + which can now be replaced via an opt-in mechanism. To make your system + perlless, you can use the new perlless profile: + ``` + { modulesPath, ... }: { + imports = [ "${modulesPath}/profiles/perlless.nix" ]; + } + ``` + ## New Services {#sec-release-24.05-new-services} -- cgit 1.4.1