Project

General

Profile

Feature #12711

Cannot use smb/server with LDAP authentication

Added by Jorge Schrauwen 6 months ago. Updated 6 months ago.

Status:
New
Priority:
Low
Assignee:
-
Category:
cifs - CIFS server and client
Start date:
Due date:
% Done:

0%

Estimated time:
Difficulty:
Hard
Tags:
Gerrit CR:

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.

#1

Updated by Jorge Schrauwen 6 months 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.

#2

Updated by Jorge Schrauwen 6 months ago

To summarize my long rambling post a bit:
  • (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.

#3

Updated by Jorge Schrauwen 6 months 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.

Also available in: Atom PDF