How to create a conditional PAM entry

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.


Well it turns out I'm actually an idiot, the pam_exec.so module is perfectly fine for creating PAM conditionals.

Tim Smith was correct in assessing that both tests in my /etc/security/deny-ssh-user.sh script were NEVER setting the variable SSH_SESSION to true. I didn't take that into consideration because the script works in a normal shell, but the environment context is stripped when executed by pam_exec.so.

I ended up rewriting the script to use the last utility just like his example, however I had to change some of it because the switches for last differ from Arch Linux to RedHat.

Here is the revised script at /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

function isSshSession {
    local terminal="${1}"
    if $(/usr/bin/last -i | 
        /usr/bin/grep "${terminal}" |
        /usr/bin/grep 'still logged in' |
        /usr/bin/awk '{print $3}' |
        /usr/bin/grep -q --invert-match '0\.0\.0\.0'); then
        echo true
    else
        echo false
    fi
}

function stripTerminal {
    local terminal="${1}"

    # PAM_TTY is in the form /dev/pts/X
    # Last utility displays TTY in the form pts/x
    # Returns the first five characters stripped from TTY
    echo "${terminal:5}"
}

lastTerminal=$( stripTerminal "${PAM_TTY}")
SSH_SESSION=$(isSshSession "${lastTerminal}")

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

Contents of /etc/pam.d/sudo

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

Tags:

Linux

Ssh

Pam