Unix Single Sign On using Samba Winbind and Microsoft Active Directory



Intro

There is plenty of documentation on the web about this subject with some of it more useful than others as there is also plenty of ways to set this up. In this document I show you how to setup Unix Single Sign On using static Unix UIDs and GIDs in multiple Windows domains.

In this example I will have a Unix machine that lives in a DMZ network and also has login access from users that live in an internal CORP network. This assumes there will be Windows Domain Controllers for both networks with a trust relationship between them (often one way).

This document is current at the time of writing (March 2013) and assumes some basic Windows and Unix sysadmin knowledge. It has been successfully implemented in a commercial environment running CentOS 6.3, Windows Server 2008R2 and Samba 3.x.


Windows: Extending the Active Directory schema

Samba's Winbind tool supports many ways for generating UIDs and GIDs for a Unix account other than using some password text file. Some sysadmins prefer not to fiddle with Active Directory and configure Samba Winbind to use the Windows SID (System Identifier) or Windows RID (Relative Identifier) of a Windows user's account and convert that into a UID or GID. I've found this approach to be often short lived as RID generated UIDs cause clashes with other users in multi Windows domain environments, and SID generated UIDs are effectively randomly generated and cached on the first Unix account login meaning UIDs and GIDs could be different across Unix hosts, and if the Samba Winbind cache gets cleared you will probably be assigned another random UID/GID and no longer have access to your own Unix files.

What you want is ultimate control of your UIDs and GIDs assignments, and that they remain static across all Unix hosts unaffected by any Samba Winbind cache clearing. This implies that your UIDs and GIDs assignments are stored somewhere centrally. The best place for that is within Active Directory itself along with the account names and passwords.

To extend the Active Directory schema you need to install the NIS extension for Active Directory. This is a Microsoft product and integrates well with Active Directory giving you an extra tab in both the user properties screen and security group properties screen where you can add in NIS style attributes like UID and GID to a Microsoft Windows account. Being Microsoft there are a lot of old and/or similar implementations of NIS for Active Directory that are garbage, have a dig around on Microsoft Technet to find the latest incarnation of this solution.
CAUTION: Microsoft engineers tell me that when an Active Directory schema is extended via plugins (e.g MS-Exchange, NIS, etc) the extension cannot be deleted, that is they remain forever. I'm sure this is not strictly true but be wary of this point nonetheless.

Follow Microsoft's instructions for installing the NIS extension for Active Directory (including making a backup) but do not run the NIS services, NIS is deemed to be insecure and we do not want any NIS services active so disable them from starting up. All we want to do is extend the Active Directory schema with extra NIS fields. Samba Winbind will be performing LDAP queries on Active Directory to get the static UIDs and GIDs for Unix logins and has no need to use NIS protocols so ensure they are never run. In this setup Samba Windbind will not be using LDAP for password handling (which is also insecure unless you use it over SSH) but will use the very secure Kerberos method instead.

The Active Directory service needs to be restarted for the new schema fields to become visible.


Windows: Create a Security Group for Unix users

This is a bug bear of this kind of setup. For Single Sign On to work with Active Directory the Windows user's Primary Group must be a Windows Security Group with a NIS GID associated with it. Don't mess with the "Domain Users" group which is often left as the default Primary Group by Windows sysadmins, instead create a new Security Group (say unix_users) and assign a NIS GID to it. Unlike Windows sysadmins, do not put any spaces, mixed case or non-ASCII characters in this group name, things like sudo, cron and even old version of Samba Winbind can fail because of it.

The NIS GID value you choose must fall within the idmap range you specify in your Samba Winbind client configuration. A current limitation of Samba Winbind 3.x is that the idmap range applies to both UID and GID values which is inconvenient.


Windows: Update user account details

Select an ordinary user, add them to a Windows Security Group that has a NIS GID associated with it then make this group the user's Primary Group for Windows. Assign a unique NIS UID to this user and set the default group. Depending on how you configure the Samba Winbind client you may not need to worry about filling in the rest of the fields (home directory and shell) which will then assume the Samba Winbind defaults.

The NIS UID value you choose must fall within the idmap range you specify in your Samba Winbind client configuration. A current limitation of Samba Winbind 3.x is that the idmap range applies to both UID and GID values which is inconvenient.


Unix: Automounted user home directories

I won't discuss this here but you should at least be using automounted user home directories on your Unix systems.


Unix: Installing Samba Winbind client

Install a recent version of Samba Windbind 3.x client satisfying all dependencies. The older versions of Samba Winbind 3.x depended on an older version of the tdb database system which will give you headaches.

Configure smb.conf (usually /etc/samba/smb.conf) as follows:

[global]
# General Name Options
workgroup = DMZ
realm = DMZ.MYCOMPANY.COM
name resolve order = bcast host
local master = no
domain master = no
preferred master = no
dns proxy = no

# Restrict for internal access only.
interfaces = eth0
bind interfaces only = yes

# Block out Windows domains that do not participate in SSO.
# The logic is a bit flawed here, if you watch the Winbind logs you will notice that it
# iterates through a list of discovered Windows domains for every query, blocking out the
# unwanted domains speeds things up. They should have written an allow-only clause instead.
winbind: ignore domains = TEST TEST2

password server = dc1.dmz.mycompany.com dc2.dmz.mycompany.com *

server string = Samba %v (%h)
security = ADS
kerberos method = secrets and keytab
idmap cache time = 3600
# We are not using writable backend, and there is no way to disable it other than using a limited range.
idmap backend = tdb
idmap uid = 999998-999999
idmap gid = 999998-999999
# IDMAP for DMZ domain
idmap config DMZ : backend = ad
idmap config DMZ : range = 5000-5999
idmap config DMZ : schema_mode = rfc2307
# IDMAP for CORP domain (user must use CORP\\<username> to log in)
idmap config CORP : backend = ad
idmap config CORP : range = 1000-4999
idmap config CORP : schema_mode = rfc2307

# This is the default anyway, setting it may complain for some samba versions.
#winbind separator = \
# Enumeration adds performance penalty, but enables getent command
winbind enum users = yes
winbind enum groups = yes
winbind use default domain = yes
winbind nested groups = yes
winbind expand groups = 3
# Set this if you are using the Active Directory schema template values, otherwise
# comment out and specify a standard shell and homedir for this host instead.
#winbind nss info = template rfc2307
template shell = /bin/bash
template homedir = /home/%D/%U
# Cache the connections to improve performance
winbind offline logon = true
winbind cache time = 3600

# Charsets
dos charset = ASCII
unix charset = LOCALE
display charset = UTF8

# Logging
log level = 1 winbind:0
log file = /var/log/samba/log.%m



Unix: Installing Kerberos client

We only need to run the client so do not install a Kerberos server. There are two flavours of Kerberos available: MIT restricted to USA, and Heimdal for everyone else.

Install a recent version of the Kerberos client satisfying all dependencies. Older versions of the Kerberos client could not operate in a multi Windows domain environment due to an internal bug.

A large number of configuration directives are not implemented in the Heimdal version making the man page painful.

Ensure the clock on your Unix host is within 5 minutes of the clock on your Windows Domain Controllers otherwise Kerberos will fail all login attempts. You should install the Network Time Protocol (NTP) daemon on all your hosts to keep your computer clocks in sync to avoid this problem.

Configure krb5.conf (usually /etc/krb5.conf) as follows:

[logging]
 default = FILE:/var/log/krb5libs.log
 kdc = FILE:/var/log/krb5kdc.log
 admin_server = FILE:/var/log/kadmind.log

[libdefaults]
 default_realm = DMZ.MYCOMPANY.COM
 dns_lookup_realm = true
 dns_lookup_kdc = true
 ticket_lifetime = 24h
 forwardable = yes

[realms]
 DMZ.MYCOMPANY.COM = {
   kdc = dc1.dmz.mycompany.com
   kdc = dc2.dmz.mycompany.com
   admin_server = dc1.dmz.mycompany.com
   admin_server = dc2.dmz.mycompany.com
   kpasswd_server = dc1.dmz.mycompany.com:464
   kpasswd_server = dc2.dmz.mycompany.com:464
   default_domain = dmz.mycompany.com
 }

[domain_realm]
 dmz.mycompany.com = DMZ.MYCOMPANY.COM
 .dmz.mycompany.com = DMZ.MYCOMPANY.COM

[appdefaults]
 pam = {
   debug = false
   ticket_lifetime = 36000
   renew_lifetime = 36000
   forwardable = true
   krb4_convert = false
 }



Unix: nsswitch.conf

Tell the Name Service Switch to also use Samba Winbind.

Configure specific entries in nsswitch.conf (usually /etc/nsswitch.conf) as follows:

passwd:     files winbind
shadow:     files winbind
group:      files winbind



Unix: Configuring PAM

There are many ways to configure PAM which is complicated further by the many differing implementation techniques used by different Unix systems. The goal however is the same and that is to load the pam_winbind.so library at the correct point. You may want to search the web or consult your own system's documentation on how to go about this on your own computer(s).

The following is what I used for a CentOS 6.x systems.

Configure pam_winbind.conf (usually /etc/security/pam_winbind.conf) as follows:

[global]
# turn on debugging
;debug = yes

