星期五, 三月 23, 2007

AA Center (2), openldap for centrialized login

在前面说明 Kerberos 的时候,已经说明的使用 openldap 的必要性,也就是要集中用户信息,来实现集中登录。这样我不需要在每一个主机上单独维护用户信息了。

openldap 标准的文档在:
http://www.openldap.org/doc/admin23/
应该先阅读这个文档,我这里只说明一些比较特殊不好理解的地方,特别是这个文档中没有说清楚的问题。以及利用基本的知识建立集中用户信息的方法。

有一个中文版:
http://www.infosecurity.org.cn/article/pki/ldap/23484.html

首先来看看 LDAP 的基本原理。LDAP(Lightweight Directory Access Protocol),轻型目录访问协议,从用户的角度来说,可以看成是对一个树形结构的数据库的访问协议,也就是目录服务。

数据模型:
在这个树形结构中,每一个节点是以条目(Entry)来表示的,这相当于 XML 中的 Element。每一个 Entry 最基本的信息就是 DN(Distinguished Name)和RDN(Relative Distinguished Name),用来表示这个节点,类似于相对路径和绝对路径的概念。

每一个 Entry 是一组属性(Attribute)的集合,每一个 Attribute 包含 Value 以及相应的 Type 和 ObjectClass 说明。

ObjectClass 是面向对象的概念,因此每一个 Entry 可以看作一个 Class 的实例(Instance)。而这个 Class 的定义会说明这种类型的 Entry 必须(MUST)包含哪些属性,可能(MAY)包含哪些属性等。Type 的定义与之类似。

RFC2551
Each entry MUST have an objectClass attribute. The objectClass
attribute specifies the object classes of an entry, which along with
the system and user schema determine the permitted attributes of an
entry. Values of this attribute may be modified by clients, but the
objectClass attribute cannot be removed. Servers may restrict the
modifications of this attribute to prevent the basic structural class
of the entry from being changed (e.g. one cannot change a person into
a country).


每一个 Entry 必须包含至少一个 ObjectClass 声明。

那么这些 ObjectClass 和 AttributeType 的声明在什么地方呢?一般都放在 Schema 中。Schema 一般都以文件的形式而存在,例如 /usr/local/etc/openldap/schema,因此与 DocBook 的 DTD 就有些相似了。

可以看一个 Entry 的例子:
dn: uid=rocky,ou=People,dc=shopex,dc=cn
uid: rocky
cn: rocky
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword: {crypt}$1$Kk00o2L8$fKM7c./FLiobXS4I.ktHU1
shadowLastChange: 13524
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/rocky
gecos: rocky
dn 就是前面说的 DN,那么其他那些 uid,o,ou,dc,cn,st,... 等又是表示什么含义呢?随便找一些 LDAP 的技术资料,都会给你一些如上的例子,其中有很多 o,ou,dc,cn 之类的东西,但几乎从来没有什么通俗点的文档解释过这些东西的来历。我怎么知道什么时候要使用那一个呢?

事实上,这些东西都是 AttributeType,在 RFC2253 中,你可以看到如下的说明:

String X.500 AttributeType
------------------------------
CN commonName
L localityName
ST stateOrProvinceName
O organizationName
OU organizationalUnitName
C countryName
STREET streetAddress
DC domainComponent
UID userid


前面已经说过,ObjectClass 和 AttributeType 都是有定义的,定义在 schema 中。那么对这些 AttributeType 的定义就在 /usr/local/etc/openldap/schema/core.shema 中。所以,可以看到,虽然 O,OU,C,CN,DC,UID 这些是很基础的属性类型,也仍然是由外部来定义的。注意:dn 不是由 schema 定义的。

所以在上面那个 Entry 的例子中,除了 objectClass 声明之外,其他都是 Attribute Type 声明,其实质是相同的,所以 cn, uid 与 uidNumber, userPassword 是一样的,只不过那些属性类型的声明不再 core.schema 中,而在 /usr/local/etc/openldap/schema/nis.schema。

