LDAP 身份验证

商标

FreeBSD 是 FreeBSD 基金会的注册商标。

制造商和销售商用来区分其产品的许多名称都被声明为商标。在本文档中出现这些名称的地方,并且 FreeBSD 项目知道商标声明,这些名称后面已加上“™”或“®”符号。

摘要

本文档旨在作为在 FreeBSD 上配置 LDAP 服务器(主要是 OpenLDAP 服务器)进行身份验证的指南。这在许多服务器需要相同用户帐户的情况下很有用,例如作为 NIS 的替代方案。


1. 前言

本文档旨在让读者对 LDAP 有足够的了解,以便配置 LDAP 服务器。本文档将尝试解释 net/nss_ldapsecurity/pam_ldap,以便与客户端机器服务一起用于 LDAP 服务器。

完成后,读者应该能够配置和部署一个可以托管 LDAP 目录的 FreeBSD 服务器,以及配置和部署一个可以针对 LDAP 目录进行身份验证的 FreeBSD 服务器。

本文档并非旨在全面介绍配置 LDAP 或本文档中讨论的其他服务的安全、稳健性或最佳实践注意事项。虽然作者尽力做到一切正确,但他们没有解决超出一般范围的安全问题。本文档应被视为仅奠定理论基础,任何实际实现都应伴随着仔细的需求分析。

2. 配置 LDAP

LDAP 代表“轻量级目录访问协议”,是 X.500 目录访问协议的一个子集。其最新的规范在 RFC4510 及其相关文档中。本质上,它是一个数据库,预期读取的次数多于写入的次数。

LDAP 服务器 OpenLDAP 将在本文件的示例中使用;虽然此处的原理通常适用于许多不同的服务器,但大多数具体的管理是 OpenLDAP 特定的。端口中有多个服务器版本,例如 net/openldap26-server。客户端服务器将需要相应的 net/openldap26-client 库。

LDAP 服务需要配置的区域(基本上)有两个。第一个是设置服务器以正确接收连接,第二个是向服务器的目录添加条目,以便 FreeBSD 工具知道如何与之交互。

2.1. 设置服务器以接收连接

本节特定于 OpenLDAP。如果您使用的是其他服务器,则需要查阅该服务器的文档。

2.1.1. 安装 OpenLDAP

首先,安装 OpenLDAP

示例 1. 安装 OpenLDAP
# cd /usr/ports/net/openldap26-server
# make install clean

这将安装 slapdslurpd 二进制文件,以及所需的 OpenLDAP 库。

2.1.2. 配置 OpenLDAP

接下来,我们必须配置 OpenLDAP。

您需要在与 LDAP 服务器的连接中要求加密;否则,用户的密码将以明文形式传输,这被认为是不安全的。我们将使用的工具支持两种非常相似的加密方式,SSL 和 TLS。

TLS 代表“传输层安全”。使用 TLS 的服务倾向于与不使用 TLS 的相同服务在相同端口上连接;因此,支持 TLS 的 SMTP 服务器将在端口 25 上侦听连接,而 LDAP 服务器将在 389 上侦听。

SSL 代表“安全套接字层”,并且实现 SSL 的服务不会在与非 SSL 对等方相同的端口上侦听。因此,SMTPS 在端口 465(而不是 25)上侦听,HTTPS 在 443 上侦听,而 LDAPS 在 636 上侦听。

SSL 使用与 TLS 不同的端口的原因是,TLS 连接最初以明文形式开始,并在 STARTTLS 指令后切换到加密流量。SSL 连接从一开始就是加密的。除此之外,两者之间没有实质性差异。

我们将调整 OpenLDAP 以使用 TLS,因为 SSL 被认为已弃用。

通过端口安装 OpenLDAP 后,/usr/local/etc/openldap/slapd.conf 中的以下配置参数将启用 TLS

security ssf=128

TLSCertificateFile /path/to/your/cert.crt
TLSCertificateKeyFile /path/to/your/cert.key
TLSCACertificateFile /path/to/your/cacert.crt

