NA-HW4-LDAP-使用者管理

NA-hw4 LDAP

HW4 spec

Create virtual machine

LDAP Provider

os: Ubuntu 20.04
username: cychen
passwd:

interfaces:

  1. enp0s3: Intranet(get IP addr 10.113.40.40 from DHCP)

LDAP Consumer

os: Ubuntu 20.04
username: cychen
passwd:

interfaces:

  1. enp0s3: Intranet(get IP addr 10.113.40.41 from DHCP)

Workstation

os: Ubuntu 20.04
username: cychen
passwd:

interfaces:

  1. enp0s3: Intranet(get IP addr 10.113.40.45 from DHCP)

LDAP Server

DNS

增加三筆 A records:

1
2
3
ldapprovider    IN      A       10.113.40.40
ldapconsumer IN A 10.113.40.41
workstation IN A 10.113.40.45

安裝並設定 LDAP

安裝

1
$ apt -y install slapd ldap-utils

這邊的設定可以跳過沒關係,反正下一步會重設一次 xD。

設定

1
2
3
4
5
6
$ dpkg-reconfigure slapd
Omit OpenLDAP server configuration? No
DNS domain name? 40.nasa
Organization name? 40.nasa
Do you want the database to be removed when slapd is plurged? No
Move old database? Yes

設定 Schema

先創一個資料夾,之後在這個資料夾底下做事

1
2
$ mkdir /var/ldap
$ cd /var/ldap
1
2
3
$ vim convert-schema.conf

include /var/ldap/publickey.schema
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ vim publickey.schema

# octetString SYNTAX
attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey'
DESC 'MANDATORY: OpenSSH Public key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

# printableString SYNTAX yes|no
objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
DESC 'MANDATORY: OpenSSH LPK objectclass'
MUST ( sshPublicKey $ uid )
)

之後再執行以下步驟:

1
2
3
$ slaptest -f convert-schema.conf -F /etc/ldap/slapd.d
$ chown -R openldap:openldap /etc/ldap/slapd.d
$ service slapd restart

如果能順利啟用,schema 就建好了。

LDAP 新增 Organizations

1
2
3
4
5
6
7
8
9
10
$ vim ou.ldif

dn: ou=People,dc=40,dc=nasa
objectClass: organizationalUnit
ou: People

dn: ou=Group,dc=40,dc=nasa
objectClass: organizationalUnit
ou: Group

執行 ldapadd 來寫入:

1
$ ldapadd -D "cn=admin,dc=40,dc=nasa" -W < ou.ldif

LDAP 引入 Groups、Users 資料

1
2
3
4
5
6
7
8
9
10
11
$ vim grps.ldif

dn: cn=ta,ou=Group,dc=40,dc=nasa
objectClass: posixGroup
cn: ta
gidNumber: 3000

dn: cn=stu40,ou=Group,dc=40,dc=nasa
objectClass: posixGroup
cn: stu40
gidNumber: 3040
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ vim usrs.ldif

dn: uid=ta,ou=People,dc=40,dc=nasa
uid: ta
userPassword: {password 原文}
uidNumber: 3000
cn: ta
gidNumber: 3000
homeDirectory: /u/nasa/ta/
objectClass: posixAccount
objectClass: shadowAccount
objectClass: ldapPublicKey
objectClass: account
sshPublicKey: {publickey}

dn: uid=stu40,ou=People,dc=40,dc=nasa
uid: stu40
userPassword: {password 原文}
uidNumber: 3040
cn: stu40
gidNumber: 3040
homeDirectory: /u/nasa/stu40/
objectClass: posixAccount
objectClass: shadowAccount
objectClass: account

執行 ldapadd 來寫入:

1
2
$ ldapadd -D "cn=admin,dc=40,dc=nasa" -W < grps.ldif
$ ldapadd -D "cn=admin,dc=40,dc=nasa" -W < usrs.ldif

新增 syncuser

1
2
3
4
5
6
7
8
9
$ vim syncuser.ldif

dn: cn=syncuser,dc=40,dc=nasa
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: syncuser
description: LDAP consumer
userPassword: {CRYPT}x

再執行:

1
ldapadd -x -D "cn=admin,dc=40,dc=nasa" -W -f syncuser.ldif

最後幫他改個密碼:

1
$ ldappasswd -x -D "cn=admin,dc=40,dc=nasa" -W -S cn=syncuser,dc=40,dc=nasa

ACL