上面的 Entry 是有格式的。按照这个格式编写的文件就称之为 LDIF(LDAP Data Interchange Format)文件。那么 ldif 又如何知道要用哪个 schema 的呢?

这在 slapd.conf 中定义。slapd 是 ldap 的服务守护进程。
sh# vi /usr/local/etc/openldap/slapd.conf
include /Opt/LDAP/etc/openldap/schema/nis.schema


用 LDAP 集中登录(Kerberos):
就目 前我所知之,利用 LDAP 集中用户信息以实现登录有两种方案,一是利用 pam_ldap 来实现认证,另一种是利用 Kerberos 替代 pam_ldap 来做认证,而用户信息存放在 LDAP 数据库中。无论采取那种形式,都必须用到 nss_ldap 库,从而可以在 /etc/nsswitch.conf 中增加对 ldap 的使用选项。

配置服务器,修改 slapd.conf,将 suffix 和 rootdn 都改成你自己的域,如下:
sh# slappasswd
{SSHA}ck65VXczIGUsE/EOCYdF8qxwBCf73di7
sh# vi /usr/local/etc/openldap/slapd.conf
# suffix "dc=my-domain,dc=com"
suffix "dc=shopex,dc=cn"
# rootdn "cn=Manager,dc=my-domain,dc=com"
rootdn "cn=ldapadmin,dc=shopex,dc=org"
# rootpw secret
rootpw {SSHA}ck65VXczIGUsE/EOCYdF8qxwBCf73di7
这样,openldap 将以 cn=ldampadmin,dc=shopex,dc=cn 作为你实际的顶级域。rootpw 是服务器密码,是可以通过网络来访问的,默认是明文,所以上面使用 slappasswd 来生成加密串,一旦完成配置,应该注释禁用该条目。openldap 默认使用明文传送密码,除非在 slapd.conf 中配置使用了 SSL/TSL(Transaction Layer Security)。

然后启动服务:
sh# /usr/local/libexec/slapd

sh# /usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf
在继续之前,先看看有没有问题,运行如下命令:
sh# ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts
# extended LDIF
#
# LDAPv3
# base <> with scope baseObject
# filter: (objectclass=*)
# requesting: namingContexts
#

#
dn:
namingContexts: dc=shopex,dc=org

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
然后我需要在 LDAP 的数据库中添加用户信息,这可以通过编写 LDIF 文件,然后用 ldapadd 工具来导入。前面的 Entry 例子即是这样一个包含完整用户信息的一个 LDIF 文件。但如果要将现有的用户信息迁移到 LDAP,那么可以使用一个称之为 MigrationTools 的工具;可能你不太属性这个 LDIF 应该怎么手工编写,那么也可以利用这个工具先生成一个模板,再在那个基础上进行修改。

可以在 http://www.padl.com/download/MigrationTools.tgz 下载最新的版本。然后来看看怎么做:
sh# cd MigrationTools-47/
sh# vi migrate_common.ph
$DEFAULT_BASE = "dc=shopex,dc=cn"
sh# ./migrate_base.pl >/tmp/base.ldif
sh# ./migrate_group.pl /etc/group /tmp/group.ldif
sh# ./migrate_hosts.pl /etc/hosts /tmp/hosts.ldif
sh# ./migrate_passwd.pl /etc/passwd /tmp/passwd.ldif
然后修改这几个文件,特别是 passwd.ldif 和 group.ldif,因为这里我们之做实验,不做实际的迁移,所以我们将上面 Entry 例子中的内容照搬过来,其 他的条目删除——注意,在做实际迁移的时候,因为迁移后很多用户比如 root 不再使用本地 /etc/passwd 中的信息,所以如果中间出错,有可能导致无法登录,所以如果要迁移 root 等重要用户,应该保证有一个 root 登录会话,并且在测试成功之前不要注销!

