Authentication Servers

The whole point of an authentication service is that it allows the client to prove itself to be trustworthy, or at least to prove itself to be the same nefarious character it claims.

Infrastructures.org

I want to make our existing Active Directory the source for all the following:

  • Lists of users allowed to log into the managed infrastructure
  • UIDs and GIDs of those users
  • Passwords of those users

But I’m striking out on everything except the passwords themselves, and I’ve had that part working for years. So no new progress on that front, despite reading tons of LDAP and NSS howtos, bug reports, etc. So instead, let me lay out our planned implementation and workflow:

  1. Windows administrator makes a new user account in Active Directory.
  2. Windows administrator (or sometimes me) logs into the main file server, and runs an nearly-unmodified adduser script that creates the user’s Unix UID, home directory, and registers the user on either our student mailing list or our faculty/staff mailing list. This script currently has a big ‘ssh in a for loop’ part for adding accounts on the client systems, but this will be removed.
  3. A cron job on the file server runs every two minutes (even-numbered ones) and puts all the usernames, UIDs, and GIDs into a protected file accessible via NFS export. This takes only a fraction of a second to run.
  4. A cron job on the rest of the Linux systems runs every two minutes (odd-numbered ones — good thing I got time synchronization working earlier, right?), reads the protected file and the contents of our main home directory NFS area, and determines which users should be added and deleted on its local passwd and shadow files. Even on a freshly-reinstalled system with no domain users added, this takes seconds to run.
  5. Whenever a user gets deleted via our deluser script, the rest of the Linux systems will pick up on the deletion by the fact that their home directory will be owned by root and inaccessible to all other users.

You may have noticed that there’s nothing in any of this about the user’s actual password. It’s only stored on the domain controllers, never on any of the Linux systems. We use Kerberos and PAM to directly authenticate the users against the Active Directory servers. Code and configurations for this setup after the jump.

The main adduser.cae script on the file server:

for user in $*; do
/usr/sbin/adduser --debug
  --home /home/CAE/$user
  --ingroup users
  --disabled-password
  --gecos " "
  --shell /usr/bin/scponly
  $user

This script calls adduser.local:

#!/bin/bash
NEW_USERNAME=$1
NEW_UID=$2
NEW_GID=$3
NEW_HOME=$4
# Figure out which CAE Lab mailing list to add them to: students or
# faculty
CLASSIFICATION=$(ldapclassification ${NEW_USERNAME})
case $CLASSIFICATION in
  "Undergraduate"|"Graduate")
    echo "${NEW_USERNAME} is a student"
    LISTNAME="cae-students";;
  "Faculty"|"Staff")
    echo "${NEW_USERNAME} is faculty/staff"
    LISTNAME="cae-faculty";;
  *)
    echo "Assuming ${NEW_USERNAME} is faculty/staff"
    LISTNAME="cae-faculty";;
esac
echo "${NEW_USERNAME}@tntech.edu" | /usr/sbin/add_members -n - -w n $LISTNAME
# Give the user a real name in the gecos field
REALNAME=$(ldapname ${NEW_USERNAME})
chfn -f "${REALNAME}" $1
chmod 711 ${NEW_HOME}
chown -R ${NEW_USERNAME} ${NEW_HOME}
chgrp -R ${NEW_GID} ${NEW_HOME}

where ldapname and ldapclassification are just wrappers around ldapsearch that query the campus Active Directory and determine the user’s real name and student/faculty/staff classification based off Windows groups they’re members of in that domain.

ldapname:

#!/bin/bash
LDAPACCT=REDACTED
LDAPPASSWD=REDACTED_TOO

PCLABSERVER=REDACTED_MORE
PCLABBASEDN="dc=pclab,dc=tntech,dc=edu"
PCLABDN="CN=${LDAPACCT},CN=Users,${PCLABBASEDN}"
TTUSERVER=MOSTLY_REDACTED
TTUBASEDN="dc=tntech,dc=edu"
TTUDN="CN=${LDAPACCT},CN=Users,${TTUBASEDN}"

USERNAME=$1

PCLABREALNAME=$(ldapsearch -x -LLL
    -D ${PCLABDN}
    -w ${LDAPPASSWD}
    -h ${PCLABSERVER}
    -b ${PCLABBASEDN}
    "(CN=$USERNAME)"
    displayname | grep displayName)