LDAP 的 ACL 是選第一個匹配到的,所以嚴格的條件放前面,寬鬆的條件放後面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ vim acl.ldif

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0} to attrs=userPassword
by self write
by * auth
olcAccess: {1} to dn.exact="cn=syncuser,dc=40,dc=nasa"
by dn.exact="cn=syncuser,dc=40,dc=nasa" none
by * read
olcAccess: {2} to *
by dn.exact="cn=syncuser,dc=40,dc=nasa" write
by * break
olcAccess: {3} to dn.one="ou=People,dc=40,dc=nasa"
by self write
by * read
olcAccess: {4} to dn.subtree="dc=40,dc=nasa"
by * read
-
add: olcLimits
olcLimits: dn.exact="cn=syncuser,dc=40,dc=nasa"
time.soft=unlimited time.hard=unlimited
size.soft=unlimited size.hard=unlimited

從上到下解釋:

  1. 如果是 userPassword,那麼只有擁有者可讀可寫,其餘人等只能用此做認證。
  2. 對於 syncuser,他無法取得自己的資訊,這讓 consumer 不能同步這隻使用者。
  3. 對於其他資訊,他都有可寫的權限。
  4. 對於 People 的子節點(其他使用者),他們可以修改自己的資訊,也可以讀取別人的資訊。
  5. 讓所有人都可以讀取其他的資訊。
1
$ ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f  acl.ldif

As Client

LDAPProvider 自己也是 LDAP client(當執行ldapadd、ldapmodify⋯⋯時),所以也必須為 client 端做設置:

1
2
3
4
5
6
7
$ vim /etc/ldap/ldap.conf

...
BASE dc=40,dc=nasa
URI ldap://ldapconsumer.40.nasa ldap://ldapprovider.40.nasa
...


於此,LDAP 的主體就弄完了,接下來先弄 Workstation。

Workstation LDAP Authentication

LDAP Auth Client 安裝

1
$ apt -y install libnss-ldap libpam-ldap ldap-utils nfs-common
  1. LDAP Server Uniform Resource Identifier:
    • ldap://ldapprovider.40.nasa ldap://ldapconsumer.40.nasa
  2. BaseDN:
    • dc=40,dc=nasa
  3. LDAP version to use:
    • 3
  4. Make local root Database admin:
    • Yes
  5. Does the LDAP database require login:
    • No
  6. LDAP account for root:
    • cn=admin,dc=40,dc=nasa
  7. LDAP root account password:

這些設定如果不小心設錯了,可以在 /etc/ldap.conf 和 /etc/ldap.secret 中修改。

As Client

1
2
3
4
5
6
7
$ vim /etc/ldap/ldap.conf

...
BASE dc=40,dc=nasa
URI ldap://ldapconsumer.40.nasa ldap://ldapprovider.40.nasa
...

這部分並不會影響到 auth,因為 libnss-ldap 的設置檔會覆蓋掉 BASE 和 URI,但是此文件末有 TLS_CACERT 是會影響到的,這個我們待會在設置 TLS 的時候會再仔細的弄。雖然不會影響到 auth,但是會影響到 judge,judge 會使用 ldapsearch、ldapmodify 等 client 工具。

Workstation Users & PAM

1
2
3
4
5
6
7
8
$ vim /etc/nsswitch.conf

...
passwd: files systemd ldap
group: files systemd ldap
shadow: files
gshadow: files
...

啟用 libnss 之後就可以了:service libnss-ldap start

可以看使用者是否被引入:

1
2
$ id ta
uid=3000(ta) gid=3000(ta) groups=3000(ta)

此時可以執行指令:pam-auth-update
你可以在這支程式當中選擇是否要讓 UNIX User 登入、是否要讓 LDAP User 登入⋯⋯,我們確認 LDAP User 可以登入,確認無誤後,試著登入看看:

1
2
3
$ su ta
Password: <輸入你的密碼>
# 登入成功

NFS Mount

1
2
$ mkdir -p /u/nasa
$ vim /etc/fstab

加入這一行到文件末:

1
10.113.0.254:/vol/40/home       /u/nasa nfs     defaults      0       0

之後執行:mount -a
然後去看看: df -h | grep /u/nasa,看到的確有 mount 上。
mount 上之後,我們為 stu40 添加家目錄:

1
2
$ mkdir /u/nasa/stu40
$ chown stu40:stu40 /u/nasa/stu40

:::warning
因為前面有執行過 apt -y install nfs-common,所以才有的 NFS client,否則預設情況 Ubuntu 沒有 nfs client,你可能 mount 不上。
:::

如果做到這邊都成功,Workstation 就大功告成了。

LDAP Server StartTLS

支援 StartTLS

回到 LDAPProvider,先安裝以下工具:

1
$ apt-get install gnutls-bin ssl-cert
1
2
3
4
5
6
$ mkdir /etc/ssl/templates
$ vim /etc/ssl/templates/ca_server.conf