如果要迁移 root 用户,还有其他一些重要的问题需要考虑。
sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=shopex,dc=org" -w "secret" -f passwd.ldif
adding new entry "uid=rocky,ou=People,dc=shopex,dc=cn"
ldap_add: Invalid syntax (21)
additional info: objectClass: value #0 invalid per syntax
出现这个错误就是和上面说的那样,应该将 nis.schema 包含进来。
sh# vi /usr/local/etc/openldap/slapd.conf
include /usr/local/etc/openldap/schema/core.schema
include /usr/local/etc/openldap/schema/nis.schema
sh# killall -HUP slapd
# /usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf
/usr/local/etc/openldap/schema/nis.schema: line 203: AttributeType not found: "manager"
这是因为还 nis.schema 还依赖于另一个 schema,consine.schema
sh# vi /usr/local/etc/openldap/slapd.conf
include /usr/local/etc/openldap/schema/core.schema
include /usr/local/etc/openldap/schema/cosine.schema
include /usr/local/etc/openldap/schema/nis.schema
我不太清楚为什么使用加密后的密码不行,所以还是只好先使用了默认的那个密码。
sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=shoepx,dc=org" -f passwd.ldif -w "{SSHA}ck65VXczIGUsE/EOCYdF8qxwBCf73di7"
ldap_bind: Invalid credentials (49)
如果你在导入时发现如下错误:
sh# /usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf
sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=shopex,dc=org" -w "secret" -f passwd.ldif
adding new entry "uid=rocky,ou=People,dc=shopex,dc=cn"
ldap_add: Server is unwilling to perform (53)
additional info: no global superior knowledge
那么你应该首先检查 slapd.conf 文件的配置是使用了正确的域,和 migrate_common.ph 中应该是一样的。这里是
rootdn      "cn=ldapadmin,dc=shopex,dc=org"
写错了!

如果没有问题,那么应该是如下的输出:
adding new entry "uid=rocky,ou=People,dc=shopex,dc=cn"
adding new entry "cn=rocky,ou=Group,dc=shopex,dc=cn"
接着配置各个客户端。你应该已经安装 Kerberos 的方法配置了 PAM 认证,所以不需要再做改动,唯一需要更改的就是 /etc/nsswitch.conf 了:
passwd: files ldap
shadow: files ldap
group: files ldap
注意,和前面配置 Kerberos 认证时一样,rocky 这个帐户并不存在与本地的 /etc/passwd 文件中,现在只有在 LDAP 数据库中有他的信息,并且 Kerberos 中有其相应的 Client principal。

从 PuTTY 登录的情况来看:
 login as: rocky
rocky@192.168.0.98's password:
Last login: Tue Mar 20 13:23:46 2007 from 192.168.0.64
Could not chdir to home directory /home/rocky: No such file or directory
-bash-3.00$

sh# tail -f /var/log/message
Mar 21 09:37:53 docs sshd(pam_unix)[3913]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64 user=rocky
Mar 21 09:37:53 docs sshd[3913]: pam_krb5[3913]: The "hosts" configuration directive is not supported with your release of Kerberos. Please check if your release supports an `extra_addresses' directive instead.
Mar 21 09:37:53 docs sshd[3913]: pam_krb5[3913]: authentication succeeds for 'rocky' (rocky@SHOPEX.CN)
Mar 21 09:37:53 docs sshd(pam_unix)[3915]: session opened for user rocky by (uid=0)

sh# tail -f /var/log/krb5kdc.log
Mar 21 09:37:53 docs.shopex.cn krb5kdc[23244](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1174484273, etypes {rep=16 tkt=23 ses=16}, rocky@SHOPEX.CN for krbtgt/SHOPEX.CN@SHOPEX.CN
Mar 21 09:37:53 docs.shopex.cn krb5kdc[23244](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1174484273, etypes {rep=16 tkt=23 ses=16}, rocky@SHOPEX.CN for krbtgt/SHOPEX.CN@SHOPEX.CN
那是不是说我实际上不需要 Kerberos 呢?因为几乎所有的信息都在 LDAP 数据库中。

sh# /etc/init.d/krb5kdc stop
后再尝试登录,则 /var/log/messages 输出如下:

Mar 21 09:42:05 docs sshd(pam_unix)[3972]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64 user=rocky
Mar 21 09:42:05 docs sshd[3972]: pam_krb5[3972]: The "hosts" configuration directive is not supported with your release of Kerberos. Please check if your release supports an `extra_addresses' directive instead.
Mar 21 09:42:05 docs sshd[3972]: pam_krb5[3972]: authentication fails for 'rocky' (rocky@SHOPEX.CN): Authentication service cannot retrieve authentication info. (Cannot contact any KDC for requested realm)
这说明 Kerberos 的验证确实在起作用。而且 LDAP 中的 userPassword 和 Kerberos 的 rocky 用户的 Password 实际上是不一样的