这里,ssf=128 告诉 OpenLDAP 要求所有连接(包括搜索和更新)都使用 128 位加密。可以根据站点的安全需求配置此参数,但很少需要削弱它,因为大多数 LDAP 客户端库都支持强加密。

cert.crtcert.keycacert.crt 文件对于客户端验证是有效的 LDAP 服务器是必需的。如果您只需要运行服务器,则可以使用 OpenSSL 创建自签名证书

示例 2. 生成 RSA 密钥
% openssl genrsa -out cert.key 1024
Generating RSA private key, 1024 bit long modulus
....................++++++
...++++++
e is 65537 (0x10001)

% openssl req -new -key cert.key -out cert.csr

此时,系统将提示您输入一些值。您可以输入任何您喜欢的值;但是,重要的是“公用名称”值为 OpenLDAP 服务器的完全限定域名。在本例中以及此处的示例中,服务器为server.example.org。错误设置此值会导致客户端在建立连接时失败。这可能会导致极大的挫败感,因此请确保您仔细遵循这些步骤。

最后,需要对证书签名请求进行签名

示例 3. 自签名证书
% openssl x509 -req -in cert.csr -days 365 -signkey cert.key -out cert.crt
Signature ok
subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd
Getting Private key

这将创建一个自签名证书,可用于 slapd.conf 中的指令,其中 cert.crtcacert.crt 是同一个文件。如果您要使用多个 OpenLDAP 服务器(用于通过 slurpd 进行复制),则需要查看 LDAP 的 OpenSSL 证书 以生成 CA 密钥并使用它来签名各个服务器证书。

完成后,将以下内容放入 /etc/rc.conf

slapd_enable="YES"

然后运行 /usr/local/etc/rc.d/slapd start。这应该会启动 OpenLDAP。使用以下命令确认它是否在 389 上侦听

% sockstat -4 -p 389
ldap     slapd      3261  7  tcp4   *:389                 *:*

2.1.3. 配置客户端

安装 net/openldap26-client 端口以获取 OpenLDAP 库。客户端机器将始终具有 OpenLDAP 库,因为这是 security/pam_ldapnet/nss_ldap 支持的,至少目前是这样。

OpenLDAP 库的配置文件是 /usr/local/etc/openldap/ldap.conf。编辑此文件以包含以下值

base dc=example,dc=org
uri ldap://server.example.org/
ssl start_tls
tls_cacert /path/to/your/cacert.crt

重要的是,您的客户端能够访问 cacert.crt,否则它们将无法连接。

有两个名为 ldap.conf 的文件。第一个是此文件,用于 OpenLDAP 库并定义如何与服务器通信。第二个是 /usr/local/etc/ldap.conf,用于 pam_ldap。

此时,您应该能够在客户端机器上运行 ldapsearch -Z-Z 表示“使用 TLS”。如果您遇到错误,则表示某些配置错误;最有可能的是您的证书。使用 openssl(1)s_clients_server 来确保您已正确配置和签名了它们。

2.2. 数据库中的条目

针对 LDAP 目录进行身份验证通常是通过尝试以连接用户身份绑定到目录来完成的。这是通过使用提供的用户名在目录上建立“简单”绑定来完成的。如果存在一个 uid 等于用户名的条目,并且该条目的 userPassword 属性与提供的密码匹配,则绑定成功。

我们要做的第一件事是确定用户将在目录中的哪个位置。

我们数据库的基本条目是 dc=example,dc=org。大多数客户端似乎期望的用户默认位置类似于 ou=people,base,因此这里将使用该位置。但是请记住,这是可配置的。

因此,people 组织单位的 ldif 条目将如下所示

dn: ou=people,dc=example,dc=org
objectClass: top
objectClass: organizationalUnit
ou: people

所有用户都将作为此组织单位的子条目创建。

可以考虑一下用户将属于的对象类。大多数工具默认情况下将使用 people,如果您只需要提供要针对其进行身份验证的条目,则此方法很好。但是,如果您也要在 LDAP 数据库中存储用户信息,则可能需要使用 inetOrgPerson,它具有许多有用的属性。无论哪种情况,都需要在 slapd.conf 中加载相关的模式。