cn = LDAP Server CA
ca
cert_signing_key
1
2
3
4
5
6
7
8
$ vim /etc/ssl/templates/ldap_server.conf

organization = "40.nasa"
cn = ldapprovider.40.nasa
tls_www_server
encryption_key
signing_key
expiration_days = 3652

之後就可以生公私鑰和憑證了:

1
2
3
# CA
$ certtool -p --outfile /etc/ssl/private/ca_server.key
$ certtool -s --load-privkey /etc/ssl/private/ca_server.key --template /etc/ssl/templates/ca_server.conf --outfile /etc/ssl/certs/ca_server.pem
1
2
3
# LDAP Server
$ certtool -p --sec-param high --outfile /etc/ssl/private/ldap_server.key
$ certtool -c --load-privkey /etc/ssl/private/ldap_server.key --load-ca-certificate /etc/ssl/certs/ca_server.pem --load-ca-privkey /etc/ssl/private/ca_server.key --template /etc/ssl/templates/ldap_server.conf --outfile /etc/ssl/certs/ldap_server.pem

還要讓 LDAP 有辦法讀取到:

1
2
3
$ usermod -aG ssl-cert openldap
$ chown :ssl-cert /etc/ssl/private/ldap_server.key
$ chmod 640 /etc/ssl/private/ldap_server.key

接著就可以來設定 LDAP 了:

1
2
3
4
5
6
7
8
9
10
11
12
$ vim addcerts.ldif

dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/ca_server.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/ldap_server.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/ldap_server.key

執行指令:

1
$ ldapmodify -H ldapi:// -Y EXTERNAL -f addcerts.ldif

:::warning

如果出現以下錯誤:

1
ldap_modify: Other (e.g., implementation specific) error (80)

可以先關閉 slapd 試試看:

1
2
$ service slapd restart
$ ldapmodify -H ldapi:// -Y EXTERNAL -f addcerts.ldif

如果仍然出現這個錯誤,十有八成是 ldap 無法讀取到 ca_server.pem、ldap_server.pem、ldap_server.key 這三個文件,要檢查權限設置。

:::

然後再執行:

1
$ service slapd force-reload

As Client

我們還要把 CA 的憑證加給 ldap client。

首先在 ldaprovider 這樣做:

1
2
3
$ vim /etc/ldap/ldap.conf

TLS_CACERT /etc/ssl/certs/ca_server.pem

接著將 CA 憑證傳給 workstation:

1
$ scp /etc/ssl/certs/ca_server.pem user@workstation.40.nasa:/tmp/ca.pem

然後在 workstation 做這樣子的事情:

1
2
3
4
$ mv /tmp/ca.pem /etc/ldap/ca_server.pem
$ vim /etc/ldap/ldap.conf

TLS_CACERT /etc/ldap/ca_server.pem

之後就可以來測試了:

1
ldapwhoami -H ldap://ldapprovider.40.nasa -x -ZZ

如果出現 anonymous 就代表成功;如果出現 Connect error (-11),你就做錯了,最可能是因為 ldap client 沒有設置好 CACERT。

:::danger

設置 TLS_CACERT {CA 憑證位置} 的動作非常非常的重要,因為當你的程式有以 ldap client 的身分做事的時候,他都會用到這個設定,所以如果沒做這個設定,你在做了 forceTLS 之後,你 libnss 會無法和 ldap 伺服器拿資料,你 consumer 和 provider 也無法建立連線作 sync。

:::

強制 StartTLS

1
2
3
4
5
6
7
$ vim forcetls.ldif

dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcSecurity
olcSecurity: tls=1

執行 ldapmodify -H ldapi:// -Y EXTERNAL -f forcetls.ldif

:::warning

我一開始過不了這部分的測資,寫信問助教,助教這樣回覆:

所以我前面有加上為每一台設備做 client 端的設置。

:::

讓 Workstation 啟用 StartTLS

在強制 TLS 之後,Workstation 就無法 access 到 ldap 了,因為沒有啟用。

1
$ vim /etc/ldap.conf

尋找 ssl start_tls 並取消註解,之後再重啟就可以了: service libnss-ldap restart

DNS Cert Record

1
$ echo -n "cert IN TXT (" && cat /etc/ssl/certs/ca_server.pem | base64 | sed -n 's/\(.*\)/"\1"/p' && echo ")"

