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