在本示例中,我们将使用 person 对象类。如果您使用的是 inetOrgPerson,则步骤基本相同,只是需要 sn 属性。

要添加名为 tuser 的测试用户,ldif 将如下所示

dn: uid=tuser,ou=people,dc=example,dc=org
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: top
uidNumber: 10000
gidNumber: 10000
homeDirectory: /home/tuser
loginShell: /bin/csh
uid: tuser
cn: tuser

我将 LDAP 用户的 UID 从 10000 开始,以避免与系统帐户冲突;您可以在此处配置您希望的任何数字,只要它小于 65536 即可。

我们还需要组条目。它们与用户条目一样可配置,但我们将使用以下默认值

dn: ou=groups,dc=example,dc=org
objectClass: top
objectClass: organizationalUnit
ou: groups

dn: cn=tuser,ou=groups,dc=example,dc=org
objectClass: posixGroup
objectClass: top
gidNumber: 10000
cn: tuser

要将这些条目输入到您的数据库中,您可以使用包含这些条目的文件上的 slapaddldapadd。或者,您可以使用 sysutils/ldapvi

客户端机器上的 ldapsearch 实用程序现在应该返回这些条目。如果返回,则您的数据库已正确配置为用作 LDAP 身份验证服务器。

3. 客户端配置

客户端应该已经从 配置客户端 中获得了 OpenLDAP 库,但是如果您要安装多台客户端机器,则需要在每台机器上安装 net/openldap26-client

FreeBSD 需要安装两个端口才能对 LDAP 服务器进行身份验证,security/pam_ldapnet/nss_ldap

3.1. 身份验证

security/pam_ldap 通过 /usr/local/etc/ldap.conf 进行配置。

这是一个与 OpenLDAP 库函数配置文件 /usr/local/etc/openldap/ldap.conf 不同的文件;但是,它采用了许多相同的选项;事实上,它是该文件的超集。在本节的其余部分中,“ldap.conf” 将指代 /usr/local/etc/ldap.conf

因此,我们需要将我们原始的所有配置参数从 openldap/ldap.conf 复制到新的 ldap.conf。完成此操作后,我们需要告诉 security/pam_ldap 在目录服务器上查找什么。

我们使用 uid 属性识别用户。要配置此属性(尽管它是默认值),请在 ldap.conf 中设置 pam_login_attribute 指令。

示例 4. 设置 pam_login_attribute
pam_login_attribute uid

设置此属性后,security/pam_ldap 将在 base 下的整个 LDAP 目录中搜索值 uid=username。如果找到一个且只有一个条目,它将尝试以该用户身份使用提供的密码进行绑定。如果绑定成功,则允许访问。否则将失败。

shell 不在 /etc/shells 中的用户将无法登录。当在 LDAP 服务器上将 Bash 设置为用户 shell 时,这一点尤其重要。Bash 不包含在 FreeBSD 的默认安装中。从软件包或端口安装时,它位于 /usr/local/bin/bash。验证服务器上 shell 的路径是否设置正确。

% getent passwd username

当输出在最后一列中显示 /bin/bash 时,有两种选择。第一种是将 LDAP 服务器上用户的条目更改为 /usr/local/bin/bash。第二种选择是在 LDAP 客户端计算机上创建一个符号链接,以便在正确的位置找到 Bash。

# ln -s /usr/local/bin/bash /bin/bash

确保 /etc/shells 包含 /usr/local/bin/bash/bin/bash 的条目。然后,用户将能够使用 Bash 作为其 shell 登录到系统。

3.1.1. PAM

PAM(代表“可插拔身份验证模块”)是 FreeBSD 对其大多数会话进行身份验证的方法。要告诉 FreeBSD 我们希望使用 LDAP 服务器,我们需要在相应的 PAM 文件中添加一行。

大多数情况下,相应的 PAM 文件是 /etc/pam.d/sshd,如果您想使用 SSH(请记住在 /etc/ssh/sshd_config 中设置相关的选项,否则 SSH 将不会使用 PAM)。

要使用 PAM 进行身份验证,请添加以下行:

auth  sufficient  /usr/local/lib/pam_ldap.so  no_warn