把這個結果放入 cert 這個 TXT record:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cert            IN      TXT     ( "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVBekNDQW11Z0F3SUJBZ0lVVUVsblhDMDh5" 
"aVorSmxHSGRzSW9OV2Z6R0pvd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dURVhNQlVHQTFVRUF4TU9U"
"RVJCVUNCVFpYSjJaWElnUTBFd0hoY05NakV3TmpBeE1qRXdNRFUwV2hjTgpNakl3TmpBeE1qRXdN"
"RFUwV2pBWk1SY3dGUVlEVlFRREV3NU1SRUZRSUZObGNuWmxjaUJEUVRDQ0FhSXdEUVlKCktvWklo"
"dmNOQVFFQkJRQURnZ0dQQURDQ0FZb0NnZ0dCQU5OTmw0WHkwZ3J4YXY1blhEYlhZSTZkSVlYWlF6"
"emQKcXdnc3RXeTYwTVM5NEVQdXAxajJybHY4L2tmSUtKVmQ0dHBZN0YzV0F0WjZSYXc3SnhKc2NI"
"Q295dFF1bDlYRwpZKy83Mk10UlVVa3dXd1RYbjJGbkEvTlhmNmV5c1FMTWV0Z2ZUTldaSGpPcnRV"
"OXF0RGlhVG9NcGNHK0J6Y25SCjJWcU1Nd3JialBQWnAyVDZmRnljc2hpS0VYalB4eVhHN3pBL1hL"
"MU9zLzE0UlFGdFh2Z2hTak9TcFl5d2c4M1UKeEM5cWhFanBVQk5zN2FuYkxEOGdhYnMzS3ZxRWlV"
"Skd4VnNwSGExK3lZVFBNd1UyLzlYd2xTM3dCeEg2L1B6QQpSTUs5Z050YXVCZVlqRkhFY2MwZC9M"
"VEN4RkwrSytLaUh3OXBRSFpUd3NYVjl4d1JLeW5mQ21VNXpuYllFT2o2Clc4VC80aEhndGFMdHRM"
"SDBxanp4QmZmdSszZlkwd0tXa3RqdlJseWlZVE5mNHhHdXhSUWsvcGp4bnhUME9GbTUKc2o4VmxP"
"TW00WGNiY0xQeG9mWURydlNvbWszbG55T1JFekl4N1NSOVlvVG1JcEFzNEZYdVFIcnhrZEtwNml6"
"RQpuTndHSktZMnlITmhBUWd4R2FFMU1nNFYxNHZ5UWt0SUFRSURBUUFCbzBNd1FUQVBCZ05WSFJN"
"QkFmOEVCVEFECkFRSC9NQThHQTFVZER3RUIvd1FGQXdNSEJBQXdIUVlEVlIwT0JCWUVGUGZnNytx"
"aTNIRTJBbStaZkpsbFlLSVgKVXErZ01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQmdRQVJscGhiZExt"
"c0NkWWxmc2t3RkRML1J3TTArSU44MXFMMApiYkx6aWpoRERxQjYvdmxuTkpTME5xMDlBQml5N214"
"aGdnZFY3c2FpMmpmQ25IREE3SkdmM1BxOU03MnF2LzZHCmZQQUgwdVdXQThsY3h6bnVocXNXS3Er"
"OGNNbUI2MldCODluNHVBRzFNdWlYV2c3RC9NL3V4MFlTL0QrVGI4SVQKNHpaU0hnOFl5bHJPN3dU"
"NVI4dU9oVXZ5WW9FV29Tc2VWY2c2V1Z3R05JRm9STjQxbGhlQVN1b0RqbGg0d21sdgptVXdxSXhL"
"T0ozbURnanpDQXpCeU5tZXFOeFp6SlJHTk1wTWQ0WWYrRTA2QjhwWXh2V05wa3JFRGpHRHpNNUhy"
"CkliMERoaWNsK1l2eW5DY3prZUhqUStGK3g4VzdJR3NHa3VNUXJtZmlnOWQ2L2dwcWJBM1M5VXhB"
"WWlYL2VESG8KelZFT3VOeXJudDJDM2dpS0JMRGd6THZkRGtBL0JrbE9oQXVDbXoySXY1M0loT1hP"
"WTRTU2FOa0tuU2wxQzgwTAp2UVA2WUR4emNsZUhZaGlTN0o2SVVYL1dxNlFJcW9lRlkwYmZnczFK"
"ZjZRanliem9JZHBiUnRURitDM2NZUENhCk5KWFd0S0duR0tZUGVOQUY4MUNxQU9tZTR3RTFPY2c9"
"Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K")

:::warning

這個步驟是為了要讓別人知道你的 CA 是誰,我不確定在實務上是否會運用這種方式,不過 judge 會利用這個 cert 來讀取 CA 的證書,然後使用這個證書來建立 TLS 連線作測試,所以如果沒有把這個 record 加上去或是加錯,會導致你這一部分失分。

:::

參考資料

LDAP Consumer

預先處理

