diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 9e3d2f772b48..507eeb3a9ff0 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/security/account-utils.nix b/nixos/modules/security/account-utils.nix new file mode 100644 index 000000000000..bd2bf08f55b2 --- /dev/null +++ b/nixos/modules/security/account-utils.nix @@ -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 + }; + }; +} diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 586ae6fd9ede..7d924bff8ca1 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -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"; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 1c760d0648f0..09b0a4632d28 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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; diff --git a/nixos/tests/login-nosuid.nix b/nixos/tests/login-nosuid.nix new file mode 100644 index 000000000000..1f00c37d0886 --- /dev/null +++ b/nixos/tests/login-nosuid.nix @@ -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") + ''; +} diff --git a/pkgs/by-name/ac/account-utils/package.nix b/pkgs/by-name/ac/account-utils/package.nix new file mode 100644 index 000000000000..c0df5e64109d --- /dev/null +++ b/pkgs/by-name/ac/account-utils/package.nix @@ -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; + }; +}) diff --git a/pkgs/by-name/li/libeconf/package.nix b/pkgs/by-name/li/libeconf/package.nix new file mode 100644 index 000000000000..4c0b848c0ae9 --- /dev/null +++ b/pkgs/by-name/li/libeconf/package.nix @@ -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; + }; +})