尝试先从 Kerberos 取得票据:
sh# kinit rocky
sh# ssh rocky@docs.shopex.cn
Last login: Wed Mar 21 09:56:11 2007 from docs.shopex.cn
Could not chdir to home directory /home/rocky: No such file or directory
-bash-3.00$ id
uid=1000(rocky) gid=1000(rocky) groups=1000(rocky)

sh# tail -f /var/log/messages
Mar 21 09:56:57 docs sshd(pam_unix)[4129]: session opened for user rocky by (uid=0)
可见,使用 kinit 取得票据之后,登录就不会再有密码提示了。
[root@docs ~]# su - rocky
su: warning: cannot change directory to /home/rocky: No such file or directory
-bash-3.00$
也没有问题。

再看看以其他用户的身份会是什么情况:
sh# su - sysadm
sh$ ssh rocky@docs.shopex.cn
The authenticity of host 'docs.shopex.cn (192.168.0.98)' can't be established.
RSA key fingerprint is 82:25:6e:1f:8e:70:dc:66:62:3e:7b:4c:f0:40:3e:4c.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'docs.shopex.cn,192.168.0.98' (RSA) to the list of known hosts.
rocky@docs.shopex.cn's password:
Last login: Wed Mar 21 09:56:57 2007 from docs.shopex.cn
Could not chdir to home directory /home/rocky: No such file or directory
-bash-3.00$