先在 slave 機上做好一下的事情:

  1. 安裝好 slapd 和 ldap-utils,設定好 slapd 的 admin 帳號和 baseDN

    1
    2
    3
    4
    5
    6
    7
    8
    $ apt -y install slapd ldap-utils gnutls-bin ssl-cert
    $ dpkg-reconfigure slapd

    Omit OpenLDAP server configuration? No
    DNS domain name? 40.nasa
    Organization name? 40.nasa
    Do you want the database to be removed when slapd is plurged? No
    Move old database? Yes
  2. 讓 slave 機的 schema 和 master 機一致

    1
    2
    $ mkdir /var/ldap
    $ cd /var/ldap
    1
    2
    3
    $ vim convert-schema.conf

    include /var/ldap/publickey.schema
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $ vim publickey.schema

    # octetString SYNTAX
    attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey'
    DESC 'MANDATORY: OpenSSH Public key'
    EQUALITY octetStringMatch
    SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )

    # printableString SYNTAX yes|no
    objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY
    DESC 'MANDATORY: OpenSSH LPK objectclass'
    MUST ( sshPublicKey $ uid )
    )

    1
    2
    3
    $ slaptest -f convert-schema.conf -F /etc/ldap/slapd.d
    $ chown -R openldap:openldap /etc/ldap/slapd.d
    $ service slapd restart
  3. 用相同的 CA 生憑證,讓 slave 機支援 TLS

    我們回到 Provider 機做這件事:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $ vim /etc/ssl/templates/ldap_consumer.conf

    organization = "40.nasa"
    cn = ldapconsumer.40.nasa
    tls_www_server
    encryption_key
    signing_key
    expiration_days = 3652

    $ certtool -p --sec-param high --outfile /etc/ssl/private/ldap_consumer.key
    $ certtool -c --load-privkey /etc/ssl/private/ldap_consumer.key --load-ca-certificate /etc/ssl/certs/ca_server.pem --load-ca-privkey /etc/ssl/private/ca_server.key --template /etc/ssl/templates/ldap_consumer.conf --outfile /etc/ssl/certs/ldap_consumer.pem

    接著再把 ca_server.pem、ldap_consumer.key 和 ldap_consumer.pem 傳到 Consumer 機:

    1
    2
    3
    4

    $ scp /etc/ssl/private/ldap_consumer.key user@ldapconsumer.40.nasa:/tmp/ldap.key
    $ scp /etc/ssl/certs/ldap_consumer.pem user@ldapconsumer.40.nasa:/tmp/ldap.pem
    $ scp /etc/ssl/certs/ca_server.pem user@ldapconsumer.40.nasa:/tmp/ca.pem

    之後我們到 Consumer 機執行以下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # 改使用者
    $ chown root:root /tmp/ca.pem /tmp/ldap.key /tmp/ldap.pem
    # 移動到應該存在的位置
    $ mv /tmp/ca.pem /etc/ssl/certs/ca_server.pem
    $ mv /tmp/ldap.key /etc/ssl/private/ldap_server.key
    $ mv /tmp/ldap.pem /etc/ssl/certs/ldap_server.pem
    # 確保 openldap 可以讀取到檔案
    $ usermod -aG ssl-cert openldap
    $ chown :ssl-cert /etc/ssl/private/ldap_server.key
    $ chmod 640 /etc/ssl/private/ldap_server.key
    $ chmod 644 /etc/ssl/certs/ca_server.pem /etc/ssl/certs/ldap_server.pem
    # 做 ldap 設定
    $ vim /var/ldap/addcerts.ldif

    dn: cn=config
    changetype: modify
    add: olcTLSCACertificateFile
    olcTLSCACertificateFile: /etc/ssl/certs/ca_server.pem
    -
    add: olcTLSCertificateFile
    olcTLSCertificateFile: /etc/ssl/certs/ldap_server.pem
    -
    add: olcTLSCertificateKeyFile
    olcTLSCertificateKeyFile: /etc/ssl/private/ldap_server.key

    $ service slapd restart
    $ ldapmodify -H ldapi:// -Y EXTERNAL -f addcerts.ldif

    強制TLS:

    1
    2
    3
    4
    5
    6
    7
    8
    $ vim /var/ldap/forcetls.ldif

    dn: olcDatabase={1}mdb,cn=config
    changetype: modify
    add: olcSecurity
    olcSecurity: tls=1

    $ ldapmodify -H ldapi:// -Y EXTERNAL -f /var/ldap/forcetls.ldif

    最後別忘了:

    1
    2
    3
    4
    $ vim /etc/ldap/ldap.conf

    ...
    TLS_CACERT /etc/ssl/certs/ca_server.pem

    :::warning

    這個方法有點小髒,更好的作法是先在 Consumer 弄好公鑰私鑰,在讓 CA 為此公鑰簽署證書會更好,這是為了簡化流程。
    :::

  4. As Client

    1
    2
    3
    4
    5
    6
    $ vim /etc/ldap/ldap.conf

    ...
    BASE dc=40,dc=nasa
    URI ldap://ldapconsumer.40.nasa ldap://ldapprovider.40.nasa
    ...