# request a cached login if possible
# (needs "winbind offline logon = yes" in smb.conf)
cached_login = yes

# authenticate using kerberos
;krb5_auth = yes

# when using kerberos, request a "FILE" krb5 credential cache type
# (leave empty to just do krb5 authentication but not have a ticket
# afterwards)
;krb5_ccache_type = FILE

# make successful authentication dependend on membership of one SID
# (can also take a name)
;require_membership_of =

Configure password-auth-ac (usually /etc/pam.d/password-auth-ac) as follows:
NOTE: This file should be managed using the OS's native sysadmin tools.

#%PAM-1.0
auth        required      pam_env.so
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 500 quiet
auth        sufficient    pam_winbind.so cached_login use_first_pass
auth        required      pam_deny.so

account     required      pam_unix.so broken_shadow
account     sufficient    pam_localuser.so
account     sufficient    pam_succeed_if.so uid < 500 quiet
account     [default=bad success=ok user_unknown=ignore] pam_winbind.so cached_login
account     required      pam_permit.so

password    requisite     pam_cracklib.so try_first_pass retry=3 type=
password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password    sufficient    pam_winbind.so cached_login use_authtok
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so

Configure system-auth-ac (usually /etc/pam.d/system-auth-ac) as follows:
NOTE: This file should be managed using the OS's native sysadmin tools.

#%PAM-1.0
auth        required      pam_env.so
auth        sufficient    pam_unix.so nullok try_first_pass
auth        requisite     pam_succeed_if.so uid >= 500 quiet
auth        sufficient    pam_winbind.so cached_login use_first_pass
auth        required      pam_deny.so

account     required      pam_unix.so broken_shadow
account     sufficient    pam_localuser.so
account     sufficient    pam_succeed_if.so uid < 500 quiet
account     [default=bad success=ok user_unknown=ignore] pam_winbind.so cached_login
account     required      pam_permit.so

password    requisite     pam_cracklib.so try_first_pass retry=3 type=
password    sufficient    pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password    sufficient    pam_winbind.so cached_login use_authtok
password    required      pam_deny.so

