account-utils: init at 1.3.0, nixos/account-utils: init (#453557)

This commit is contained in:
Ramses 2026-06-24 12:06:02 +00:00 committed by GitHub
commit 91cd0708d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 318 additions and 8 deletions

View file

@ -387,6 +387,7 @@
./programs/zsh/zsh-syntax-highlighting.nix
./programs/zsh/zsh.nix
./rename.nix
./security/account-utils.nix
./security/acme
./security/agnos.nix
./security/apparmor.nix

View file

@ -0,0 +1,65 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.security.account-utils;
in
{
options.security.account-utils = {
enable = lib.mkEnableOption "the account-utils implementation of Unix user authentication and management";
package = lib.mkPackageOption pkgs "account-utils" { };
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ ];
example = [
"--debug"
"-v"
];
description = ''
List of arguments to pass to the socket activated service executables.
::: {.note}
This is passed to both pwupdd and pwaccessd, which support identical flags.
:::
'';
};
};
config = lib.mkIf cfg.enable {
# use account-utils reimplementation of pam_unix
security.pam = {
pam_unixModulePath = "${cfg.package}/lib/security/pam_unix_ng.so";
enableLegacySettings = false;
};
systemd = {
packages = [ cfg.package ];
sockets.pwaccessd.wantedBy = [ "sockets.target" ];
sockets.pwupdd.wantedBy = lib.optional config.users.mutableUsers "sockets.target"; # immutable users do not need password updating
sockets.newidmapd.wantedBy = [ "sockets.target" ];
services."pwupdd@".environment.PWUPDD_OPTS = lib.escapeShellArgs cfg.extraArgs;
services."pwaccessd".environment.PWACCESSD_OPTS = lib.escapeShellArgs cfg.extraArgs;
};
environment.systemPackages = [ cfg.package ];
security.pam.services = {
pwupd-passwd = { };
pwupd-chsh = { };
pwupd-chfn = { };
};
# covered by account-utils via socket-activated service
security.wrappers = {
# shadow suid binaries are no longer necessary, but disabling the entire shadow module is too intrusive
newuidmap.enable = false;
newgidmap.enable = false;
chsh.enable = false;
passwd.enable = false;
unix_chkpwd.enable = false; # Not necessary when using pam_unix_ng.so
};
};
}

View file

