linger-users: Fix strict shell checks, fix for mutable users (#363209)

This commit is contained in:
Grimmauld
2025-11-10 09:37:32 +00:00
committed by GitHub
6 changed files with 143 additions and 47 deletions

View File

@@ -385,6 +385,8 @@ and [release notes for v18](https://goteleport.com/docs/changelog/#1800-070325).
- In all other cases, you'll need to set this option to `true` yourself. - In all other cases, you'll need to set this option to `true` yourself.
- `boot.isNspawnContainer` being `true` implies [](#opt-boot.isContainer) being `true`. - `boot.isNspawnContainer` being `true` implies [](#opt-boot.isContainer) being `true`.
- `users.users.*.linger` now defaults to `null` rather than `false`, meaning NixOS will not attempt to enable or disable lingering for that user account. In practice, this is unlikely to make a difference for most people, as new users are created without lingering configured, but it means users who use `loginctl` commands to manage lingering imperatively will not have their changes overridden by default. There is a new, related option, `users.manageLingering`, which can be used to prevent NixOS attempting to manage lingering entirely.
- Due to [deprecation of gnome-session X11 support](https://blogs.gnome.org/alatiera/2025/06/08/the-x11-session-removal/), `services.desktopManager.pantheon` now defaults to pantheon-wayland session. The X11 session has been removed, see [this issue](https://github.com/elementary/session-settings/issues/91) for details. - Due to [deprecation of gnome-session X11 support](https://blogs.gnome.org/alatiera/2025/06/08/the-x11-session-removal/), `services.desktopManager.pantheon` now defaults to pantheon-wayland session. The X11 session has been removed, see [this issue](https://github.com/elementary/session-settings/issues/91) for details.
- `bcachefs` file systems will now use the out-of-tree module for supported kernels. The in-tree module has been removed, and users will need to switch to kernels that support the out-of-tree module. - `bcachefs` file systems will now use the out-of-tree module for supported kernels. The in-tree module has been removed, and users will need to switch to kernels that support the out-of-tree module.

View File

@@ -11,6 +11,7 @@ let
any any
attrNames attrNames
attrValues attrValues
boolToString
concatMap concatMap
concatMapStringsSep concatMapStringsSep
concatStrings concatStrings
@@ -43,6 +44,7 @@ let
stringLength stringLength
trace trace
types types
versionOlder
xor xor
; ;
@@ -128,6 +130,10 @@ let
''; '';
userOpts = userOpts =
let
# Pass state version through despite config being overwritten in the inner module
inherit (config.system) stateVersion;
in
{ name, config, ... }: { name, config, ... }:
{ {
@@ -455,16 +461,22 @@ let
}; };
linger = mkOption { linger = mkOption {
type = types.bool; type = types.nullOr types.bool;
default = false; example = true;
default = if versionOlder stateVersion "26.11" then false else null;
defaultText = literalExpression "if lib.versionOlder config.system.stateVersion \"25.11\" then false else null";
description = '' description = ''
Whether to enable lingering for this user. If true, systemd user Whether to enable or disable lingering for this user. Without
units will start at boot, rather than starting at login and stopping lingering, user units will not be started until the user logs in,
at logout. This is the declarative equivalent of running and may be stopped on logout depending on the settings in
`loginctl enable-linger` for this user. `logind.conf`.
If false, user units will not be started until the user logs in, and By default, NixOS will not manage lingering, new users will default
may be stopped on logout depending on the settings in `logind.conf`. to not lingering, and you can change the linger setting using
`loginctl enable-linger` or `loginctl disable-linger`. Setting
this option to `true` or `false` is the declarative equivalent of
running `loginctl enable-linger` or `loginctl disable-linger`
respectively.
''; '';
}; };
}; };
@@ -661,8 +673,6 @@ let
shells = mapAttrsToList (_: u: u.shell) cfg.users; shells = mapAttrsToList (_: u: u.shell) cfg.users;
in in
filter types.shellPackage.check shells; filter types.shellPackage.check shells;
lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger)));
in in
{ {
imports = [ imports = [
@@ -710,6 +720,13 @@ in
''; '';
}; };
users.manageLingering = mkOption {
type = types.bool;
default = true;
description = "Whether to manage whether users linger or not.";
example = false;
};
users.users = mkOption { users.users = mkOption {
default = { }; default = { };
type = with types; attrsOf (submodule userOpts); type = with types; attrsOf (submodule userOpts);
@@ -894,32 +911,52 @@ in
else else
""; # keep around for backwards compatibility ""; # keep around for backwards compatibility
systemd.services.linger-users = lib.mkIf ((length lingeringUsers) > 0) { systemd.services.linger-users = lib.mkIf cfg.manageLingering {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "systemd-logind.service" ]; after = [ "systemd-logind.service" ];
requires = [ "systemd-logind.service" ]; requires = [ "systemd-logind.service" ];
script = script =
let let
lingerDir = "/var/lib/systemd/linger"; lingeringUsers = filterAttrs (n: v: v.linger == true) cfg.users;
lingeringUsersFile = builtins.toFile "lingering-users" ( nonLingeringUsers = filterAttrs (n: v: v.linger == false) cfg.users;
concatStrings (map (s: "${s}\n") (sort (a: b: a < b) lingeringUsers)) lingeringUserNames = mapAttrsToList (n: v: v.name) lingeringUsers;
); # this sorting is important for `comm` to work correctly nonLingeringUserNames = mapAttrsToList (n: v: v.name) nonLingeringUsers;
in in
'' ''
mkdir -vp ${lingerDir} ${lib.strings.toShellVars { inherit lingeringUserNames nonLingeringUserNames; }}
cd ${lingerDir}
for user in $(ls); do user_configured () {
if ! id "$user" >/dev/null; then # Use `id` to check if the user exists rather than checking the
echo "Removing linger for missing user $user" # NixOS configuration, as it may be that the user has been
rm --force -- "$user" # manually configured, which is permitted if users.mutableUsers
# is true (the default).
id "$1" >/dev/null
}
shopt -s dotglob nullglob
for user in *; do
if ! user_configured "$user"; then
# systemd has this user configured to linger despite them not
# existing.
echo "Removing linger for missing user $user" >&2
rm -- "$user"
fi fi
done done
ls | sort | comm -3 -1 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl disable-linger
ls | sort | comm -3 -2 ${lingeringUsersFile} - | xargs -r ${pkgs.systemd}/bin/loginctl enable-linger if (( ''${#nonLingeringUserNames[*]} > 0 )); then
${config.systemd.package}/bin/loginctl disable-linger "''${nonLingeringUserNames[@]}"
fi
if (( ''${#lingeringUserNames[*]} > 0 )); then
${config.systemd.package}/bin/loginctl enable-linger "''${lingeringUserNames[@]}"
fi
''; '';
serviceConfig.Type = "oneshot"; serviceConfig = {
Type = "oneshot";
StateDirectory = "systemd/linger";
WorkingDirectory = "/var/lib/systemd/linger";
};
}; };
# Warn about user accounts with deprecated password hashing schemes # Warn about user accounts with deprecated password hashing schemes
@@ -1163,6 +1200,22 @@ in
users.groups.${user.name} = {}; users.groups.${user.name} = {};
''; '';
} }
{
assertion = user.linger != null -> cfg.manageLingering;
message = ''
users.manageLingering is set to false, but
users.users.${user.name}.linger is configured.
If you want NixOS to manage whether user accounts linger or
not, you must set users.manageLingering to true. This is the
default setting.
If you do not want NixOS to manage whether user accounts linger
or not, you must set users.users.${user.name}.linger to null.
This is the default setting provided system.stateVersion is at
least "25.11".
'';
}
] ]
++ (map ++ (map
(shell: { (shell: {

View File

@@ -32,6 +32,7 @@
boot.kexec.enable = lib.mkDefault false; boot.kexec.enable = lib.mkDefault false;
# Relies on bash scripts # Relies on bash scripts
powerManagement.enable = lib.mkDefault false; powerManagement.enable = lib.mkDefault false;
users.manageLingering = lib.mkDefault false;
# Relies on the gzip command which depends on bash # Relies on the gzip command which depends on bash
services.logrotate.enable = lib.mkDefault false; services.logrotate.enable = lib.mkDefault false;

View File

@@ -1519,6 +1519,7 @@ in
systemd-sysusers-password-option-override-ordering = runTest ./systemd-sysusers-password-option-override-ordering.nix; systemd-sysusers-password-option-override-ordering = runTest ./systemd-sysusers-password-option-override-ordering.nix;
systemd-timesyncd-nscd-dnssec = runTest ./systemd-timesyncd-nscd-dnssec.nix; systemd-timesyncd-nscd-dnssec = runTest ./systemd-timesyncd-nscd-dnssec.nix;
systemd-user-linger = runTest ./systemd-user-linger.nix; systemd-user-linger = runTest ./systemd-user-linger.nix;
systemd-user-linger-purge = runTest ./systemd-user-linger-purge.nix;
systemd-user-tmpfiles-rules = runTest ./systemd-user-tmpfiles-rules.nix; systemd-user-tmpfiles-rules = runTest ./systemd-user-tmpfiles-rules.nix;
systemd-userdbd = runTest ./systemd-userdbd.nix; systemd-userdbd = runTest ./systemd-userdbd.nix;
systemtap = handleTest ./systemtap.nix { }; systemtap = handleTest ./systemtap.nix { };

View File

@@ -0,0 +1,37 @@
# This test checks #418101, where lingering users would not be cleared up if
# the configuration is updated to remove lingering from all users.
rec {
name = "systemd-user-linger-purge";
nodes.machine = {
users.users = {
bob = {
isNormalUser = true;
linger = false;
uid = 1001;
};
};
};
testScript =
let
uidStrings = builtins.mapAttrs (k: v: builtins.toString v.uid) nodes.machine.users.users;
in
''
machine.fail("test -e /var/lib/systemd/linger/bob")
machine.fail("systemctl status user-${uidStrings.bob}.slice")
with subtest("missing users have linger purged"):
machine.succeed("touch /var/lib/systemd/linger/alice")
machine.systemctl("restart linger-users")
machine.succeed("test ! -e /var/lib/systemd/linger/alice")
with subtest("mutable users can linger"):
machine.succeed("useradd alice")
machine.succeed("test ! -e /var/lib/systemd/linger/alice")
machine.succeed("loginctl enable-linger alice")
machine.succeed("test -e /var/lib/systemd/linger/alice")
machine.systemctl("restart linger-users")
machine.succeed("test -e /var/lib/systemd/linger/alice")
'';
}

View File

@@ -1,10 +1,7 @@
{ lib, ... }: rec {
{
name = "systemd-user-linger"; name = "systemd-user-linger";
nodes.machine = nodes.machine = {
{ ... }:
{
users.users = { users.users = {
alice = { alice = {
isNormalUser = true; isNormalUser = true;
@@ -15,23 +12,28 @@
bob = { bob = {
isNormalUser = true; isNormalUser = true;
linger = false; linger = false;
uid = 10001; uid = 1001;
}; };
}; };
}; };
testScript = testScript =
{ ... }: let
uidStrings = builtins.mapAttrs (k: v: builtins.toString v.uid) nodes.machine.users.users;
in
'' ''
machine.wait_for_file("/var/lib/systemd/linger/alice") machine.wait_for_file("/var/lib/systemd/linger/alice")
machine.succeed("systemctl status user-1000.slice") machine.succeed("systemctl status user-${uidStrings.alice}.slice")
machine.fail("test -e /var/lib/systemd/linger/bob") machine.fail("test -e /var/lib/systemd/linger/bob")
machine.fail("systemctl status user-1001.slice") machine.fail("systemctl status user-${uidStrings.bob}.slice")
with subtest("missing users have linger purged"): with subtest("mutable users can linger"):
machine.succeed("touch /var/lib/systemd/linger/missing") machine.succeed("useradd clare")
machine.succeed("test ! -e /var/lib/systemd/linger/clare")
machine.succeed("loginctl enable-linger clare")
machine.succeed("test -e /var/lib/systemd/linger/clare")
machine.systemctl("restart linger-users") machine.systemctl("restart linger-users")
machine.succeed("test ! -e /var/lib/systemd/linger/missing") machine.succeed("test -e /var/lib/systemd/linger/clare")
''; '';
} }