此行在文件中的确切位置以及第四列中出现的选项决定了身份验证机制的确切行为;请参阅 pam(d)

使用此配置,您应该能够对 LDAP 目录进行用户身份验证。PAM 将使用您的凭据执行绑定,如果成功,则会告诉 SSH 允许访问。

但是,允许目录中的每个用户进入每个客户端计算机不是一个好主意。使用当前配置,用户只需一个 LDAP 条目即可登录到计算机。幸运的是,有一些方法可以限制用户访问。

ldap.conf 支持 pam_groupdn 指令;连接到此计算机的每个帐户都需要是此处指定的组的成员。例如,如果您有

pam_groupdn cn=servername,ou=accessgroups,dc=example,dc=org

ldap.conf 中,则只有该组的成员才能登录。但是,需要注意一些事项。

此组的成员在1个或多个memberUid属性中指定,并且每个属性都必须具有成员的完整区分名称。因此,memberUid: someuser 将不起作用;它必须是

memberUid: uid=someuser,ou=people,dc=example,dc=org

此外,此指令在身份验证期间不会在 PAM 中进行检查,而是在帐户管理期间进行检查,因此您需要在 account 下的 PAM 文件中添加第二行。这反过来将要求每个用户都列在组中,这并非我们想要的。为了避免阻止不在 LDAP 中的用户,您应该启用 ignore_unknown_user 属性。最后,您应该设置 ignore_authinfo_unavail 选项,以便在 LDAP 服务器不可用时不会被锁定在每台计算机之外。

然后您的 pam.d/sshd 可能如下所示:

示例 5. pam.d/sshd 示例
auth            required        pam_nologin.so          no_warn
auth            sufficient      pam_opie.so             no_warn no_fake_prompts
auth            requisite       pam_opieaccess.so       no_warn allow_local
auth            sufficient      /usr/local/lib/pam_ldap.so      no_warn
auth            required        pam_unix.so             no_warn try_first_pass

account         required        pam_login_access.so
account         required        /usr/local/lib/pam_ldap.so      no_warn ignore_authinfo_unavail ignore_unknown_user

由于我们专门在 pam.d/sshd 中添加了这些行,因此这只会对 SSH 会话产生影响。LDAP 用户将无法在控制台上登录。要更改此行为,请检查 /etc/pam.d 中的其他文件并相应地修改它们。

3.2. 名称服务切换

NSS 是将属性映射到名称的服务。因此,例如,如果一个文件由用户 1001 拥有,应用程序将查询 NSS 以获取 1001 的名称,它可能会得到 bobted 或用户的任何名称。

现在我们的用户信息保存在 LDAP 中,我们需要告诉 NSS 在被查询时到那里查找。

net/nss_ldap 端口可以做到这一点。它使用与 security/pam_ldap 相同的配置文件,安装后不应需要任何额外的参数。相反,剩下的只是编辑 /etc/nsswitch.conf 以利用该目录。只需替换以下行

group: compat
passwd: compat

group: files ldap
passwd: files ldap

这将允许您将用户名映射到 UID 以及将 UID 映射到用户名。

恭喜!您现在应该拥有了可用的 LDAP 身份验证。

3.3. 注意事项

不幸的是,在撰写本文时,FreeBSD 不支持使用 passwd(1) 更改用户密码。因此,大多数管理员只能自己实现解决方案。我在这里提供一些示例。请注意,如果您编写自己的密码更改脚本,则需要了解一些安全问题;请参阅 密码存储

示例 6. 用于更改密码的 Shell 脚本
#!/bin/sh

stty -echo
read -p "Old Password: " oldp; echo
read -p "New Password: " np1; echo
read -p "Retype New Password: " np2; echo
stty echo

if [ "$np1" != "$np2" ]; then
  echo "Passwords do not match."
  exit 1
fi

ldappasswd -D uid="$USER",ou=people,dc=example,dc=org \
  -w "$oldp" \
  -a "$oldp" \
  -s "$np1"

此脚本几乎没有进行任何错误检查,但更重要的是,它在存储密码的方式上非常草率。如果您执行此类操作,至少要调整 security.bsd.see_other_uids sysctl 值。