@ -1008,7 +1008,7 @@ let
{
name = "unix";
control = "required";
modulePath = "${package}/lib/security/pam_unix.so";
modulePath = config.security.pam.pam_unixModulePath;
}
# pam_slurm_adopt must be the last module in the account stack.
{
@ -1217,11 +1217,11 @@ let
name = "unix-early";
enable = cfg.unixAuth;
control = "optional";
modulePath = "${package}/lib/security/pam_unix.so";
modulePath = config.security.pam.pam_unixModulePath;
settings = {
nullok = cfg.allowNullPassword;
inherit (cfg) nodelay;
likeauth = true;
likeauth = lib.mkIf config.security.pam.enableLegacySettings true;
};
}
{
@ -1315,11 +1315,11 @@ let
name = "unix";
enable = cfg.unixAuth;
control = "sufficient";
modulePath = "${package}/lib/security/pam_unix.so";
modulePath = config.security.pam.pam_unixModulePath;
settings = {
nullok = cfg.allowNullPassword;
inherit (cfg) nodelay;
likeauth = true;
likeauth = lib.mkIf config.security.pam.enableLegacySettings true;
try_first_pass = true;
};
}
@ -1404,10 +1404,10 @@ let
{
name = "unix";
control = "sufficient";
modulePath = "${package}/lib/security/pam_unix.so";
modulePath = config.security.pam.pam_unixModulePath;
settings = {
nullok = true;
yescrypt = true;
yescrypt = lib.mkIf config.security.pam.enableLegacySettings true;
};
}
{
@ -1493,7 +1493,7 @@ let
{
name = "unix";
control = "required";
modulePath = "${package}/lib/security/pam_unix.so";
modulePath = config.security.pam.pam_unixModulePath;
}
{
name = "loginuid";
@ -1860,6 +1860,14 @@ in
security.pam.package = lib.mkPackageOption pkgs "pam" { };
security.pam.pam_unixModulePath = lib.mkOption {
type = lib.types.pathInStore;
default = "${package}/lib/security/pam_unix.so";
defaultText = "\${config.security.pam.package}/lib/security/pam_unix.so";
description = "The pam_unix module to use in all the default pam services.";
internal = true;
};
security.pam.loginLimits = lib.mkOption {
default = [ ];
type = limitsType;
@ -1904,6 +1912,19 @@ in
'';
};
security.pam.enableLegacySettings = lib.mkOption {
default = true;
type = lib.types.bool;
description = ''
Alternative implementations of pam_unix may not support all legacy arguments.
This option will disable all known legacy settings.
::: {.note}
Setting this option to false will omit arguments, such as `yescrypt`.
Doing so is only safe if the defaults used by pam_unix are sensible.
:::
'';
};
security.pam.makeHomeDir.skelDirectory = lib.mkOption {
type = lib.types.str;
default = "/var/empty";

View file

@ -940,6 +940,7 @@ in
localsend = runTest ./localsend.nix;
locate = runTest ./locate.nix;
login = runTest ./login.nix;
login-nosuid = runTest ./login-nosuid.nix;
logkeys = runTest ./logkeys.nix;
logrotate = runTest ./logrotate.nix;
loki = runTest ./loki.nix;

View file

@ -0,0 +1,105 @@
{
name = "login-nosuid";
meta = {
maintainers = [ ];
};
# node.pkgsReadOnly = false; # needed when overriding pam to debug mode
nodes.machine =
{ pkgs, ... }:
{
security.enableWrappers = false;
systemd.settings.Manager.NoNewPrivileges = true;
security.account-utils.enable = true;
users.mutableUsers = true;
security.account-utils.extraArgs = [
"-v"
"--debug"
];
security.loginDefs.chfnRestrict = "f"; # allow allice to change name
environment.systemPackages = [
pkgs.which
pkgs.fish # environment.shells does not actually link fish to /run/current-system/sw/bin, causing chsh to fail unexpectedly
];
environment.shells = [ pkgs.fish ];
# pam debug without giant rebuild
# system.replaceDependencies.replacements = [
# {
# oldDependency = pkgs.linux-pam;
# newDependency = pkgs.linux-pam.override { debugMode = true; };
# }
# ];
};
testScript = ''
machine.start(allow_reboot = True)
machine.wait_for_unit("multi-user.target")
machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
machine.screenshot("postboot")
with subtest("account-utils passwd has priority"):
passwd_path = machine.succeed("realpath $(which passwd)")
print(f"passwd path is: {passwd_path}")
assert "account-utils" in passwd_path
with subtest("create user"):
machine.succeed("useradd -m alice")
machine.succeed("(echo foobar; echo foobar) | passwd alice")
with subtest("Check whether switching VTs works"):
machine.fail("pgrep -f 'agetty.*tty2'")
machine.send_key("alt-f2")
machine.wait_until_succeeds("[ $(fgconsole) = 2 ]")
machine.wait_for_unit("getty@tty2.service")
machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
with subtest("Log in as alice on a virtual console"):
machine.wait_until_tty_matches("2", "login: ")
machine.send_chars("alice\n")
machine.wait_until_tty_matches("2", "login: alice")
machine.wait_until_succeeds("pgrep login")
machine.wait_until_tty_matches("2", "Password: ")
machine.sleep(1) # something is racy here, so lets just sleep a bit... Not great, but seems to work
machine.send_chars("foobar\n")
machine.wait_until_succeeds("pgrep -u alice bash")
machine.send_chars("touch done\n")
machine.wait_for_file("/home/alice/done")
with subtest("Systemd gives and removes device ownership as needed"):
machine.succeed("getfacl /dev/snd/timer | grep -q alice")
machine.send_key("alt-f1")
machine.wait_until_succeeds("[ $(fgconsole) = 1 ]")
machine.fail("getfacl /dev/snd/timer | grep -q alice")
machine.succeed("chvt 2")
machine.wait_until_succeeds("getfacl /dev/snd/timer | grep -q alice")
with subtest("User can change their login shell"):
machine.send_chars("clear\n") # remove previous password prompts
machine.send_chars("chsh -s /run/current-system/sw/bin/fish\n")
machine.wait_until_tty_matches("2", "Password: ")
machine.send_chars("foobar\n")
machine.wait_until_fails("pgrep pwupdd")
login_shell = machine.succeed("getent passwd alice | cut -d: -f7").strip()
print(f"login shell of user alice: {login_shell}")
assert "/run/current-system/sw/bin/fish" == login_shell
with subtest("User can change their name"):
machine.send_chars("clear\n") # remove previous password prompts
machine.send_chars("chfn -f 'Alice in Wonderland'\n")
machine.wait_until_tty_matches("2", "Password: ")
machine.send_chars("foobar\n")
machine.wait_until_fails("pgrep pwupdd")
full_name = machine.succeed("getent passwd alice | cut -d: -f5").strip()
print(f"full name of user alice: {full_name}")
assert "Alice in Wonderland" == full_name
with subtest("Virtual console logout"):
machine.send_chars("exit\n")
machine.wait_until_fails("pgrep -u alice bash")
machine.screenshot("getty")
'';
}

View file

@ -0,0 +1,77 @@
{
lib,
stdenv,
fetchFromGitHub,
meson,
ninja,
pkg-config,
linux-pam,
systemdLibs,
libcap,
libxcrypt,
libeconf,
libselinux,
docbook-xsl-ns,
libxslt,
nixosTests,
}:
let
selinuxSupport = lib.meta.availableOn stdenv.hostPlatform libselinux;
in
stdenv.mkDerivation (finalAttrs: {
pname = "account-utils";
version = "1.3.0";
src = fetchFromGitHub {
owner = "thkukuk";
repo = "account-utils";
tag = "v${finalAttrs.version}";
hash = "sha256-9l+y7FLb0IZXXp4RstlhNR6yA7b4b831obFuiVtO9+k=";
};
__structuredAttrs = true;
strictDeps = true;
nativeBuildInputs = [
meson
ninja
pkg-config
libxslt
docbook-xsl-ns
];
buildInputs = [
linux-pam
systemdLibs
libxcrypt
libeconf
libcap
]
++ lib.optional selinuxSupport libselinux;
mesonFlags = [
(lib.mesonEnable "selinux" selinuxSupport)
(lib.mesonOption "c_args" "-ffat-lto-objects")
];
passthru.tests = {
inherit (nixosTests) login-nosuid;
};
meta = {
description = "Services, utilities and PAM modules, which allow authentication and account management on systems with the NoNewPrivs flag set (no setuid/setgid binaries)";
homepage = "https://github.com/thkukuk/account-utils";
changelog = "https://github.com/thkukuk/account-utils/releases/tag/v${finalAttrs.version}";
license =
with lib.licenses;
AND [
gpl2Plus
lgpl21Plus
bsd2
];
maintainers = with lib.maintainers; [ grimmauld ];
platforms = lib.platforms.linux;
# take precedence over shadow
priority = -1;
};
})

View file

@ -0,0 +1,40 @@
{
lib,
stdenv,
fetchFromGitHub,
meson,
ninja,
nix-update-script,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "libeconf";
version = "0.8.3";
src = fetchFromGitHub {
owner = "openSUSE";
repo = "libeconf";
tag = "v${finalAttrs.version}";
hash = "sha256-ZXZcXQdG3hXAMwwftrIWL5GbVdPXk+AyqdhGTnaKL1I=";
};
__structuredAttrs = true;
strictDeps = true;
nativeBuildInputs = [
meson
ninja
];
passthru.updateScript = nix-update-script { };
meta = {
description = "Enhanced config file parser, which merges config files placed in several locations into one";
homepage = "https://github.com/openSUSE/libeconf";
changelog = "https://github.com/openSUSE/libeconf/blob/${finalAttrs.src.tag}/NEWS";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ grimmauld ];
mainProgram = "econftool";
platforms = lib.platforms.all;
};
})