【问题标题】:Python Authentication: PAM or LDAP bind() vs. LDAP {SSHA} check?Python 身份验证:PAM 或 LDAP bind() 与 LDAP {SSHA} 检查?
【发布时间】:2015-03-29 11:34:34
【问题描述】:

我正在编写一个相当简单的内部应用程序(目前原型在 Bottle),它将事件和变更管理事件的通知发送到内部邮件列表,同时强制这些通知符合几个标准模板(并确保,通过Python Jira API 表明所需的问题引用存在并且处于适当的状态)。

当然,我们要求用户在我发送消息之前对我的应用程序进行身份验证,这些消息将归于他们。我们使用 LDAP,所有密码哈希都以 {SSHA} 格式存储。

我发现了至少三种不同的身份验证方式:

  • 使用具有足够 LDAP ACI 的服务帐户绑定到 LDAP 以获取密码哈希(与我们的 /etc/sssd/sssd.conf 系统级身份验证相同),使用它来查找 dn 并提取“userPassword”属性;然后使用 hashlib 验证建议的密码
  • 使用具有有限搜索权限的服务帐户绑定到 LDAP,使用该帐户查找用户的 dn,然后尝试使用该 dn 绑定到 LDAP,用户建议密码
  • 使用 simplepam(纯 Python/ctypes 包装器)或 python-pam 模块中的 authenticate() 函数

这里的代码似乎正确地实现了其中的第一个:

#!python
import hashlib
import ConfigParser, os
from base64 import encodestring as encode
from base64 import decodestring as decode

import ldap

config = ConfigParser.ConfigParser()
config.read(os.path.expanduser('~/.creds.ini'))
uid = config.get('LDAP', 'uid')
pwd = config.get('LDAP', 'pwd')
svr = config.get('LDAP', 'svr')
bdn = config.get('LDAP', 'bdn')

ld = ldap.initialize(svr)
ld.protocol_version = ldap.VERSION3
ld.simple_bind_s(uid, pwd)

def chk(prop, pw):
    pw=decode(pw[6:])         # Base64 decode after stripping off {SSHA}
    digest = pw[:20]          # Split digest/hash of PW from salt
    salt = pw[20:]            # Extract salt
    chk = hashlib.sha1(prop)  # Hash the string presented
    chk.update(salt)          # Salt to taste:
    return chk.digest() == digest

if __name__ == '__main__':
    import sys
    from getpass import getpass
    max_attempts = 3

    if len(sys.argv) < 2:
        print 'Must supply username against which to authenticate'
        sys.exit(127)
    name = sys.argv[1]

    user_dn = ld.search_s(bdn, ldap.SCOPE_SUBTREE, '(uid=%s)' % name)
    if len(user_dn) < 1:
        print 'No DN found for %s' % name
        sys.exit(126)
    pw = user_dn[0][1].get('userPassword', [''])[0]

    exit_value = 1
    attempts = 0
    while attempts < max_attempts:
        prop = getpass('Password: ')
        if chk(prop, pw):
            print 'Authentication successful'
            exit_value = 0
            break
        else:
            print 'Authentication failed'
        attempts += 1
    else:
        print 'Maximum retries exceeded'
    sys.exit(exit_value)

这似乎可行(假设我们在 .creds.ini 中有适当的值)。

下面是实现第二个选项的一些代码:

#!python
# ...
### Same ConfigParser and LDAP initialization as before
# ...

def chk(prop, dn):
    chk = ldap.initialize(svr)
    chk.protocol_version = ldap.VERSION3
    try:
        chk.simple_bind_s(dn, prop)
    except ldap.INVALID_CREDENTIALS:
        return False
    chk.unbind()
    return True

if __name__ == '__main__':
    import sys
    from getpass import getpass
    max_attempts = 3


    if len(sys.argv) < 2:
        print 'Must supply username against which to authenticate'
        sys.exit(127)
    name = sys.argv[1]

    user_dn = ld.search_s(bdn, ldap.SCOPE_SUBTREE, '(uid=%s)' % name)
    if len(user_dn) < 1:
        print 'No distinguished name (DN) found for %s' % name
        sys.exit(126)

    dn = user_dn[0][0]

    exit_value = 1
    attempts = 0
    while attempts < max_attempts:
        prop = getpass('Password: ')
        if chk(prop, dn):
            print 'Authentication successful'
            exit_value = 0
            break
        else:
            print 'Authentication failed'
        attempts += 1
    else:
        print 'Maximum retries exceeded'
    sys.exit(exit_value)

此处未显示,但我还测试了我可以继续使用 ld LDAP 连接,而独立于瞬态 chk LDAP 对象。所以我长期运行的网络服务可以继续重复使用一个连接。

无论我使用两个 PAM 模块中的哪一个,最后一个选项几乎相同。这是一个使用 python-pam 的示例:

#!/usr/bin/env python
import pam

pam_conn = pam.pam()

def chk(prop, name):
    return pam_conn.authenticate(name, prop)

if __name__ == '__main__':
    import sys
    from getpass import getpass
    max_attempts = 3


    if len(sys.argv) < 2:
        print 'Must supply username against which to authenticate'
        sys.exit(127)
    name = sys.argv[1]

    exit_value = 1
    attempts = 0
    while attempts < max_attempts:
        prop = getpass('Password: ')
        if chk(prop, name):
            print 'Authentication successful'
            exit_value = 0
            break
        else:
            print 'Authentication failed'
        attempts += 1
    else:
        print 'Maximum retries exceeded'
    sys.exit(exit_value)

我的问题是:我应该使用哪一个。他们中的任何一个是否比其他人特别不安全?对此“最佳实践”是否有任何共识?

【问题讨论】:

  • 最后:你是用哪种方式实现的?
  • 第二种方法(使用 userDN 绑定到 LDAP),如下@jwilleke 推荐。

标签: ldap pam


【解决方案1】:

绝对使用绑定作为用户提供的 userDN 和密码。

“使用具有有限搜索权限的服务帐户绑定到 LDAP,使用它来查找用户的 dn,然后尝试使用该 dn 和用户建议的密码绑定到 LDAP”

抱歉,我不知道“Authenticate() 函数”的行为方式。

不绑定为 userDN,可能会绕过适用于密码策略或帐户限制和入侵者检测的内置服务器功能。

-吉姆

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-27
    • 1970-01-01
    • 1970-01-01
    • 2017-06-03
    • 2011-09-23
    相关资源
    最近更新 更多