PAM – How to Create a Conditional PAM Entry

linuxpamssh

I am testing PAM scenarios on RedHat 7.5

The pam module pam_succeed_if.so looks like the most basic level of conditional testing that PAM has to offer, and it is not meeting my needs.
You can only create tests on the user, uid, gid, shell, home, ruser, rhost, tty, and service fields.

In my situation, I do want to test based off the 'rhost' field, however after putting the module into debug, I saw that the rhost field was not set.

My goal is to only run a PAM module in /etc/pam.d/sudo if the user is logged in locally to the machine. If we can detect that the user is logged in through SSH, then I want to skip the module. I had actually come up with 3 different ideas that I thought would work, but all ended up failing.

I'll share a couple of the solutions that ended up failing

Conditional entry using pam_exec.so

I wanted to add the following pam entry on top of the pam module that I want to conditionally skip:

auth    [success=ok default=1]    pam_exec.so /etc/security/deny-ssh-user.sh

The contents of /etc/security/deny-ssh-user.sh

#!/bin/bash
# Returns 1 if the user is logged in through SSH
# Returns 0 if the user is not logged in through SSH
SSH_SESSION=false
if [ -n "${SSH_CLIENT}" ] || [ -n "${SSH_TTY}" ] || [ -n "${SSH_CONNECTION}" ]; then
    SSH_SESSION=true
else
    case $(ps -o comm= -p $PPID) in
        sshd|*/sshd) SSH_SESSION=true;;
    esac
fi

if "${SSH_SESSION}"; then
    exit 1
else
    exit 0
fi

I've gone through the source code for pam_exec.so at https://github.com/linux-pam/linux-pam/blob/master/modules/pam_exec/pam_exec.c and astonishingly, it will ALWAYS return PAM_SUCCESS, regardless of the exit code of the script. And I can't get the script to cause the pam_exec module to return PAM_SERVICE_ERR, PAM_SYSTEM_ERR, or PAM_IGNORE.

Conditional entry using pam_access.so

Again, I add the following pam entry on top of the pam module that I want to conditionally skip

auth    [success=ok perm_denied=1]    pam_access.so    accessfile=/etc/security/ssh-sudo-access.conf noaudit

The contents of /etc/security/ssh-sudo-access.conf

+:ALL:localhost 127.0.0.1 
-:ALL:ALL

Wow, super clean right? It will return success if you are logged in locally, and deny everything else. Well no. It turns out when the pam_access.so module is put into debug, it has no knowledge of remote hosts, only the terminal that is being used. So the pam_access can't actually block access by remote hosts.

It has been an infuriating day of literally reading source code just to find out what black magic I have to cast just so I can skip a PAM module.

Best Answer

Here is a solution that works for me. My /etc/pam.d/sudo:

#%PAM-1.0
auth            [success=1]     pam_exec.so    /tmp/test-pam
auth            required        pam_deny.so
auth            include         system-auth
account         include         system-auth
session         include         system-auth

And /tmp/test-pam:

#! /bin/bash
/bin/last -i -p now ${PAM_TTY#/dev/} | \
    /bin/awk 'NR==1 { if ($3 != "0.0.0.0") exit 9; exit 0; }'

I get this behavior:

$ sudo date
[sudo] password for jdoe:
Thu Jun 28 23:51:58 MDT 2018
$ ssh localhost
Last login: Thu Jun 28 23:40:23 2018 from ::1
valli$ sudo date
/tmp/test-pam failed: exit code 9
[sudo] password for jdoe:
sudo: PAM authentication error: System error
valli$

The first line added to the default pam.d/sudo calls pam_exec and, if it succeeds, skips the next entry. The second line just denies access unconditionally.

In /tmp/test-pam I call last to get the IP address associated with the TTY pam was invoked from. ${PAM_TTY#/dev/} removes /dev/ from the front of the value, because last doesn't recognize the full device path. The -i flag makes last show either the IP address or the placeholder 0.0.0.0 if there is no IP address; by default it shows an info string which is much harder to check. This is also why I used last instead of who or w; those don't have a similar option. The -p now option isn't strictly necessary, as we'll see awk is only checking the first line of output, but it restricts last to show only users who are presently logged in.

The awk command just checks the first line, and if the third field isn't 0.0.0.0 it exits with an error. Since this is the last command in /tmp/test-pam, awk's exit code becomes the exit code for the script.

On my system, none of the tests you were trying in your deny-ssh-user.sh would work. If you put env > /tmp/test-pam.log at the top of your script, you'll see that the environment has been stripped, so none of your SSH_FOO variables will be set. And $PPID could point to any number of processes. For example, run perl -e 'system("sudo cat /etc/passwd")' and see that $PPID refers to the perl process.

This is Arch Linux, kernel 4.16.11-1-ARCH, in case it matters. I don't think it should, though.

Related Question