Loading nixos/modules/security/polkit.nix +22 −0 Original line number Diff line number Diff line Loading @@ -80,6 +80,28 @@ in systemd.sockets."polkit-agent-helper".wantedBy = [ "sockets.target" ]; systemd.services."polkit-agent-helper@".serviceConfig = lib.mkMerge [ # The upstream unit inherits stderr to the polkit agent, which causes # agent processes to misinterpret diagnostic output from PAM modules # as protocol errors, resulting in tight re-execution loops. { StandardError = "journal"; } # The upstream unit uses PrivateDevices=yes and ProtectHome=yes, # which prevents PAM modules from accessing hardware (e.g. FIDO # tokens via /dev/hidraw*) or reading key files from home directories. (lib.mkIf config.security.pam.u2f.enable { # Override upstream PrivateDevices=yes to allow access to /dev/hidraw* PrivateDevices = false; DeviceAllow = [ "/dev/urandom r" "char-hidraw rw" ]; # Override upstream ProtectHome=yes so pam_u2f can read # ~/.config/Yubico/u2f_keys (the default key file location) ProtectHome = "read-only"; }) ]; # The polkit daemon reads action/rule files environment.pathsToLink = [ "/share/polkit-1" ]; Loading nixos/tests/all-tests.nix +1 −0 Original line number Diff line number Diff line Loading @@ -1220,6 +1220,7 @@ in pam-oath-login = runTest ./pam/pam-oath-login.nix; pam-pgsql = runTest ./pam/pam-pgsql.nix; pam-u2f = runTest ./pam/pam-u2f.nix; pam-u2f-polkit = runTest ./pam/pam-u2f-polkit.nix; pam-ussh = runTest ./pam/pam-ussh.nix; pam-zfs-key = runTest ./pam/zfs-key.nix; pangolin = runTest ./pangolin.nix; Loading nixos/tests/pam/pam-u2f-polkit.nix 0 → 100644 +90 −0 Original line number Diff line number Diff line { hostPkgs, ... }: { name = "pam-u2f-polkit"; qemu.package = hostPkgs.qemu_test.override { u2fEmuSupport = true; }; nodes.machine = { pkgs, ... }: { virtualisation.qemu.options = [ "-usb" "-device u2f-emulated" ]; security.polkit.enable = true; security.pam.u2f.enable = true; environment.systemPackages = with pkgs; [ libfido2 pam_u2f ]; }; testScript = '' machine.wait_for_unit("multi-user.target") # The upstream polkit-agent-helper@.service has PrivateDevices=yes and # DevicePolicy=strict with only /dev/null allowed. This blocks hidraw. # Verify that: run a command under the upstream defaults and show it fails. machine.fail( "systemd-run --wait --pipe " "--property=PrivateDevices=yes " "--property=DevicePolicy=strict " "--property='DeviceAllow=/dev/null rw' " "test -c /dev/hidraw0" ) # The PR overrides PrivateDevices=no and adds DeviceAllow for hidraw. # Verify that the actual polkit-agent-helper@ unit got these overrides. props = machine.succeed("systemctl show polkit-agent-helper@dummy.service") assert "PrivateDevices=no" in props, f"Expected PrivateDevices=no, got: {props}" assert "ProtectHome=read-only" in props, f"Expected ProtectHome=read-only, got: {props}" # Run fido2-token under the same constraints as the fixed service. # This proves the device is not just visible but actually usable # inside the polkit-agent-helper@ sandbox. machine.succeed( "systemd-run --wait --pipe " "--property=PrivateDevices=no " "--property=DevicePolicy=strict " "--property='DeviceAllow=/dev/null rw' " "--property='DeviceAllow=/dev/urandom r' " "--property='DeviceAllow=char-hidraw rw' " "--property=ProtectHome=read-only " "--property=PrivateNetwork=yes " "--property=ProtectSystem=strict " "--property=ProtectKernelModules=yes " "--property=ProtectKernelLogs=yes " "--property=ProtectKernelTunables=yes " "--property=ProtectControlGroups=yes " "--property=ProtectClock=yes " "--property=ProtectHostname=yes " "--property=LockPersonality=yes " "--property=MemoryDenyWriteExecute=yes " "--property=NoNewPrivileges=yes " "--property=PrivateTmp=yes " "--property=RemoveIPC=yes " "--property='RestrictAddressFamilies=AF_UNIX' " "--property=RestrictNamespaces=yes " "--property=RestrictRealtime=yes " "--property=RestrictSUIDSGID=yes " "--property=SystemCallArchitectures=native " "fido2-token -I /dev/hidraw0" ) # Also verify that pamu2fcfg can register a credential inside the sandbox # (needs hidraw + urandom access) machine.succeed( "systemd-run --wait --pipe " "--property=PrivateDevices=no " "--property=DevicePolicy=strict " "--property='DeviceAllow=/dev/null rw' " "--property='DeviceAllow=/dev/urandom r' " "--property='DeviceAllow=char-hidraw rw' " "--property=ProtectHome=read-only " "pamu2fcfg" ) ''; } nixos/tests/pam/pam-u2f.nix +50 −9 Original line number Diff line number Diff line { ... }: { hostPkgs, ... }: { name = "pam-u2f"; qemu.package = hostPkgs.qemu_test.override { u2fEmuSupport = true; }; nodes.machine = { ... }: { pkgs, ... }: { virtualisation.qemu.options = [ "-usb" "-device u2f-emulated" ]; security.pam.u2f = { enable = true; control = "required"; control = "sufficient"; settings = { cue = true; debug = true; interactive = true; origin = "nixos-test"; # Freeform option userpresence = 1; origin = "pam://nixos-test"; }; }; users.users.alice = { isNormalUser = true; uid = 1000; }; environment.systemPackages = with pkgs; [ libfido2 pam_u2f ]; # Allow non-root users to access the virtual U2F device services.udev.extraRules = '' KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0666" ''; }; testScript = '' machine.wait_for_unit("multi-user.target") machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") # The virtual U2F device should be recognized machine.succeed("fido2-token -L | grep -q hidraw") # Register a U2F credential for alice machine.succeed("mkdir -p /home/alice/.config/Yubico") machine.succeed( 'egrep "auth required .*/lib/security/pam_u2f.so.*cue.*debug.*interactive.*origin=nixos-test.*userpresence=1" /etc/pam.d/ -R' "pamu2fcfg -u alice -o pam://nixos-test" " > /home/alice/.config/Yubico/u2f_keys" ) machine.succeed("chown -R alice:users /home/alice/.config") # Log in as alice on tty2. With control=sufficient, pam_u2f runs # before pam_unix. The emulated device auto-approves user presence, # so alice is authenticated by her U2F key — no password needed. machine.send_key("alt-f2") machine.wait_until_succeeds("[ $(fgconsole) = 2 ]") machine.wait_for_unit("getty@tty2.service") machine.wait_until_tty_matches("2", "login: ") machine.send_chars("alice\n") # alice should get a shell without being asked for a password machine.wait_until_succeeds("pgrep -u alice bash") machine.send_chars("touch /tmp/u2f-login-success\n") machine.wait_for_file("/tmp/u2f-login-success") ''; } Loading
nixos/modules/security/polkit.nix +22 −0 Original line number Diff line number Diff line Loading @@ -80,6 +80,28 @@ in systemd.sockets."polkit-agent-helper".wantedBy = [ "sockets.target" ]; systemd.services."polkit-agent-helper@".serviceConfig = lib.mkMerge [ # The upstream unit inherits stderr to the polkit agent, which causes # agent processes to misinterpret diagnostic output from PAM modules # as protocol errors, resulting in tight re-execution loops. { StandardError = "journal"; } # The upstream unit uses PrivateDevices=yes and ProtectHome=yes, # which prevents PAM modules from accessing hardware (e.g. FIDO # tokens via /dev/hidraw*) or reading key files from home directories. (lib.mkIf config.security.pam.u2f.enable { # Override upstream PrivateDevices=yes to allow access to /dev/hidraw* PrivateDevices = false; DeviceAllow = [ "/dev/urandom r" "char-hidraw rw" ]; # Override upstream ProtectHome=yes so pam_u2f can read # ~/.config/Yubico/u2f_keys (the default key file location) ProtectHome = "read-only"; }) ]; # The polkit daemon reads action/rule files environment.pathsToLink = [ "/share/polkit-1" ]; Loading
nixos/tests/all-tests.nix +1 −0 Original line number Diff line number Diff line Loading @@ -1220,6 +1220,7 @@ in pam-oath-login = runTest ./pam/pam-oath-login.nix; pam-pgsql = runTest ./pam/pam-pgsql.nix; pam-u2f = runTest ./pam/pam-u2f.nix; pam-u2f-polkit = runTest ./pam/pam-u2f-polkit.nix; pam-ussh = runTest ./pam/pam-ussh.nix; pam-zfs-key = runTest ./pam/zfs-key.nix; pangolin = runTest ./pangolin.nix; Loading
nixos/tests/pam/pam-u2f-polkit.nix 0 → 100644 +90 −0 Original line number Diff line number Diff line { hostPkgs, ... }: { name = "pam-u2f-polkit"; qemu.package = hostPkgs.qemu_test.override { u2fEmuSupport = true; }; nodes.machine = { pkgs, ... }: { virtualisation.qemu.options = [ "-usb" "-device u2f-emulated" ]; security.polkit.enable = true; security.pam.u2f.enable = true; environment.systemPackages = with pkgs; [ libfido2 pam_u2f ]; }; testScript = '' machine.wait_for_unit("multi-user.target") # The upstream polkit-agent-helper@.service has PrivateDevices=yes and # DevicePolicy=strict with only /dev/null allowed. This blocks hidraw. # Verify that: run a command under the upstream defaults and show it fails. machine.fail( "systemd-run --wait --pipe " "--property=PrivateDevices=yes " "--property=DevicePolicy=strict " "--property='DeviceAllow=/dev/null rw' " "test -c /dev/hidraw0" ) # The PR overrides PrivateDevices=no and adds DeviceAllow for hidraw. # Verify that the actual polkit-agent-helper@ unit got these overrides. props = machine.succeed("systemctl show polkit-agent-helper@dummy.service") assert "PrivateDevices=no" in props, f"Expected PrivateDevices=no, got: {props}" assert "ProtectHome=read-only" in props, f"Expected ProtectHome=read-only, got: {props}" # Run fido2-token under the same constraints as the fixed service. # This proves the device is not just visible but actually usable # inside the polkit-agent-helper@ sandbox. machine.succeed( "systemd-run --wait --pipe " "--property=PrivateDevices=no " "--property=DevicePolicy=strict " "--property='DeviceAllow=/dev/null rw' " "--property='DeviceAllow=/dev/urandom r' " "--property='DeviceAllow=char-hidraw rw' " "--property=ProtectHome=read-only " "--property=PrivateNetwork=yes " "--property=ProtectSystem=strict " "--property=ProtectKernelModules=yes " "--property=ProtectKernelLogs=yes " "--property=ProtectKernelTunables=yes " "--property=ProtectControlGroups=yes " "--property=ProtectClock=yes " "--property=ProtectHostname=yes " "--property=LockPersonality=yes " "--property=MemoryDenyWriteExecute=yes " "--property=NoNewPrivileges=yes " "--property=PrivateTmp=yes " "--property=RemoveIPC=yes " "--property='RestrictAddressFamilies=AF_UNIX' " "--property=RestrictNamespaces=yes " "--property=RestrictRealtime=yes " "--property=RestrictSUIDSGID=yes " "--property=SystemCallArchitectures=native " "fido2-token -I /dev/hidraw0" ) # Also verify that pamu2fcfg can register a credential inside the sandbox # (needs hidraw + urandom access) machine.succeed( "systemd-run --wait --pipe " "--property=PrivateDevices=no " "--property=DevicePolicy=strict " "--property='DeviceAllow=/dev/null rw' " "--property='DeviceAllow=/dev/urandom r' " "--property='DeviceAllow=char-hidraw rw' " "--property=ProtectHome=read-only " "pamu2fcfg" ) ''; }
nixos/tests/pam/pam-u2f.nix +50 −9 Original line number Diff line number Diff line { ... }: { hostPkgs, ... }: { name = "pam-u2f"; qemu.package = hostPkgs.qemu_test.override { u2fEmuSupport = true; }; nodes.machine = { ... }: { pkgs, ... }: { virtualisation.qemu.options = [ "-usb" "-device u2f-emulated" ]; security.pam.u2f = { enable = true; control = "required"; control = "sufficient"; settings = { cue = true; debug = true; interactive = true; origin = "nixos-test"; # Freeform option userpresence = 1; origin = "pam://nixos-test"; }; }; users.users.alice = { isNormalUser = true; uid = 1000; }; environment.systemPackages = with pkgs; [ libfido2 pam_u2f ]; # Allow non-root users to access the virtual U2F device services.udev.extraRules = '' KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0666" ''; }; testScript = '' machine.wait_for_unit("multi-user.target") machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") # The virtual U2F device should be recognized machine.succeed("fido2-token -L | grep -q hidraw") # Register a U2F credential for alice machine.succeed("mkdir -p /home/alice/.config/Yubico") machine.succeed( 'egrep "auth required .*/lib/security/pam_u2f.so.*cue.*debug.*interactive.*origin=nixos-test.*userpresence=1" /etc/pam.d/ -R' "pamu2fcfg -u alice -o pam://nixos-test" " > /home/alice/.config/Yubico/u2f_keys" ) machine.succeed("chown -R alice:users /home/alice/.config") # Log in as alice on tty2. With control=sufficient, pam_u2f runs # before pam_unix. The emulated device auto-approves user presence, # so alice is authenticated by her U2F key — no password needed. machine.send_key("alt-f2") machine.wait_until_succeeds("[ $(fgconsole) = 2 ]") machine.wait_for_unit("getty@tty2.service") machine.wait_until_tty_matches("2", "login: ") machine.send_chars("alice\n") # alice should get a shell without being asked for a password machine.wait_until_succeeds("pgrep -u alice bash") machine.send_chars("touch /tmp/u2f-login-success\n") machine.wait_for_file("/tmp/u2f-login-success") ''; }