nixos/pam: add support for pam-ussh

pam-ussh allows authorizing using an SSH certificate stored in your
SSH agent, in a similar manner to pam-ssh-agent-auth, but for
certificates rather than raw public keys.
This commit is contained in:
Luke Granger-Brown 2022-03-13 17:20:23 +00:00
parent f63d93bc3d
commit 1853015550
7 changed files with 195 additions and 1 deletions

View File

@ -1286,6 +1286,15 @@
been added by default.
</para>
</listitem>
<listitem>
<para>
<literal>security.pam.ussh</literal> has been added, which
allows authorizing PAM sessions based on SSH
<emphasis>certificates</emphasis> held within an SSH agent,
using
<link xlink:href="https://github.com/uber/pam-ussh">pam-ussh</link>.
</para>
</listitem>
<listitem>
<para>
The <literal>zrepl</literal> package has been updated from

View File

@ -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.

View File

@ -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 <option>security.pam.ussh</option> options.
Note that the <option>security.pam.ussh.enable</option> 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 (<literal>pam-ussh</literal>) module.
This is similar to <literal>pam-ssh-agent</literal>, 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 <literal>usshAuth</literal>).
More information can be found <link
xlink:href="https://github.com/uber/pam-ussh">here</link>.
'';
};
caFile = mkOption {
default = null;
type = with types; nullOr path;
description = ''
By default <literal>pam-ussh</literal> reads the trusted user CA keys
from <filename>/etc/ssh/trusted_user_ca</filename>.
This should be set the same as your <literal>TrustedUserCAKeys</literal>
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 <literal>pam-ussh</literal> 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 <literal>authorizedPrincipalsFile</literal>.
'';
};
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 <literal>pam-ussh</literal> 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 <literal>authorizedPrincipals</literal>.
'';
};
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
<citerefentry>
<refentrytitle>pam.conf</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry>
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,
'' +

View File

@ -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 =

View File

@ -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 {};

View File

@ -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'
)
'';
})

View File

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