{ 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" ]; }; }; }); }; }; }; }