/var/log/messages
Mar 21 09:57:33 docs su(pam_unix)[4148]: session opened for user sysadm by root(uid=0)
Mar 21 09:57:44 docs sshd(pam_unix)[4174]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=docs.shopex.cn user=rocky
Mar 21 09:57:44 docs sshd[4174]: pam_krb5[4174]: The "hosts" configuration directive is not supported with your release of Kerberos. Please check if your release supports an `extra_addresses' directive instead.
Mar 21 09:57:44 docs sshd[4174]: pam_krb5[4174]: authentication succeeds for 'rocky' (rocky@SHOPEX.CN)
Mar 21 09:57:44 docs sshd(pam_unix)[4176]: session opened for user rocky by (uid=0)

sh# su - sysadm
sh$ kinit rocky
Password for rocky@SHOPEX.CN:
sh$ klist
Ticket cache: FILE:/tmp/krb5cc_501
Default principal: rocky@SHOPEX.CN

Valid starting Expires Service principal
03/21/07 09:59:52 03/22/07 09:59:52 krbtgt/SHOPEX.CN@SHOPEX.CN

Kerberos 4 ticket cache: /tmp/tkt501
klist: You have no tickets cached
sh$ ssh rocky@docs.shopex.cn
Last login: Wed Mar 21 09:57:44 2007 from docs.shopex.cn
Could not chdir to home directory /home/rocky: No such file or directory
-bash-3.00$

/var/log/messages
Mar 21 10:00:32 docs sshd(pam_unix)[4234]: session opened for user rocky by (uid=0)
Mar 21 10:18:44 docs su[4629]: nss_ldap: reconnecting to LDAP server...
Mar 21 10:18:44 docs su[4629]: nss_ldap: reconnected to LDAP server after 1 attempt(s)

注意上面的 FILE:/tmp/krb5cc_501, 可以发现是以用户的 UID 来命名的。

然后看看增加一个 Kerberos 用户 roc,并从这个用户登录到 rocky 是否可行:
sh# kadmin -p rocky/admin@SHOPEX.CN
Authenticating as principal rocky/admin@SHOPEX.CN with password.
Password for rocky/admin@SHOPEX.CN:
kadmin: addprinc roc
WARNING: no policy specified for roc@SHOPEX.CN; defaulting to no policy
Enter password for principal "roc@SHOPEX.CN":
Re-enter password for principal "roc@SHOPEX.CN":
Principal "roc@SHOPEX.CN" created.
kadmin: quit
sh# kinit roc
Password for roc@SHOPEX.CN:
sh# klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: roc@SHOPEX.CN

Valid starting Expires Service principal
03/21/07 10:16:05 03/22/07 10:16:05 krbtgt/SHOPEX.CN@SHOPEX.CN

Kerberos 4 ticket cache: /tmp/tkt0
klist: You have no tickets cached

sh# ssh rocky@docs.shopex.cn
rocky@docs.shopex.cn's password:
Last login: Wed Mar 21 10:19:32 2007 from docs.shopex.cn
Could not chdir to home directory /home/rocky: No such file or directory
-bash-3.00$

Mar 21 10:20:46 docs sshd(pam_unix)[4745]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=docs.shopex.cn user=rocky
Mar 21 10:20:46 docs sshd[4745]: pam_krb5[4745]: The "hosts" configuration directive is not supported with your release of Kerberos. Please check if your release supports an `extra_addresses' directive instead.
Mar 21 10:20:46 docs sshd[4745]: pam_krb5[4745]: authentication succeeds for 'rocky' (rocky@SHOPEX.CN)
Mar 21 10:20:46 docs sshd(pam_unix)[4747]: session opened for user rocky by (uid=0)
这说明 Kerberos 的 User principal 也还是必须和 LDAP 的 User infomation 互相匹配,否则仍然是不能登录的。

方案比较,以及为什么选择 Kerberos:
前面说过,除了使用 Kerberos 的 pam_krb5 来进行 Authentication(这也是上一篇中使用的方法)之外,另一种选择是使用 pam_ldap。看起来,如果使用 pam_ldap 似乎要简单的多,因为只使用一套软件就解决问题了。其实不然。因为这是在做身份验证,所以安全性是非常重要的问题,那么加密就必不可少,而 openldap 默认是使用明文加密的。可以看下面这个例子:
sh# ldapsearch -x -b 'uid=rocky,ou=People,dc=shopex,dc=cn'
# extended LDIF
#
# LDAPv3
# base with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# rocky, People, shopex.cn
dn: uid=rocky,ou=People,dc=shopex,dc=cn
uid: rocky
cn: rocky
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword:: e2NyeXB0fSQxJEtrMDBvMkw4JGZLTTdjLi9GTGlvYlhTNEkua3RIVTE=
shadowLastChange: 13524
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 1000
gidNumber: 1000
homeDirectory: /home/rocky
gecos: rocky

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
这里可以看到,其他的用户信息无所谓,但 userPassword 是可以毫无障碍的被任何人查询的,如果密码确实存放在 LDAP 中,那就危险了。

Kerberos 默认是加密的,而 openldap 要实现加密则必须使用 SSL/TLS,这也就意味着你必须为每一台客户系统维护一对私钥/证书,在这里也可以看到 Kerberos 的一个好处,那就是票据的加密是由 Kerberos 自动维护的。这并不是说就不再需要 SSL/TLS 了,不过在认证这一块,Kerberos 确实更方便。

而且,你不再需要频繁的输入密码了!

所以这里最终的效果就是,openldap 充当了 /etc/passwd 的角色,而 Kerberos 相当于 /etc/shadow。

/---------->KDC
| /------KDC
(1) |
| (2)
| |
| V
Client------(3)------>Server(4)------(5)------>LDAP
(4) Lookup /etc/passwd and /etc/nsswitch.conf
(5) Lookup LDAP Database for account information(NOT Password)