diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml index 9cf27e56827a..ede0e10e0346 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml @@ -1286,6 +1286,15 @@ been added by default. + + + security.pam.ussh has been added, which + allows authorizing PAM sessions based on SSH + certificates held within an SSH agent, + using + pam-ussh. + + The zrepl package has been updated from diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md index 58a1b23d17bf..90ac7e90e729 100644 --- a/nixos/doc/manual/release-notes/rl-2205.section.md +++ b/nixos/doc/manual/release-notes/rl-2205.section.md @@ -470,6 +470,8 @@ In addition to numerous new and upgraded packages, this release has the followin - `services.logrotate.enable` now defaults to true if any rotate path has been defined, and some paths have been added by default. +- `security.pam.ussh` has been added, which allows authorizing PAM sessions based on SSH _certificates_ held within an SSH agent, using [pam-ussh](https://github.com/uber/pam-ussh). + - The `zrepl` package has been updated from 0.4.0 to 0.5: - The RPC protocol version was bumped; all zrepl daemons in a setup must be updated and restarted before replication can resume. diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index c0ef8b5f30bd..f9697d61f1b2 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -61,6 +61,19 @@ let ''; }; + usshAuth = mkOption { + default = false; + type = types.bool; + description = '' + If set, users with an SSH certificate containing an authorized principal + in their SSH agent are able to log in. Specific options are controlled + using the options. + + Note that the must also be + set for this option to take effect. + ''; + }; + yubicoAuth = mkOption { default = config.security.pam.yubico.enable; defaultText = literalExpression "config.security.pam.yubico.enable"; @@ -475,6 +488,9 @@ let optionalString cfg.usbAuth '' auth sufficient ${pkgs.pam_usb}/lib/security/pam_usb.so '' + + (let ussh = config.security.pam.ussh; in optionalString (config.security.pam.ussh.enable && cfg.usshAuth) '' + auth ${ussh.control} ${pkgs.pam_ussh}/lib/security/pam_ussh.so ${optionalString (ussh.caFile != null) "ca_file=${ussh.caFile}"} ${optionalString (ussh.authorizedPrincipals != null) "authorized_principals=${ussh.authorizedPrincipals}"} ${optionalString (ussh.authorizedPrincipalsFile != null) "authorized_principals_file=${ussh.authorizedPrincipalsFile}"} ${optionalString (ussh.group != null) "group=${ussh.group}"} + '') + (let oath = config.security.pam.oath; in optionalString cfg.oathAuth '' auth requisite ${pkgs.oathToolkit}/lib/security/pam_oath.so window=${toString oath.window} usersfile=${toString oath.usersFile} digits=${toString oath.digits} '') + @@ -926,6 +942,96 @@ in }; }; + security.pam.ussh = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enables Uber's USSH PAM (pam-ussh) module. + + This is similar to pam-ssh-agent, except that + the presence of a CA-signed SSH key with a valid principal is checked + instead. + + Note that this module must both be enabled using this option and on a + per-PAM-service level as well (using usshAuth). + + More information can be found here. + ''; + }; + + caFile = mkOption { + default = null; + type = with types; nullOr path; + description = '' + By default pam-ussh reads the trusted user CA keys + from /etc/ssh/trusted_user_ca. + + This should be set the same as your TrustedUserCAKeys + option for sshd. + ''; + }; + + authorizedPrincipals = mkOption { + default = null; + type = with types; nullOr commas; + description = '' + Comma-separated list of authorized principals to permit; if the user + presents a certificate with one of these principals, then they will be + authorized. + + Note that pam-ussh also requires that the certificate + contain a principal matching the user's username. The principals from + this list are in addition to those principals. + + Mutually exclusive with authorizedPrincipalsFile. + ''; + }; + + authorizedPrincipalsFile = mkOption { + default = null; + type = with types; nullOr path; + description = '' + Path to a list of principals; if the user presents a certificate with + one of these principals, then they will be authorized. + + Note that pam-ussh also requires that the certificate + contain a principal matching the user's username. The principals from + this file are in addition to those principals. + + Mutually exclusive with authorizedPrincipals. + ''; + }; + + group = mkOption { + default = null; + type = with types; nullOr str; + description = '' + If set, then the authenticating user must be a member of this group + to use this module. + ''; + }; + + control = mkOption { + default = "sufficient"; + type = types.enum [ "required" "requisite" "sufficient" "optional" ]; + description = '' + This option sets pam "control". + If you want to have multi factor authentication, use "required". + If you want to use the SSH certificate instead of the regular password, + use "sufficient". + + Read + + pam.conf + 5 + + for better understanding of this option. + ''; + }; + }; + security.pam.yubico = { enable = mkOption { default = false; @@ -1110,6 +1216,9 @@ in optionalString (isEnabled (cfg: cfg.usbAuth)) '' mr ${pkgs.pam_usb}/lib/security/pam_usb.so, '' + + optionalString (isEnabled (cfg: cfg.usshAuth)) '' + mr ${pkgs.pam_ussh}/lib/security/pam_ussh.so, + '' + optionalString (isEnabled (cfg: cfg.oathAuth)) '' "mr ${pkgs.oathToolkit}/lib/security/pam_oath.so, '' + diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix index 99e578f8adae..4bf239fca8f9 100644 --- a/nixos/modules/security/sudo.nix +++ b/nixos/modules/security/sudo.nix @@ -245,7 +245,7 @@ in environment.systemPackages = [ sudo ]; - security.pam.services.sudo = { sshAgentAuth = true; }; + security.pam.services.sudo = { sshAgentAuth = true; usshAuth = true; }; environment.etc.sudoers = { source = diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 043d8a56d0c6..1ed12c54c575 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -388,6 +388,7 @@ in pam-file-contents = handleTest ./pam/pam-file-contents.nix {}; pam-oath-login = handleTest ./pam/pam-oath-login.nix {}; pam-u2f = handleTest ./pam/pam-u2f.nix {}; + pam-ussh = handleTest ./pam/pam-ussh.nix {}; pantalaimon = handleTest ./matrix/pantalaimon.nix {}; pantheon = handleTest ./pantheon.nix {}; paperless-ng = handleTest ./paperless-ng.nix {}; diff --git a/nixos/tests/pam/pam-ussh.nix b/nixos/tests/pam/pam-ussh.nix new file mode 100644 index 000000000000..ba0570dbf97d --- /dev/null +++ b/nixos/tests/pam/pam-ussh.nix @@ -0,0 +1,70 @@ +import ../make-test-python.nix ({ pkgs, lib, ... }: + +let + testOnlySSHCredentials = pkgs.runCommand "pam-ussh-test-ca" { + nativeBuildInputs = [ pkgs.openssh ]; + } '' + mkdir $out + ssh-keygen -t ed25519 -N "" -f $out/ca + + ssh-keygen -t ed25519 -N "" -f $out/alice + ssh-keygen -s $out/ca -I "alice user key" -n "alice,root" -V 19700101:forever $out/alice.pub + + ssh-keygen -t ed25519 -N "" -f $out/bob + ssh-keygen -s $out/ca -I "bob user key" -n "bob" -V 19700101:forever $out/bob.pub + ''; + makeTestScript = user: pkgs.writeShellScript "pam-ussh-${user}-test-script" '' + set -euo pipefail + + eval $(${pkgs.openssh}/bin/ssh-agent) + + mkdir -p $HOME/.ssh + chmod 700 $HOME/.ssh + cp ${testOnlySSHCredentials}/${user}{,.pub,-cert.pub} $HOME/.ssh + chmod 600 $HOME/.ssh/${user} + chmod 644 $HOME/.ssh/${user}{,-cert}.pub + + set -x + + ${pkgs.openssh}/bin/ssh-add $HOME/.ssh/${user} + ${pkgs.openssh}/bin/ssh-add -l &>2 + + exec sudo id -u -n + ''; +in { + name = "pam-ussh"; + meta.maintainers = with lib.maintainers; [ lukegb ]; + + machine = + { ... }: + { + users.users.alice = { isNormalUser = true; extraGroups = [ "wheel" ]; }; + users.users.bob = { isNormalUser = true; extraGroups = [ "wheel" ]; }; + + security.pam.ussh = { + enable = true; + authorizedPrincipals = "root"; + caFile = "${testOnlySSHCredentials}/ca.pub"; + }; + + security.sudo = { + enable = true; + extraConfig = '' + Defaults lecture="never" + ''; + }; + }; + + testScript = + '' + with subtest("alice should be allowed to escalate to root"): + machine.succeed( + 'su -c "${makeTestScript "alice"}" -l alice | grep root' + ) + + with subtest("bob should not be allowed to escalate to root"): + machine.fail( + 'su -c "${makeTestScript "bob"}" -l bob | grep root' + ) + ''; +}) diff --git a/pkgs/os-specific/linux/pam_ussh/default.nix b/pkgs/os-specific/linux/pam_ussh/default.nix index 499239500acc..889c8bc6f57c 100644 --- a/pkgs/os-specific/linux/pam_ussh/default.nix +++ b/pkgs/os-specific/linux/pam_ussh/default.nix @@ -2,6 +2,7 @@ , fetchFromGitHub , pam , lib +, nixosTests }: buildGoModule rec { @@ -54,6 +55,8 @@ buildGoModule rec { runHook postInstall ''; + passthru.tests = { inherit (nixosTests) pam-ussh; }; + meta = with lib; { homepage = "https://github.com/uber/pam-ussh"; description = "PAM module to authenticate using SSH certificates";