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";