PCLABREALNAME=$(echo $PCLABREALNAME | cut -d  -f2-)

if [ -z "$PCLABREALNAME" ]; then
    # Check TTU Domain if PCLAB domain fails
    TTUREALNAME=$(ldapsearch -x -LLL
        -D ${TTUDN}
        -w ${LDAPPASSWD}
        -h ${TTUSERVER}
        -b ${TTUBASEDN}
        "(CN=$USERNAME)"
        displayname | grep displayName)
    TTUREALNAME=$(echo $TTUREALNAME | cut -d  -f2-)
fi

if [ -z "$PCLABREALNAME" -a -z "$TTUREALNAME" ]; then
    echo "Can't find a PCLAB or TTU domain entry for $USERNAME."
    exit 1
elif [ ! -z "$PCLABREALNAME" ]; then
    REALNAME="${PCLABREALNAME}"
elif [ ! -z "$TTUREALNAME" ]; then
    REALNAME="${TTUREALNAME}"
fi
echo "${REALNAME}"

ldapclassification

#!/bin/bash
LDAPACCT=REDACTED
LDAPPASSWD=REDACTEDLY_CORRECT

PCLABSERVER=EXTRA_REDACTION
PCLABBASEDN="dc=pclab,dc=tntech,dc=edu"
PCLABDN="CN=${LDAPACCT},CN=Users,${PCLABBASEDN}"
TTUSERVER=DOUBLE_SECRET_REDACTION
TTUBASEDN="dc=tntech,dc=edu"
TTUDN="CN=${LDAPACCT},CN=Users,${TTUBASEDN}"

USERNAME=$1

PCLABREALNAME=$(ldapsearch -x -LLL
    -D ${PCLABDN}
    -w ${LDAPPASSWD}
    -h ${PCLABSERVER}
    -b ${PCLABBASEDN}
    "(CN=$USERNAME)"
    displayname | grep displayName)

PCLABREALNAME=$(echo $PCLABREALNAME | cut -d  -f2-)

if [ -z "$PCLABREALNAME" ]; then
    # Check TTU Domain if PCLAB domain fails
    TTUREALNAME=$(ldapsearch -x -LLL
        -D ${TTUDN}
        -w ${LDAPPASSWD}
        -h ${TTUSERVER}
        -b ${TTUBASEDN}
        "(CN=$USERNAME)"
        displayname | grep displayName)

    TTUREALNAME=$(echo $TTUREALNAME | cut -d  -f2-)
fi

if [ -z "$PCLABREALNAME" -a -z "$TTUREALNAME" ]; then
    echo "Can't find a PCLAB or TTU domain entry for $USERNAME."
    exit 1
elif [ ! -z "$PCLABREALNAME" ]; then
    CLASSIFICATION=$(ldapsearch -x -LLL
        -D ${PCLABDN}
        -w ${LDAPPASSWD}
        -h ${PCLABSERVER}
        -b ${PCLABBASEDN}
        "(CN=$USERNAME)"
        memberOf |
        grep memberOf | egrep -i 'graduate|faculty|staff' | tail -1 | cut -d, -f1 | cut -d= -f2)
    REALNAME="${PCLABREALNAME}"
elif [ ! -z "$TTUREALNAME" ]; then
    CLASSIFICATION=$(ldapsearch -x -LLL
        -D ${TTUDN}
        -w ${LDAPPASSWD}
        -h ${TTUSERVER}
        -b ${TTUBASEDN}
        "(CN=$USERNAME)"
        memberOf |
        grep memberOf | egrep -i 'gradudate|faculty|staff' | tail -1 | cut -d, -
f1 | cut -d= -f2)
    REALNAME="${TTUREALNAME}"
fi
echo "${CLASSIFICATION}"

On the file server, the following command is run every two minutes to generate the list of UIDs: grep /home/CAE /etc/passwd | cut -d: -f1,3,4 | sort > /home/CAE/root/current-username-uid-gid.txt

syncusers, run every two minutes on clients:

#!/bin/bash

#DEBUG=echo

