Feature #12711
openCannot use smb/server with LDAP authentication
0%
Description
Currently it is not possible to use the smb/server with ldap authentication.
To populate /var/smb/smbpasswd you need to enable pam_smb_passwd which will ignore password changes for accounts that are not provided by files.
This seems to be enforced here (http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/pam_modules/smb/smb_passwd.c#128) in the source.
Ideally I guess smb/server would have a way of quering LDAP for either lmhash and/or nthashes or use userPassword
if the stored passwords are not hashed. (Going kerberos is the only option that seems to be viable, but I'd prefer not to touch kerberos just for smb/server).
My userPassword's are currently unhashed for freeradius and some other software but they are only usable for bindings and the proxy account used by freeradius.
Based on this I currently have a very very ugly workaround:
#!/bin/bash # # This use smbencrypt from freeradius # # read all accounts from ldap ldapload() { local acc_uid= local acc_name= local acc_pass= ldapsearch -LLL -x -Z -H ldapi:/// -W \ -D cn=admin,dc=acheron,dc=be \ -b ou=accounts,dc=acheron,dc=be \ -s one objectClass=posixAccount \ uid uidNumber userPassword | while read line; do if [[ -z "$line" && -n "$acc_pass" && -n "$acc_uid" && -n "$acc_name" ]]; then acc2smbpasswd "$acc_name" "$acc_uid" "$acc_pass" fi if [[ "$line" =~ ^dn: ]]; then acc_uid= acc_name= acc_pass= fi if [[ "$line" =~ ^uid: ]]; then acc_name="$(echo $line | awk -F ': ' '{ print $2 }')" fi if [[ "$line" =~ ^uidNumber: ]]; then acc_uid="$(echo $line | awk -F ': ' '{ print $2 }')" fi if [[ "$line" =~ ^userPassword:: ]]; then acc_pass="$(echo $line | awk -F ':: ' '{ print $2 }' | base64 -d)" fi done } # convert ldap info to smbpasswd entry acc2smbpasswd() { local account="${1:-}" local uid="${2:-}" local password="${3:-}" local nthash="$(smbencrypt "$password" 2>&1 | tail -n 1 | awk '{ print $2 }')" local rnthash= for s in $(seq 0 2 $((${#nthash} - 2))); do rnthash="${rnthash}$(echo ${nthash:${s}:2} | rev)" done echo "$account:$uid::$rnthash" } ## main tmp_smbpasswd="$(mktemp)" ldapload >> $tmp_smbpasswd chown root:sys $tmp_smbpasswd chmod 0400 $tmp_smbpasswd [ ! -e "$tmp_smbpasswd" ] || mv "$tmp_smbpasswd" /var/smb/smbpasswd
Sadly I also noticed the nthash field in /var/smb/smnpasswd has the values flipped compare to other software like samba, freeradius, ... which was the biggest hurdle in getting this ugly thing to work. It now syncs from ldap every hours. It's not perfect, but it's workable.
Perhaps I could extend pam_smb_passwd with an extra argument to relax the constraint on requiring user to be provided by files
, as it seems to work fine if they are available via ldap and have a nthash in /var/smb/smbpasswd. That way I could just loop over the accounts (and maybe even filter them more) and just called passwd
for the username, that would ensure the updates to smbpasswd happen through the correct codepaths instead of playing switcheroo like my current workaround does.
Looking at the code, that seems like something I could probably do. But after mulling it over, I'm not really sure that is even something that should be done. It's better than my current workaround for sure. But it's still really ugly.
Updated by Jorge Schrauwen about 2 years ago
I've tagged gwr as he knows the smb code very well and jbk as he helped me with a pam change before and also has worked on the ldap code recently.
Updated by Jorge Schrauwen about 2 years ago
- (very bad - current) ugly cron that does a switcheroo (depends on having userPassword unhashed in ldap)
- (bad) relax pam_smb_passwd restrictions -> use smb code for updating smbpasswd, proper locking, ... but still not really great. But something I can do)
- (good) have smb code optionally talk to ldap and use a unhashed userPassword (way outside of my skill set, requires no schema extensions)
- (best) have smb code optionally talk to ldap and use a new attribute that stores nthash or lmhash in ldap (way outside of my skill set, require schema extension... which brings fun things like getting an OID space, creating a new objectClass and attributes...)
The last option would probably also allow for storing things like a SID in ldap, which would be nice to have idmap and other bits the same over multiple hosts.
Updated by Jorge Schrauwen about 2 years ago
#!/opt/local/bin/python3 import os import sys import ldap3 import hashlib import binascii import argparse from getpass import getpass from pwd import getpwnam from grp import getgrnam SMBPASSWD_PATH="/var/smb/smbpasswd" SMBPASSWD_MODE=0o400 SMBPASSWD_UID="root" SMBPASSWD_GID="sys" def nthash(password, illumos=False): nthash = binascii.hexlify( hashlib.new( 'md4', password.encode('utf-16le') ).digest() ).upper().decode('utf-8') if illumos: nthash_bs = "" for step in range(0, len(nthash), 2): nthash_bs += nthash[step:(step+2)][::-1] return nthash_bs return nthash if __name__ == "__main__": ## parse arguments parser = argparse.ArgumentParser() parser.add_argument("-D", help="bind DN", metavar="binddn", dest="binddn", required=True, type=str) parser.add_argument("-H", help="LDAP Uniform Resource Identifier", metavar="URI", dest="uri", required=True, type=str) parser.add_argument("-b", help="base dn for search", metavar="basedn", dest="basedn", required=True, type=str) parser.add_argument("-w", help="bind password", metavar="passwd", dest="password", type=str) parser.add_argument("-W", help="prompt for bind password", dest="prompt_password", action="store_true") args = parser.parse_args() if args.prompt_password: args.password = getpass("Enter LDAP Password: ") if not args.password: print( "Please specify password using -w or use -W to prompt for password.", file=sys.stderr, ) sys.exit(1) ## sync ldap to smbpasswd ldapsrv = ldap3.Server(args.uri) with ldap3.Connection(ldapsrv, auto_bind=False, user=args.binddn, password=args.password) as conn: conn.start_tls() conn.bind() conn.search( search_base=args.basedn, search_scope=ldap3.SUBTREE, search_filter="(objectClass=posixAccount)", attributes=['uid', 'uidNumber', 'userPassword'], ) fd_path = "{}.ldap".format(SMBPASSWD_PATH) with open(fd_path, "w") as fd: os.chmod(fd_path, SMBPASSWD_MODE) os.chown(fd_path, getpwnam(SMBPASSWD_UID).pw_uid, getgrnam(SMBPASSWD_GID).gr_gid) for account in conn.entries: print("{user}:{uid}:{lmhash}:{nthash}".format( user=account.uid.value, uid=account.uidNumber.value, lmhash="", nthash=nthash(account.userPassword.value.decode('utf-8'), True), ), file=fd) if os.path.exists(fd_path): os.replace(fd_path, SMBPASSWD_PATH)
Rewrite of the ugly shell script in python, at least it is readable now.