linger-users: Fix strict shell checks, fix for mutable users (#363209)
This commit is contained in:
@@ -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.
|
||||
- `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.
|
||||
|
||||
- `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.
|
||||
|
||||
@@ -11,6 +11,7 @@ let
|
||||
any
|
||||
attrNames
|
||||
attrValues
|
||||
boolToString
|
||||
concatMap
|
||||
concatMapStringsSep
|
||||
concatStrings
|
||||
@@ -43,6 +44,7 @@ let
|
||||
stringLength
|
||||
trace
|
||||
types
|
||||
versionOlder
|
||||
xor
|
||||
;
|
||||
|
||||
@@ -128,6 +130,10 @@ let
|
||||
'';
|
||||
|
||||
userOpts =
|
||||
let
|
||||
# Pass state version through despite config being overwritten in the inner module
|
||||
inherit (config.system) stateVersion;
|
||||
in
|
||||
{ name, config, ... }:
|
||||
{
|
||||
|
||||
@@ -455,16 +461,22 @@ let
|
||||
};
|
||||
|
||||
linger = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
type = types.nullOr types.bool;
|
||||
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 = ''
|
||||
Whether to enable lingering for this user. If true, systemd user
|
||||
units will start at boot, rather than starting at login and stopping
|
||||
at logout. This is the declarative equivalent of running
|
||||
`loginctl enable-linger` for this user.
|
||||
Whether to enable or disable lingering for this user. Without
|
||||
lingering, user units will not be started until the user logs in,
|
||||
and may be stopped on logout depending on the settings in
|
||||
`logind.conf`.
|
||||
|
||||
If false, user units will not be started until the user logs in, and
|
||||
may be stopped on logout depending on the settings in `logind.conf`.
|
||||
By default, NixOS will not manage lingering, new users will default
|
||||
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;
|
||||
in
|
||||
filter types.shellPackage.check shells;
|
||||
|
||||
lingeringUsers = map (u: u.name) (attrValues (flip filterAttrs cfg.users (n: u: u.linger)));
|
||||
in
|
||||
{
|
||||
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 {
|
||||
default = { };
|
||||
type = with types; attrsOf (submodule userOpts);
|
||||
@@ -894,32 +911,52 @@ in
|
||||
else
|
||||
""; # 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" ];
|
||||
after = [ "systemd-logind.service" ];
|
||||
requires = [ "systemd-logind.service" ];
|
||||
|
||||
script =
|
||||
let
|
||||
lingerDir = "/var/lib/systemd/linger";
|
||||
lingeringUsersFile = builtins.toFile "lingering-users" (
|
||||
concatStrings (map (s: "${s}\n") (sort (a: b: a < b) lingeringUsers))
|
||||
); # this sorting is important for `comm` to work correctly
|
||||
lingeringUsers = filterAttrs (n: v: v.linger == true) cfg.users;
|
||||
nonLingeringUsers = filterAttrs (n: v: v.linger == false) cfg.users;
|
||||
lingeringUserNames = mapAttrsToList (n: v: v.name) lingeringUsers;
|
||||
nonLingeringUserNames = mapAttrsToList (n: v: v.name) nonLingeringUsers;
|
||||
in
|
||||
''
|
||||
mkdir -vp ${lingerDir}
|
||||
cd ${lingerDir}
|
||||
for user in $(ls); do
|
||||
if ! id "$user" >/dev/null; then
|
||||
echo "Removing linger for missing user $user"
|
||||
rm --force -- "$user"
|
||||
fi
|
||||
${lib.strings.toShellVars { inherit lingeringUserNames nonLingeringUserNames; }}
|
||||
|
||||
user_configured () {
|
||||
# Use `id` to check if the user exists rather than checking the
|
||||
# NixOS configuration, as it may be that the user has been
|
||||
# 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
|
||||
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
|
||||
@@ -1163,6 +1200,22 @@ in
|
||||
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
|
||||
(shell: {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
boot.kexec.enable = lib.mkDefault false;
|
||||
# Relies on bash scripts
|
||||
powerManagement.enable = lib.mkDefault false;
|
||||
users.manageLingering = lib.mkDefault false;
|
||||
# Relies on the gzip command which depends on bash
|
||||
services.logrotate.enable = lib.mkDefault false;
|
||||
|
||||
|
||||
@@ -1519,6 +1519,7 @@ in
|
||||
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-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-userdbd = runTest ./systemd-userdbd.nix;
|
||||
systemtap = handleTest ./systemtap.nix { };
|
||||
|
||||
37
nixos/tests/systemd-user-linger-purge.nix
Normal file
37
nixos/tests/systemd-user-linger-purge.nix
Normal 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")
|
||||
'';
|
||||
}
|
||||
@@ -1,37 +1,39 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
rec {
|
||||
name = "systemd-user-linger";
|
||||
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
linger = true;
|
||||
uid = 1000;
|
||||
};
|
||||
nodes.machine = {
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
linger = true;
|
||||
uid = 1000;
|
||||
};
|
||||
|
||||
bob = {
|
||||
isNormalUser = true;
|
||||
linger = false;
|
||||
uid = 10001;
|
||||
};
|
||||
bob = {
|
||||
isNormalUser = true;
|
||||
linger = false;
|
||||
uid = 1001;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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.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("systemctl status user-1001.slice")
|
||||
machine.fail("systemctl status user-${uidStrings.bob}.slice")
|
||||
|
||||
with subtest("missing users have linger purged"):
|
||||
machine.succeed("touch /var/lib/systemd/linger/missing")
|
||||
with subtest("mutable users can linger"):
|
||||
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.succeed("test ! -e /var/lib/systemd/linger/missing")
|
||||
machine.succeed("test -e /var/lib/systemd/linger/clare")
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user