session     optional      pam_keyinit.so revoke
session     required      pam_limits.so
session     [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session     required      pam_unix.so



Unix: Joining the Windows domain

It's best you login as root from the console until you get this right otherwise you may lock yourself out.

You need to know the Windows username and password of a privileged Windows user that has admin rights to the Windows domain to perform this step.

net ads join -U <privileged windows username>

You should see a short message about joining a domain, if not then you may have a configuration error. Some Samba Winbind implementations produce an error message about not being able to add a DNS record, depending on how you manage your DNS this can be safely ignored.

Test to see if the join to the Windows domain was successful.

net ads testjoin

If the join succeeded then you can start the Samba Winbind service.
Note that in our smb.conf file we are not running any samba shares therefore we do not need to start the CIFs service.

service nmb start
service winbind start
service winbind status

Test to see if you can see a Windows domain user with NIS attributes.

id <username>

or if in the CORP Windows domain.

id CORP\\<username>

Now try logging in to this Unix computer using that users Windows account credentials (Note: if you're still at the console then you may have this access restricted to root only).


Unix: /etc/group

Samba Winbind is mostly reliable but a bit temperamental and every now and then it gets itself in a knot. One of the common problems I witnessed was mainly caused by Windows sysadmin activity like rebooting the Windows Domain Controller which caused the loss of association between the GID and the group name. Now tools like cron, sudo and sshd are often configured using group privileges that refer to the group name and not the GID, for example most Unix admins often restrict sshd to only allow users in a specified group to log in e.g. unix_users. When the association between GID and group name is lost by Samba Winbind then these users can no longer log in.

The fix is simple, either restart Samba Winbind and perhaps even clear the cache which means logging into the console, or add the group name in /etc/group using the same GID you defined in Active Directory for that group. I recommend the latter as it will save you a lot of sysadmin headaches.

4500:*:unix_users

In sshd_config (usually /etc/sshd/sshd_config) I have a line as follows (Note the double entry to handle both cases).

AllowGroups unix_users CORP\unix_users

I do something similar for sudoers (usually /etc/sudoers.d/unix_users) I have a line as follows (Note the double entry to handle both cases).

%unix_users,+unix_users ALL = (build) /usr/bin/make



Unix: Clearing the Samba Winbind cache

This heavy handed technique was a hangover from when the tdb database system was buggy. I still use it (in a script) as it ensures a complete reset. The location of your tdb files and logs may be different on your system, but the principle remains the same. I used something similar on FreeBSD to achieve the same outcome.

rm -f /var/lib/samba/*db*
rm -f /var/log/samba/*
service winbind restart
id <username>



Unix: Useful stuff

Script to produce a report of Unix users in the current Active Directory domain. This is handy for finding new unique UIDs to assign to new users.
Must be run as root on a Unix host connected to a Windows domain.

#!/bin/sh
#
# BSD License for unix_users_in_ad.sh 
# Copyright (c) 2013, Arthur Gouros
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted provided that the following conditions are met:
# 
# - Redistributions of source code must retain the above copyright notice, 
#   this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright notice, 
#   this list of conditions and the following disclaimer in the documentation 
#   and/or other materials provided with the distribution.
# - Neither the name of Arthur Gouros nor the names of its contributors 
#   may be used to endorse or promote products derived from this software 
#   without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
# POSSIBILITY OF SUCH DAMAGE.
#
#
# Extract a list of Unix users out of Active Directory
#
#
# Author: Arthur Gouros 13/12/2011

# For posterity so I can find stuff in AD.
#
# get_uids()
# {
#   # UIDs
#   echo "Allocated UIDs"
#   /usr/bin/net ads search '(objectCategory=user)' uidNumber -P | sort | uniq
# }
# 
# get_gids()
# {
#   # GIDs
#   echo "Allocated GIDs"
#   /usr/bin/net ads search '(objectCategory=user)' gidNumber -P | sort | uniq
# }


FORMAT="text"

make_header()
{
  if test "${FORMAT}" = "html"
  then
    echo "<html>"
    echo "<head>"
    echo "<title>Unix Users in Active Directory (rfc2307)</title>"
    echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"../style/stylesheet.css\" />"
    echo "</head>"
    echo "<body>"
    echo "<div class=\"darkbg\">"
    echo "<center>"
    echo "<h1>Unix Users in Active Directory (rfc2307)</h1>"
    echo "<p>Report created: ${report_date}"
    echo "<br>"
    echo "</center>"
    echo "</div>"
    echo "<table>"
    echo "<tr class=\"darkbg\"><th>No</th><th>User Name</th><th>User Id</th><th>Group Id</th><th>Name</th><th>Created</th><th>Modified</th></tr>"
  else
    echo "Unix Users in Active Directory (rfc2307)"
    echo "Report created: ${report_date}"
    echo "User Name:User Id:Group Id:Name:Created:Modified"
    echo ""
  fi
}


make_footer()
{
  if test "${FORMAT}" = "html"
  then
    echo "</table>"
    echo "<div class=\"darkbg\">"
    echo "<center>"
    echo "<p>End-Of-Report"
    echo "</center>"
    echo "</div>"
    echo "</body>"
    echo "</html>"
  else
    echo ""
    echo "End-Of-Report"
  fi
}


get_user_attribute()
{
  arg_uid=$1
  arg_attribute=$2

  /usr/bin/net ads search '(uidNumber='${arg_uid}')' ${arg_attribute} -P | grep "${arg_attribute}: " | cut -f2- -d" "
}


make_date()
{
  arg_date=$1

  year=`echo ${arg_date} | cut -c-4`
  month=`echo ${arg_date} | cut -c5-6`
  day=`echo ${arg_date} | cut -c7-8`
  #hour=`echo ${arg_date} | cut -c9-10`
  #min=`echo ${arg_date} | cut -c11-12`

  #echo "${day}/${month}/${year} ${hour}:${min}"
  echo "${day}/${month}/${year}"
}

make_ad_passwd()
{
  i=1
  for uid in `/usr/bin/net ads search '(objectCategory=user)' uidNumber -P | sort | uniq | grep "uidNumber: " | cut -f2 -d" "`
  do
    if test -n "${uid}"
    then
      username=`get_user_attribute ${uid} uid`
      name=`get_user_attribute ${uid} name`
      gid=`get_user_attribute ${uid} gidNumber`
      user_created=`get_user_attribute ${uid} whenCreated`
      user_created=`make_date ${user_created}`
      user_changed=`get_user_attribute ${uid} whenChanged`
      user_changed=`make_date ${user_changed}`

      if test "${FORMAT}" = "html"
      then
        l_alt=`expr $i % 2`
        if test "$l_alt" = "1"
        then
          l="a"
        else
          l="b"
        fi
        echo "<tr class=\"$l\"><td>${i}</td><td>${username}</td><td>${uid}</td><td>${gid}</td><td>${name}</td><td>${user_created}</td><td>${user_changed}</td></tr>"
        i=`expr $i + 1`
      else
        echo "${username}:${uid}:${gid}:${name}:${user_created}:${user_changed}"
      fi
    fi
  done
}
  

########
## Main
########
if test "$1" = "--html"
then
  FORMAT="html"
fi
report_date="`date`"
make_header
make_ad_passwd
make_footer