# sysctl security.bsd.see_other_uids=0

通过编写自定义程序,甚至编写 Web 界面,可以使用更灵活(可能更安全)的方法。以下是可更改 LDAP 密码的 Ruby 库的一部分。它在命令行和 Web 上都使用。

示例 7. 用于更改密码的 Ruby 脚本
require 'ldap'
require 'base64'
require 'digest'
require 'password' # ruby-password

ldap_server = "ldap.example.org"
luser = "uid=#{ENV['USER']},ou=people,dc=example,dc=org"

# get the new password, check it, and create a salted hash from it
def get_password
  pwd1 = Password.get("New Password: ")
  pwd2 = Password.get("Retype New Password: ")

  raise if pwd1 != pwd2
  pwd1.check # check password strength

  salt = rand.to_s.gsub(/0\./, '')
  pass = pwd1.to_s
  hash = "{SSHA}"+Base64.encode64(Digest::SHA1.digest("#{pass}#{salt}")+salt).chomp!
  return hash
end

oldp = Password.get("Old Password: ")
newp = get_password

# We'll just replace it.  That we can bind proves that we either know
# the old password or are an admin.

replace = LDAP::Mod.new(LDAP::LDAP_MOD_REPLACE | LDAP::LDAP_MOD_BVALUES,
                        "userPassword",
                        [newp])

conn = LDAP::SSLConn.new(ldap_server, 389, true)
conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
conn.bind(luser, oldp)
conn.modify(luser, [replace])

虽然不能保证没有安全漏洞(例如,密码保存在内存中),但这比简单的 sh 脚本更干净、更灵活。

4. 安全注意事项

现在您的机器(以及可能的其他服务)正在对您的 LDAP 服务器进行身份验证,此服务器需要至少与常规服务器上的 /etc/master.passwd 一样受到保护,甚至可能需要更多保护,因为损坏或破解的 LDAP 服务器将破坏每个客户端服务。

请记住,本节并不详尽。您应该持续审查您的配置和流程以进行改进。

4.1. 将属性设置为只读

LDAP 中的多个属性应为只读。例如,如果用户可以写入,则用户可以将其 uidNumber 属性更改为 0 并获得 root 访问权限!

首先,userPassword 属性不应全局可读。默认情况下,任何可以连接到 LDAP 服务器的人都可以读取此属性。要禁用此功能,请在 slapd.conf 中添加以下内容:

示例 8. 隐藏密码
access to dn.subtree="ou=people,dc=example,dc=org"
  attrs=userPassword
  by self write
  by anonymous auth
  by * none

access to *
  by self write
  by * read

这将不允许读取 userPassword 属性,同时仍允许用户更改自己的密码。

此外,您需要阻止用户更改他们的一些属性。默认情况下,用户可以更改任何属性(除了 LDAP 模式本身拒绝更改的属性),例如 uidNumber。要关闭此漏洞,请将上述内容修改为:

示例 9. 只读属性
access to dn.subtree="ou=people,dc=example,dc=org"
  attrs=userPassword
  by self write
  by anonymous auth
  by * none

access to attrs=homeDirectory,uidNumber,gidNumber
  by * read

access to *
  by self write
  by * read

这将阻止用户能够伪装成其他用户。

4.2. root 帐户定义

通常,LDAP 服务的 root 或管理器帐户将在配置文件中定义。例如,OpenLDAP 支持此功能,并且它有效,但如果 slapd.conf 被破坏,则可能会导致问题。最好仅将其用于引导自己进入 LDAP,然后在那里定义 root 帐户。

更好的是定义具有有限权限的帐户,并完全省略 root 帐户。例如,可以添加或删除用户帐户的用户被添加到一个组中,但他们自己无法更改此组的成员资格。这样的安全策略将有助于减轻密码泄露的影响。

4.2.1. 创建管理组

假设您希望您的 IT 部门能够更改用户的 home 目录,但您不希望他们都能添加或删除用户。执行此操作的方法是为这些管理员添加一个组:

示例 10. 创建管理组
dn: cn=homemanagement,dc=example,dc=org
objectClass: top
objectClass: posixGroup
cn: homemanagement
gidNumber: 121 # required for posixGroup
memberUid: uid=tuser,ou=people,dc=example,dc=org
memberUid: uid=user2,ou=people,dc=example,dc=org

然后更改 slapd.conf 中的权限属性:

示例 11. Home 目录管理组的 ACL
access to dn.subtree="ou=people,dc=example,dc=org"
  attr=homeDirectory
  by dn="cn=homemanagement,dc=example,dc=org"
  dnattr=memberUid write

现在 tuseruser2 可以更改其他用户的 home 目录。

在此示例中,我们已将一部分管理权限授予某些用户,而没有授予他们在其他域中的权限。其思想是,很快没有单个用户帐户拥有 root 帐户的权限,但 root 拥有的所有权限至少由一个用户拥有。然后 root 帐户变得不必要,可以将其删除。

4.3. 密码存储

默认情况下,OpenLDAP 将存储 userPassword 属性的值,就像存储任何其他数据一样:明文。大多数情况下,它是 base 64 编码的,这提供了足够的保护以防止诚实的管理员知道您的密码,但除此之外没有其他保护。

因此,最好以更安全的格式存储密码,例如 SSHA(带盐的 SHA)。这是由您用于更改用户密码的任何程序完成的。

附录 A:有用工具

还有一些其他程序可能有用,尤其是在您有许多用户并且不想手动配置所有内容的情况下。

security/pam_mkhomedir 是一个始终成功的 PAM 模块;其目的是为没有主目录的用户创建主目录。如果您有几十台客户端服务器和数百个用户,使用此模块并设置骨架目录比准备每个主目录要容易得多。

sysutils/ldapvi 是一个非常棒的实用程序,用于以类似 LDIF 的语法编辑 LDAP 值。目录(或目录的子部分)在由 EDITOR 环境变量选择的编辑器中呈现。这使得轻松地对目录进行大规模更改变得容易,而无需编写自定义工具。

security/openssh-portable 能够联系 LDAP 服务器以验证 SSH 密钥。如果您有多台服务器并且不想将您的公钥复制到所有服务器上,这非常方便。

附录 B:LDAP 的 OpenSSL 证书

如果您托管两个或多个 LDAP 服务器,您可能不希望使用自签名证书,因为每个客户端都必须配置为与每个证书一起工作。虽然这在技术上是可行的,但它远不如创建您自己的证书颁发机构并使用该机构签署服务器证书那么简单。

此处列出的步骤几乎没有尝试解释正在发生的事情 - 可以在 openssl(1) 及其相关文档中找到更详细的说明。

要创建证书颁发机构,我们只需要一个自签名证书和密钥。步骤如下:

示例 12. 创建证书
% openssl genrsa -out root.key 1024
% openssl req -new -key root.key -out root.csr
% openssl x509 -req -days 1024 -in root.csr -signkey root.key -out root.crt

这些将是您的根 CA 密钥和证书。您可能希望加密密钥并将其存储在安全的地方;任何拥有该密钥的人都可以伪装成您的 LDAP 服务器之一。

接下来,使用以上前两步创建密钥 ldap-server-one.key 和证书签名请求 ldap-server-one.csr。一旦您使用 root.key 签署了签名请求,您就可以在 LDAP 服务器上使用 ldap-server-one.* 了。

在生成证书签名请求时,请勿忘记使用完全限定域名作为“通用名称”属性;否则,客户端将拒绝与您建立连接,并且可能难以诊断问题。

要签署密钥,请使用 -CA-CAkey 而不是 -signkey

示例 13. 作为证书颁发机构进行签名
% openssl x509 -req -days 1024 \
-in ldap-server-one.csr -CA root.crt -CAkey root.key \
-out ldap-server-one.crt

生成的文将是您可以在 LDAP 服务器上使用的证书。

最后,为了让客户端信任所有服务器,请将 root.crt(证书,而不是密钥!)分发到每个客户端,并在 ldap.conf 中的 TLSCACertificateFile 指令中指定它。


上次修改时间:2024 年 9 月 23 日,作者 Fernando Apesteguía