Unverified Commit c111fb7e authored by Philip Taron's avatar Philip Taron Committed by GitHub
Browse files

pam_u2f: fix usage with polkit 127 (#486044)

parents fc56e129 098b000a
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -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" ];

+1 −0
Original line number Diff line number Diff line
@@ -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;
+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"
    )
  '';
}
+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")
  '';
}