From f63d93bc3d0ad73b24b2cf9c0ee0381161071395 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sun, 13 Mar 2022 17:20:16 +0000 Subject: [PATCH 1/2] pam_ussh: init at unstable-20210615 --- pkgs/os-specific/linux/pam_ussh/default.nix | 64 +++++++++++++++++++++ pkgs/os-specific/linux/pam_ussh/go.mod | 15 +++++ pkgs/top-level/all-packages.nix | 2 + 3 files changed, 81 insertions(+) create mode 100644 pkgs/os-specific/linux/pam_ussh/default.nix create mode 100644 pkgs/os-specific/linux/pam_ussh/go.mod 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 0000000000000..499239500acce --- /dev/null +++ b/pkgs/os-specific/linux/pam_ussh/default.nix @@ -0,0 +1,64 @@ +{ buildGoModule +, fetchFromGitHub +, pam +, lib +}: + +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 + ''; + + 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 0000000000000..9adc453560a43 --- /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 6c6832ae05475..539ecd4ec481b 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -23052,6 +23052,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 { }; From 1853015550a78acbc3e9d090d174120796c4b784 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sun, 13 Mar 2022 17:20:23 +0000 Subject: [PATCH 2/2] 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. --- .../from_md/release-notes/rl-2205.section.xml | 9 ++ .../manual/release-notes/rl-2205.section.md | 2 + nixos/modules/security/pam.nix | 109 ++++++++++++++++++ nixos/modules/security/sudo.nix | 2 +- nixos/tests/all-tests.nix | 1 + nixos/tests/pam/pam-ussh.nix | 70 +++++++++++ pkgs/os-specific/linux/pam_ussh/default.nix | 3 + 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 nixos/tests/pam/pam-ussh.nix 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 9cf27e56827a1..ede0e10e03466 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 58a1b23d17bf6..90ac7e90e7295 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 c0ef8b5f30bd4..f9697d61f1b27 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 99e578f8adae6..4bf239fca8f90 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 043d8a56d0c63..1ed12c54c5752 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 0000000000000..ba0570dbf97d2 --- /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 499239500acce..889c8bc6f57cf 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";