Fun with PAM scripts

Recently I was given the requirement to help build a data exporter tool that would fit with a customer’s existing process which uses SFTP (no comment). Rather than get into the nasty business of managing a whole separate set of credentials, we thought it would be handy if we could get the regular OpenSSH SFTP daemon to authenticate against our existing authentication platform.

By default SFTP uses the UNIX PAM (Pluggable Authentication Module) authentication system, so as a proof of concept I decided to see what the simplest possible PAM setup I could come up with that would actually work.

It had to:

  • Allow the user to log in if the username and password were valid
  • Deny access if the username and password were invalid
  • Create a new account (by way of useradd) so that users would have a valid home directory once they were allowed in

I decided to run the test using login because that is the simplest auth process that I’m aware of on Linux (you can basically run it on the command line and see results immediately).

Here’s the /etc/pam.d/login file that I ended up with, stripped of every unnecessary functional line but retaining all important:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# The PAM configuration file for the Shadow `login' service
#

# Our "Special Script"
auth sufficient pam_exec.so expose_authtok debug log=/tmp/debug.log /usr/sbin/pam_verify

# here are the per-package modules (the "Primary" block)
auth    [success=1 default=ignore]  pam_unix.so nullok_secure
# here's the fallback if no module succeeds
auth    requisite           pam_deny.so
# prime the stack with a positive return value if there isn't one already;
# this avoids us returning an error just because nothing sets a success code
# since the modules above will each just jump around
auth    required            pam_permit.so

And here’s /usr/sbin/pam_verify:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash

# The password comes in through stdin
PAM_PASSWORD=`cat -`
echo "User: ${PAM_USER}; Password: ${PAM_PASSWORD}; Type: ${PAM_TYPE}"

if [[ "${PAM_PASSWORD}" != "pw" ]]; then
  echo "Incorrect password"
  exit 1
fi

# We create an account if the home directory doesn't already exist
if [[ "${PAM_TYPE}" == "open_session" ]] && [ ! -d "/home/${PAM_USER}" ]; then
  useradd -m ${PAM_USER}
fi

Obviously you should definitely NOT do this on any machine that’s exposed to the Internet in any way.

Caveats

Actually, this ended up being the biggest problem…

It was easy enough to get a simple service like login to defer to my script for all of its authentication requirements, but it turns out that the more-complicated (and ultimately more important) OpenSSH actually does two kinds of authentication on every connection:

  1. NSS - To verify that the user exists, get their user ID, and locate their home directory, and
  2. PAM - To verify the user’s password

Unfortunately there doesn’t appear to be any way to completely skip the NSS phrase, and I was unable to find a similarly open-ended module for libnss that would simply pass a username to an external program and wait for a response, and I wasn’t exactly comfortable writing a module myself given my extremely limited experience writing libraries in C.

Comments