事前準備工作這樣就做完了。

前提

後面步驟很容易做爛,LDAP 要 UNDO 設定又很麻煩,建議先做個快照,防止你做爛了之後怪我。

在 Provider 上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ vim sync.ldif

dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: syncprov.la

dn: olcOverlay=syncprov,olcDatabase={1}mdb,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcSyncProvConfig
olcOverlay: syncprov
olcSpCheckpoint: 100 10
olcSpSessionLog: 100

dn: cn=config
changetype: modify
replace: olcServerID
olcServerID: 1

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcSyncRepl
olcSyncrepl: rid=010
provider=ldap://ldapconsumer.40.nasa
binddn="cn=admin,dc=40,dc=nasa"
bindmethod=simple
credentials=nasa
searchbase="dc=40,dc=nasa"
type=refreshAndPersist
schemachecking=on
retry="5 5 300 +"
timeout=1
starttls=critical tls_reqcert=demand
-
add: olcMirrorMode
olcMirrorMode: TRUE
-
add: olcDbIndex
olcDbIndex: entryUUID eq
-
add: olcDbIndex
olcDbIndex: entryCSN eq


按照我粗糙的理解:

一開始我們需要引入 syncprov.la 這個模組,這是一個已經被實作好的模組,這個模組會去做到從另一台機器上面同步的工作。

overlay 的意思有點像是 hook,就是設定當某件事情發生的時候,你應該要做哪些事情;然後要為 syncprov 進行設定我們必須定義一個 syncprov 的 overlay,在此 overlay 中寫出那些詳細的設定:例如 SpCheckpoint 100 10 代表如果進行了 100 次寫入操作或是每過了 10 分鐘,他就會寫入一個新的 contextCSN,我們可以把 contextCSN 想成一個版本號,代表某個 DB 當前的狀態有多新;而 SpSessionLog 代表有多少筆 write 紀錄應該要被紀錄起來,而會需要紀錄這個的原因,是為了優化 pull-based 的同步,老實講我沒有完全理解為何這樣子能夠優化:

Syncrepl supports both pull-based and push-based synchronization. In its basic refreshOnly synchronization mode, the provider uses pull-based synchronization where the consumer servers need not be tracked and no history information is maintained. The information required for the provider to process periodic polling requests is contained in the synchronization cookie of the request itself. To optimize the pull-based synchronization, syncrepl utilizes the present phase of the LDAP Sync protocol as well as its delete phase, instead of falling back on frequent full reloads. To further optimize the pull-based synchronization, the provider can maintain a per-scope session log as a history store. In its refreshAndPersist mode of synchronization, the provider uses a push-based synchronization. The provider keeps track of the consumer servers that have requested a persistent search and sends them necessary updates as the provider replication content gets modified. 18.1.1. LDAP Sync Replication

然後我們設定 olcSyncRepl 的參數,例如 provider 代表 provider 伺服器的位置,而 schemacheckin 是說建立連線前是否要檢查兩邊的 schema 是否一致,這是為了確保資料不會同步過去之後發現對方根本沒有辦法正確的解析,還有例如 starttls=critical,因為我們有 forcetls,沒有這個設定會導致無法順利建立連線。

接著我們又為 mdb 這個 backend 增加一個屬性叫做 olcMirrorMode,代表說 syncrepl 在做同步的時候會採取 MirrorMode,這是因為 Spec 有要求讓 Consumer 可以對資料庫進行 update。

syncrepl 支援數種同步模式,最基本的 Sync 只能做到讓 Consumer 複製 Provider 的資料,然後當使用者要向 Consumer 做修改時,Consumer 只會發一個 referral,告訴使用者應該向哪台伺服器發 modify 請求。

而 MirrorMode 支援 multi-provider,讓兩個 provider 互相取得對方的更新,此時兩台都是 provider 也都是 consumer。其他的功能、特點可以在 Docs 中看的更詳細。

而至於 backend,backend 可以想成一個資料結構,這個資料結構負責儲存、讀取那些保存在電腦上的資料,其實我們看到的 slapd 就是一個中間人,slapd 負責與使用者溝通,而真正在處理資料的是 backend。

而 backend 有非常多種類型,比如 config 是專門儲存設定檔資料的 backend;還有像是 mdb,mdb 是一種高效率的 memory-mapped 的 key-value 資料庫,這邊設定 mdb 也是因為我的 ldap 裝下來便是採用 mdb 這種 backend。

