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 ceb9ca53716e..f3c6588033fc 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
@@ -1785,6 +1785,15 @@
should now be used instead.
+
+
+ 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 ff628d007250..023dcc9f9120 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -613,6 +613,8 @@ In addition to numerous new and upgraded packages, this release has the followin
and [services.logrotate.extraConfig](#opt-services.logrotate.extraConfig) will work, but issue deprecation
warnings and [services.logrotate.settings](#opt-services.logrotate.settings) should now be used instead.
+- `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 141e1ee9c2c8..530304b497ae 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}
'') +
@@ -927,6 +943,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;
@@ -1111,6 +1217,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 d9429920f13b..b427933cb20a 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -398,6 +398,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
new file mode 100644
index 000000000000..889c8bc6f57c
--- /dev/null
+++ b/pkgs/os-specific/linux/pam_ussh/default.nix
@@ -0,0 +1,67 @@
+{ buildGoModule
+, fetchFromGitHub
+, pam
+, lib
+, nixosTests
+}:
+
+buildGoModule rec {
+ pname = "pam_ussh";
+ version = "unstable-20210615";
+
+ src = fetchFromGitHub {
+ owner = "uber";
+ repo = "pam-ussh";
+ rev = "e9524bda90ba19d3b9eb24f49cb63a6a56a19193"; # HEAD as of 2022-03-13
+ sha256 = "0nb9hpqbghgi3zvq41kabydzyc6ffaaw9b4jkc5jrwn1klpw1xk8";
+ };
+
+ prePatch = ''
+ cp ${./go.mod} go.mod
+ '';
+ overrideModAttrs = (_: {
+ inherit prePatch;
+ });
+
+ vendorSha256 = "0hjifc3kbwmx7kjn858vi05cwwra6q19cqjfd94k726pwhk37qkw";
+
+ buildInputs = [
+ pam
+ ];
+
+ buildPhase = ''
+ runHook preBuild
+
+ if [ -z "$enableParallelBuilding" ]; then
+ export NIX_BUILD_CORES=1
+ fi
+ go build -buildmode=c-shared -o pam_ussh.so -v -p $NIX_BUILD_CORES .
+
+ runHook postBuild
+ '';
+ checkPhase = ''
+ runHook preCheck
+
+ go test -v -p $NIX_BUILD_CORES .
+
+ runHook postCheck
+ '';
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/lib/security
+ cp pam_ussh.so $out/lib/security
+
+ 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";
+ license = licenses.mit;
+ platforms = platforms.linux;
+ maintainers = with maintainers; [ lukegb ];
+ };
+}
diff --git a/pkgs/os-specific/linux/pam_ussh/go.mod b/pkgs/os-specific/linux/pam_ussh/go.mod
new file mode 100644
index 000000000000..9adc453560a4
--- /dev/null
+++ b/pkgs/os-specific/linux/pam_ussh/go.mod
@@ -0,0 +1,15 @@
+module github.com/uber/pam-ussh
+
+go 1.17
+
+require (
+ github.com/stretchr/testify v1.7.0
+ golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+)
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 7f596c047184..4b1c304c5d11 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -23106,6 +23106,8 @@ with pkgs;
pam_usb = callPackage ../os-specific/linux/pam_usb { };
+ pam_ussh = callPackage ../os-specific/linux/pam_ussh { };
+
paxctl = callPackage ../os-specific/linux/paxctl { };
paxtest = callPackage ../os-specific/linux/paxtest { };