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.
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:
- Windows administrator makes a new user account in Active Directory.
- 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.
- 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.
- 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.
- 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.
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.
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
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.