所以此時我要對 {1}mdb 這個 backend 進行這些設定,因為我知道我的資料是存放在 {1}mdb:Where is my data (directories) store by slapd (OpenLDAP) on ubuntu?,我不確定這個指令還能不能用,因為我忘記我是用哪一個指令抑或是在哪一個文件中看到它的。

olcDbIndex 是為某一個 attribute 來建立一種資料結構,例如 olcDbIndex: entryUUID eq 可以來快速方便的找到某筆 entry 且這筆 entry 的 entryUUID 恰為某個值。會需要這個這其實是因為 syncrepl 的 docs 中寫說:

Note that using the session log requires searching on the entryUUID attribute. Setting an eq index on this attribute will greatly benefit the performance of the session log on the provider.18.3.1.2. Set up the provider slapd

而至於 olcDbIndex: entryCSN eq,CSN 的意思剛剛有解釋過,而會需要這個的原因,要看這一段敘述:

Note that at startup time, if the provider is unable to read a contextCSN from the suffix entry, it will scan the entire database to determine the value, and this scan may take quite a long time on a large database. When a contextCSN value is read, the database will still be scanned for any entryCSN values greater than it, to make sure the contextCSN value truly reflects the greatest committed entryCSN in the database. On databases which support inequality indexing, setting an eq index on the entryCSN attribute and configuring contextCSN checkpoints will greatly speed up this scanning step. 18.1.1.2. Syncrepl Details

值得一提的事是,ldap 不存在一個 olcDbIndex 的方式是不等式的 index,意味著其實我們沒辦法直接的設定讓 entryCSN 可以快速的進行不等式查找,但這並不意味著做不到,事實上 Docs 有說明這一件事情:

There is no index keyword for inequality matches. Generally these matches do not use an index. However, some attributes do support indexing for inequality matches, based on the equality index. 5.2.6.7. olcDbIndex

我沒有找到更進一步的佐證說明 entryCSN 的確是,因為這可能要去看 ldap 在 eq-index 上面的實作以及 entryCSN 的定義,但我覺得既然這樣寫,應該就是了。

接下來我們會需要套用這個設定,使用下面的指令:

1
$ ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f sync.ldif

在 Consumer 上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ vim sync.ldif

dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: syncprov.la

dn: olcOverlay=syncprov,olcDatabase={1}mdb,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcSyncProvConfig
olcOverlay: syncprov
olcSpCheckpoint: 100 10
olcSpSessionLog: 100

dn: cn=config
changetype: modify
replace: olcServerID
olcServerID: 1

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcSyncRepl
olcSyncrepl: rid=010
provider=ldap://ldapconsumer.40.nasa
binddn="cn=admin,dc=40,dc=nasa"
bindmethod=simple
credentials=nasa
searchbase="dc=40,dc=nasa"
type=refreshAndPersist
schemachecking=on
retry="5 5 300 +"
timeout=1
starttls=critical tls_reqcert=demand
-
add: olcMirrorMode
olcMirrorMode: TRUE
-
add: olcDbIndex
olcDbIndex: entryUUID eq
-
add: olcDbIndex
olcDbIndex: entryCSN eq

這邊做的事情和 Provider 一模一樣,其實也很合理,在 MirrorMode 中,兩台機器幾乎是平等的,唯一有差的事情是有用 acl 來限制 Consumer 去讀取 syncuser。

1
$ ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f sync.ldif

檢查

1
$ slapcat

看一下有沒有確實資料都被引過來。

設定 ACL

:::info

【請不要使用紅色方框內的方法】

1
2
3
4
5
6
7
8
9
10
11
12
$vim /var/ldap/acl.ldif

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0} to attrs=userPassword
by * auth
olcAccess: {1} to dn.one="ou=People,dc=40,dc=nasa"
by self write
by * read
olcAccess: {2} to dn.subtree="dc=40,dc=nasa"
by * read
1
$ ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f /var/ldap/acl.ldif

:::

:::danger

【這個紅色方框裡面的實作是錯誤的方法,請參考藍色方框的方法】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ vim /var/ldap/acl.ldif

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to dn.exact="cn=syncuser,dc=40,dc=nasa"
by dn.base="cn=admin,dc=40,dc=nasa" read
by * none
olcAccess: {1} to attrs=userPassword
by * auth
olcAccess: {2} to dn.one="ou=People,dc=40,dc=nasa"
by self write
by * read
olcAccess: {3} to dn.subtree="dc=40,dc=nasa"
by * read

$ ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f sync.ldif

錯誤原因:

這邊禁止別人在 consumer 訪問 syncuser 是一種繞過 judge 的招數而已,並不是 spec 預期我們正確的行為。但我一開始忘記設置 Provider 的 ACL,資料就已經被同步過來了。而現在我已經修正了 Provider 的 ACL,資料已經不會在被同步過來了,所以此時這一條限制就沒有意義了。所以請參考在上面藍色方框的作法。

順道附上當時的 Provider ACL,足以說明差別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Provider
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0} to attrs=userPassword
by self write
by * auth
olcAccess: {1} to *
by dn.exact="cn=syncuser,dc=40,dc=nasa" write
by * break
olcAccess: {2} to dn.one="ou=People,dc=40,dc=nasa"
by self write
by * read
olcAccess: {3} to dn.subtree="dc=40,dc=nasa"
by * read

可以發現沒有針對 syncuser 讀取 syncuser 作限制,所以 syncuser 自然會把自己這筆 entry 讀進去,而事實上我們創立 syncuser 僅是為了讓 consumer 可以 bind 上 provider,並不是為了讓 consumer 可以 sync 這一筆資料,所以我們在正確的 ACL 設定當中才會加上下面這一筆限制:

1
2
3
to dn.exact="cn=syncuser,dc=40,dc=nasa"
by dn.exact="cn=syncuser,dc=40,dc=nasa" none
by * read

:::

這部分的設定我是先參考這篇文章做成之後,我再去讀文檔搞懂的,說來慚愧,我僅能勉強搞懂當中的參數,還未有能憑空生出這些設定檔的能力。

Shell Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/bin/bash

admin="cn=admin,dc=40,dc=nasa"

read -s -p "Enter the LDAP password: " ldappw

# check the password
echo
ldapwhoami -xZD $admin -w $ldappw > /dev/null || exit $?

if [ "$1" == "" ]
then
read -p "Enter the username: " name

# check if the new username is avaliable
if [ "$(ldapsearch -xZb "uid=$name,ou=People,dc=40,dc=nasa" -LLL -D $admin -w $ldappw 2>/dev/null)" != "" ]
then
echo -n "User exists: "
id $name
exit 1
fi

read -s -p "Enter the password: " password

echo

IFS= searchResult=$(ldapsearch -xZb 'ou=People,dc=40,dc=nasa' -LLL -D $admin -w $ldappw '(&(objectclass=account)(uidNumber>=4000))' uidNumber | grep -v "dn" | grep -vE '^$' | awk '{print $2}' | sort)
newUID=4000

while read i
do
if [ "$i" == "$newUID" ]
then
newUID=$((newUID + 1))
fi
done <<< $searchResult

ldapadd -xZD $admin -w $ldappw > /dev/null <<< \
"dn: cn=$name,ou=Group,dc=40,dc=nasa
objectClass: posixGroup
cn: $name
gidNumber: $newUID
memberUid: $name
memberUid: stu40

dn: uid=$name,ou=People,dc=40,dc=nasa
uid: $name
userPassword: $password
uidNumber: $newUID
cn: $name
gidNumber: $newUID
homeDirectory: /u/nasa/$name/
objectClass: posixAccount
objectClass: shadowAccount
objectClass: account" || exit $?

echo -n "User $name created: "
id $name

echo
echo -n "Enter the stu40's "
su -c "mkdir /u/nasa/$name && chown -R :$name /u/nasa/$name" stu40 > /dev/null

if [ "$?" != "0" ]
then
echo "Unable to create directory /u/nasa/$name"
exit 1
fi
echo "Directory /u/nasa/$name created."

else
# to be continued
echo
fi

這邊有個點在於 /u/nasa/ 是 nfs mount 上的,因為 server 端有設定,所以你 root 權限在 /u/nasa 裡面會變成 nobody,你僅能使用 stu40 的身分對此目錄進行操作,且也無法將 /u/nasa/{user} 的 owner 設為 {user},因為 chown 是只有 root 才能下的指令,所以我們只能將 stu40 加入到 {user} 所在的群組,再 chgrp,使得 {user} 可以完整讀寫 /u/nasa/{name} 這個目錄。

於此,我沒有想到更好的作法。

Debug

  1. 可以為 slapd 開啟 log
    1
    2
    3
    4
    5
    6
    $ vim /var/ldap/log.ldif

    dn: cn=config
    changetype: modify
    replace: olcLogLevel
    olcLogLevel: stats sync
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ vim /etc/rsyslog.d/30-ldap.conf

    # LDAP logs
    local4.* -/var/log/ldap.log

    # Uncomment the following to stop logging anything that matches the last rule.
    # Doing this will stop logging kernel generated UFW log messages to the file
    # normally containing kern.* messages (eg, /var/log/kern.log)
    & ~
    1
    $ service rsyslog restart
    遇到 bug 會比較好 de 一點。