case `uname` in
    Linux)
        # Delete overdue expired users (so overdue that we've already killed
        # their home directories)
        for USERNAME in `getent passwd | grep /home/CAE | cut -d: -f1`; do
            if [ ! -d /home/CAE/$USERNAME ] ; then
            # User has no home directory, which means they've been deleted
            # (since NFS home has already been mounted to run this script).
                $DEBUG /usr/sbin/userdel $USERNAME
            fi
        done

        # Delete recently-expired users
        cd /home/CAE
        for USERNAME in `ls -l | egrep '^drwx------.* root ' | awk '{print $NF}'`; do
            grep "^$USERNAME:" /etc/passwd > /dev/null
            if [ $? -eq 0 ]; then
            # User exists, delete them
                $DEBUG /usr/sbin/userdel $USERNAME
            else
                # User already deleted, do nothing
                :
            fi
        done

        # Add new users
        if [ -f /home/CAE/root/current-username-uid-gid.txt ]; then
            for USERNAME_UID_GID in `cat /home/CAE/root/current-username-uid-gid.txt`; do
                NEW_USERNAME=`echo $USERNAME_UID_GID | cut -d: -f1`
                NEW_UID=`echo $USERNAME_UID_GID | cut -d: -f2`
                NEW_GID=`echo $USERNAME_UID_GID | cut -d: -f3`
                NEW_HOME=/home/CAE/$NEW_USERNAME
                grep "^$NEW_USERNAME:" /etc/passwd > /dev/null
                if [ $? -eq 0 ]; then
                # User exists, do nothing
                    continue
                fi
                $DEBUG /usr/sbin/useradd -d $NEW_HOME -g $NEW_GID
                    -s /bin/bash -p '*NP*' -u $NEW_UID $NEW_USERNAME
            done
        fi
        ;;
    *)
        echo "Syncusers not yet supported on `uname`"
        exit 1
        ;;
esac

And deluser.cae:

#!/bin/bash
for USERNAME in $*; do
echo "User: $USERNAME"

# Remove user from lab mailing lists
echo -n "- Removing from mailing lists: "
for LIST in cae-students cae-faculty
do
  echo -n "$LIST "
  /usr/sbin/remove_members $LIST $USERNAME@tntech.edu
done
echo

# Protect user files for end-of-semester backup purposes
/bin/chown -R root.root /home/CAE/$USERNAME
/bin/chmod -R go-rwx /home/CAE/$USERNAME

/usr/sbin/userdel $USERNAME

done

Kerberos stuff: installed libpam-krb5, krb5-config, and krb5-user from Debian packages. Entered names of Windows domain controllers when prompted for KDCs. Entered name of Active Directory master server when prompted for the administrative server. Edited /etc/pam.d/common-auth as follows:

auth    sufficient      pam_unix.so nullok_secure
auth    required        pam_krb5.so use_first_pass

As long as the clocks are synchronized between the Linux client and the Windows domain controllers, this is all it takes. Only users listed in the local passwd files are allowed access, and we authenticate them from the domain controllers. No changes to the Windows side required at all.

Update: oh, winbind, how I love you. Last time I looked at winbind (circa 2001), it either really sucked, or else I horribly misconfigured it. In particularly, I had a problem where each Unix system would generate its own UIDs dynamically, and none of them would match. This made NFS+winbind impossible. Now with winbind generating UIDs from the Active Directory RIDs, everything matches, and I can make it the centerpiece of my authentication setup.

Join the Conversation

7 Comments

  1. You could probably accomplish this with fewer moving parts using an OpenLDAP Proxy (our CDS product) and name-service-switch (NSS) and pluggable-authentication-module (PAM) like our CNS product. Fewer moving parts, probably easier to set up.

  2. Mike, it seems like an awful lot of local modifications taking place. I can off-hand think of two solutions that would make things a lot easier for you:

    a) Direct look-up in the AD using NSS, or

    b) Set up a slave LDAP server that replicates from the AD and then NSS look-up against that using NSS.

    /peter

  3. I fought a long way through option (a) there. ldapsearch worked fine, but nss just wouldn’t cooperate. The errors I was running into were not unique, but I couldn’t find anyone with working solutions, or else I just mis-implemented them.

    Option (b) honestly never occurred to me, but it sounds promising. I may yet do that on my next round of work.

Leave a comment

Your email address will not be published. Required fields are marked *