diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index dddb2b9241ad7..66407d0321777 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -12,6 +12,11 @@ In addition to numerous new and upgraded packages, this release has the followin Take the time to review [the release notes](https://gitlab.com/cryptsetup/cryptsetup/-/raw/v2.7.0/docs/v2.7.0-ReleaseNotes). One of the highlight is that it is now possible to use hardware OPAL-based encryption of your disk with `cryptsetup`, it has a lot of caveats, see the above notes for the full details. +- A new option, `users.solitaryByDefault`, controls whether normal users default to a primary group named the same as the user, and with group id same as user id, or use the shared group "users" with group id 100. + System users always default to a group named after them, with GID = UID. + Per-user groups gives better control over permissions/file sharing and aligns NixOS with other distros like Arch Linux/Debian/Fedora/Ubuntu. + The option defaults to `system.stateVersion >= 24.11`, which means it's opt-in (to per-user groups) up to and including NixOS 24.05, but after that the option must be set to `false` to keep the current/old behaviour. + - `screen`'s module has been cleaned, and will now require you to set `programs.screen.enable` in order to populate `screenrc` and add the program to the environment. - `linuxPackages_testing_bcachefs` is now fully deprecated by `linuxPackages_latest`, and is therefore no longer available. diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index f9750b7263cac..3cb7bc2c1b403 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -3,6 +3,7 @@ with lib; let + nixosConfig = config; ids = config.ids; cfg = config.users; @@ -114,7 +115,11 @@ let group = mkOption { type = types.str; apply = x: assert (builtins.stringLength x < 32 || abort "Group name '${x}' is longer than 31 characters which is not allowed!"); x; - default = ""; + default = name; + defaultText = lib.literalMD '' + For normal users: if users.solitaryByDefault then else "users" + For system users: + ''; description = lib.mdDoc "The user's primary group."; }; @@ -362,13 +367,15 @@ let shell = mkIf config.useDefaultShell (mkDefault cfg.defaultUserShell); } (mkIf config.isNormalUser { - group = mkDefault "users"; createHome = mkDefault true; home = mkDefault "/home/${config.name}"; homeMode = mkDefault "700"; useDefaultShell = mkDefault true; isSystemUser = mkDefault false; }) + (mkIf (config.isNormalUser && !nixosConfig.users.solitaryByDefault) { + group = mkDefault "users"; + }) # If !mutableUsers, setting ‘initialPassword’ is equivalent to # setting ‘password’ (and similarly for hashed passwords). (mkIf (!cfg.mutableUsers && config.initialPassword != null) { @@ -584,6 +591,16 @@ in { ''; }; + users.solitaryByDefault = mkOption { + type = types.bool; + default = lib.versionAtLeast nixosConfig.system.stateVersion "24.11"; + description = lib.mdDoc '' + Whether normal users should default to a primary group named after + them. When disabled, normal users default to the "users" group. System + users always default to a group named after them. + ''; + }; + # systemd initrd boot.initrd.systemd.users = mkOption { description = '' @@ -659,31 +676,36 @@ in { }; }; - users.groups = { - root.gid = ids.gids.root; - wheel.gid = ids.gids.wheel; - disk.gid = ids.gids.disk; - kmem.gid = ids.gids.kmem; - tty.gid = ids.gids.tty; - floppy.gid = ids.gids.floppy; - uucp.gid = ids.gids.uucp; - lp.gid = ids.gids.lp; - cdrom.gid = ids.gids.cdrom; - tape.gid = ids.gids.tape; - audio.gid = ids.gids.audio; - video.gid = ids.gids.video; - dialout.gid = ids.gids.dialout; - nogroup.gid = ids.gids.nogroup; - users.gid = ids.gids.users; - nixbld.gid = ids.gids.nixbld; - utmp.gid = ids.gids.utmp; - adm.gid = ids.gids.adm; - input.gid = ids.gids.input; - kvm.gid = ids.gids.kvm; - render.gid = ids.gids.render; - sgx.gid = ids.gids.sgx; - shadow.gid = ids.gids.shadow; - }; + users.groups = mkMerge [ + { + root.gid = ids.gids.root; + wheel.gid = ids.gids.wheel; + disk.gid = ids.gids.disk; + kmem.gid = ids.gids.kmem; + tty.gid = ids.gids.tty; + floppy.gid = ids.gids.floppy; + uucp.gid = ids.gids.uucp; + lp.gid = ids.gids.lp; + cdrom.gid = ids.gids.cdrom; + tape.gid = ids.gids.tape; + audio.gid = ids.gids.audio; + video.gid = ids.gids.video; + dialout.gid = ids.gids.dialout; + nogroup.gid = ids.gids.nogroup; + users.gid = ids.gids.users; + nixbld.gid = ids.gids.nixbld; + utmp.gid = ids.gids.utmp; + adm.gid = ids.gids.adm; + input.gid = ids.gids.input; + kvm.gid = ids.gids.kvm; + render.gid = ids.gids.render; + sgx.gid = ids.gids.sgx; + shadow.gid = ids.gids.shadow; + } + # Create per-user primary groups + (lib.optionalAttrs nixosConfig.users.solitaryByDefault + (lib.mapAttrs' (_: user: { name = user.group; value = { gid = lib.mkDefault user.uid; }; }) nixosConfig.users.users)) + ]; system.activationScripts.users = if !config.systemd.sysusers.enable then { supportsDryActivation = true; diff --git a/nixos/modules/profiles/hardened.nix b/nixos/modules/profiles/hardened.nix index b85a2ac7e69d2..e844cfbd5a203 100644 --- a/nixos/modules/profiles/hardened.nix +++ b/nixos/modules/profiles/hardened.nix @@ -10,6 +10,9 @@ with lib; +let + normalUsers = lib.filterAttrs (n: v: v.isNormalUser) config.users.users; +in { meta = { maintainers = [ maintainers.joachifm maintainers.emily ]; @@ -17,7 +20,11 @@ with lib; boot.kernelPackages = mkDefault pkgs.linuxPackages_hardened; - nix.settings.allowed-users = mkDefault [ "@users" ]; + nix.settings.allowed-users = mkDefault ( + if config.users.solitaryByDefault then + lib.mapAttrsToList (n: v: v.name) normalUsers + else [ "@users" ] + ); environment.memoryAllocator.provider = mkDefault "scudo"; environment.variables.SCUDO_OPTIONS = mkDefault "ZeroContents=1"; diff --git a/nixos/modules/profiles/macos-builder.nix b/nixos/modules/profiles/macos-builder.nix index 786e26cf98f7f..917be5f207524 100644 --- a/nixos/modules/profiles/macos-builder.nix +++ b/nixos/modules/profiles/macos-builder.nix @@ -23,6 +23,7 @@ in ]; # swraid's default depends on stateVersion config.boot.swraid.enable = false; + config.users.solitaryByDefault = true; options.boot.isContainer = lib.mkOption { default = false; internal = true; }; } ]; diff --git a/nixos/tests/user-group-membership.nix b/nixos/tests/user-group-membership.nix new file mode 100644 index 0000000000000..3d6d11b2572f9 --- /dev/null +++ b/nixos/tests/user-group-membership.nix @@ -0,0 +1,39 @@ +import ./make-test-python.nix ({ lib, ... }: { + name = "user-group-membership"; + meta = with lib.maintainers; { maintainers = [ bjornfor ]; }; + + nodes = + let + commonConfig = { + users.users.alice = { + isNormalUser = true; + }; + users.users.bob = { + isNormalUser = true; + group = "users"; + }; + }; + in + { + shared = { + imports = [ commonConfig ]; + users.solitaryByDefault = false; + }; + solitary = { + imports = [ commonConfig ]; + users.solitaryByDefault = true; + }; + }; + + testScript = '' + start_all() + + with subtest("solitaryByDefault = false"): + shared.succeed('[ "$(stat -c %G /home/alice)" == "users" ]') + shared.succeed('[ "$(stat -c %G /home/bob)" == "users" ]') + + with subtest("solitaryByDefault = true"): + solitary.succeed('[ "$(stat -c %G /home/alice)" == "alice" ]') + solitary.succeed('[ "$(stat -c %G /home/bob)" == "users" ]') + ''; +})