summaryrefslogtreecommitdiff
path: root/flake.nix
blob: efe6a055880ac94961c58d909049d479c14466ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
{
  description = "PAM module that runs multiple PAM modules in parallel, succeeding if any one succeeds";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in
    {
      packages.${system}.default = pkgs.callPackage ./default.nix {};

      nixosModules.default = { config, pkgs, lib, ... }:
        let
          cfg = config.security.pam-any;
          pam-any = self.packages.${pkgs.stdenv.hostPlatform.system}.default;

          pamAnyConfigFile = pkgs.writeText "pam-any-config.json" (builtins.toJSON {
            mode = cfg.mode;
            modules = cfg.modules;
          });
        in
        {
          options.security.pam-any = {
            enable = lib.mkEnableOption "pam-any parallel authentication";

            mode = lib.mkOption {
              type = lib.types.enum [ "One" "All" ];
              default = "One";
              description = "\"One\" succeeds if any module succeeds, \"All\" requires all to succeed.";
            };

            modules = lib.mkOption {
              type = lib.types.attrsOf lib.types.str;
              default = {
                pam-any-fingerprint = "Fingerprint";
                pam-any-password = "Password";
              };
              description = "Map of PAM service names to display labels for parallel auth.";
            };

            services = lib.mkOption {
              type = lib.types.listOf lib.types.str;
              default = [ "sudo" "login" ];
              description = "PAM services to apply pam-any to.";
            };

            order = lib.mkOption {
              type = lib.types.int;
              default = 1000;
              description = "Order of the pam-any rule in the auth stack.";
            };
          };

          config = lib.mkIf cfg.enable {
            security.pam.services = {
              pam-any-fingerprint.text = ''
                auth required ${pkgs.fprintd}/lib/security/pam_fprintd.so
              '';

              pam-any-password.text = ''
                auth required ${pkgs.linux-pam}/lib/security/pam_unix.so
              '';
            } // lib.genAttrs cfg.services (_name: {
              fprintAuth = false;
              rules.auth = {
                # If pam-any succeeds (fingerprint or password in parallel), auth is done.
                # If it fails/crashes, fall through to normal password prompt as safety net.
                pam-any = {
                  order = cfg.order;
                  control = "[success=done default=ignore]";
                  modulePath = "${pam-any}/lib/security/pam_any.so";
                  args = [ "${pamAnyConfigFile}" ];
                };
                # Fallback: keep normal password auth but without nullok
                # so empty passwords are never accepted.
                unix = {
                  order = 11700;
                  control = "sufficient";
                  modulePath = "${pkgs.linux-pam}/lib/security/pam_unix.so";
                  args = lib.mkForce [ "likeauth" "try_first_pass" ];
                };
              };
            });
          };
        };
    };
}