LDAP のサーバ証明書がプライベートCAで発行されているため、素のままだと ldaps で証明書の検証で失敗してアクセス出来ず、PHP 側で信頼するCA証明書を設定しようと試行錯誤したメモ。
簡単な解決方法・・ダメ
LDAP_OPT_X_TLS_CACERTFILE
というオプションがあるのでこれで指定すれば OK です、完。
<?php $ldap = ldap_connect('ldaps://ldap.local:636'); ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($ldap, LDAP_OPT_X_TLS_CACERTFILE, '/mnt/ldap.crt'); ldap_bind($ldap, 'cn=admin,dc=example,dc=org', 'password');
というわけにはいきませんでした。なにやらバグ?により機能していないようです。
サーバの設定ファイルでどうにかする
サーバの証明書ストアに追加したり、
cp ldap.crt /etc/pki/ca-trust/source/anchors/ update-ca-trust
ldap.conf に追記したり。
echo TLS_CACERT /mnt/ldap.crt >> /etc/openldap/ldap.conf
ユーザーごとの .ldaprc でも良いようです。
echo TLS_CACERT /mnt/ldap.crt > ~/.ldaprc
PHP 側でどうにかする
環境変数 LDAPTLS_CACERT
でサーバ証明書を指定すれば大丈夫です。
<?php putenv('LDAPTLS_CACERT=/mnt/ldap.crt'); $ldap = ldap_connect('ldaps://ldap.local:636'); ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_bind($ldap, 'cn=admin,dc=example,dc=org', 'password');
あるいは環境変数で LDAPTLS_REQCERT=never
を指定すれば証明書関係のエラーを無視できます。
<?php putenv('LDAPTLS_REQCERT=never'); $ldap = ldap_connect('ldaps://ldap.local:636'); ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_bind($ldap, 'cn=admin,dc=example,dc=org', 'password');
もしくは・・・ ldap_set_option
で LDAP_OPT_X_TLS_CACERTFILE
を指定するときに第1引数に null を指定して libldap のグローバルなオプションにすれば良いようです。
<?php ldap_set_option(null, LDAP_OPT_X_TLS_CACERTFILE, '/mnt/ldap.crt'); $ldap = ldap_connect('ldaps://ldap.local:1636', 1636); ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_bind($ldap, 'cn=admin,dc=example,dc=org', 'password');
null ではなく LDAP リソースを渡すとダメなのですが・・次のような C のコードでも同様だったので PHP の LDAP 拡張の問題ではないようです。
#include <stdio.h> #include <stdlib.h> #include <ldap.h> void check(int rc) { if (rc != LDAP_SUCCESS) { printf("%d: %s\n", rc, ldap_err2string(rc)); exit(1); } } int main() { LDAP *ldap = NULL; check(ldap_initialize(&ldap, "ldaps://ldap.local:1636")); check(ldap_set_option(NULL, LDAP_OPT_X_TLS_CACERTFILE, "/mnt/ldap.crt")); int v = 3; check(ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &v)); char pw[] = "password"; struct berval cred = {strlen(pw), pw}; check(ldap_sasl_bind_s(ldap, "cn=admin,dc=example,dc=org", LDAP_SASL_SIMPLE, &cred, NULL, NULL, NULL)); return 0; }
最初に PHP の LDAP 拡張のバグかと思ったものの、実は libldap のバグあるいは仕様のようです(どっちなのかはわからない)。
おまけ:DNS SRV RR
通常、Active Directory のドメインコントローラーは複数台設け、DNS サーバが SRV リソースレコードを複数返すことで冗長化が図られています。
ので、Active Directory の LDAP サーバに問い合わせるなら、直接ドメインコントローラーの FQDN を名前解決して問い合わせるよりは、ドメインの SRV レコードを検索してドメインコントローラーの FQDN を特定したいものですが・・実は openldap の CLI も SRV レコードをサポートしています。
例えば次のように指定すると _ldap._tcp.example.com
の SRV レコードが検索され、その結果を元に LDAP 問い合わせが行われます。
ldapwhoami -H ldap:///dc%3Dexample%2Cdc%3Dcom -x -D ore@example.com -W
-H
オプションで指定した値は URL デコードすると ldap:///dc=example,dc=com
です。
なお ldaps:///dc%3Dexample%2Cdc%3Dcom
のように指定しても LDAPS ではアクセスできません。このように指定しても SRV レコードの問い合わせは _ldap._tcp.example.com
のままです。SRV レコードにはホスト名だけでなくポート番号 389 も含まれています。そのため LDAPS で 389 ポートにアクセスして失敗します。
http/https で言うところの https://example.com:80/
のようなものと考えてもらえればわかりやすいと思います。
下記によると openldap の CLI では SRV レコードを解決しつつの LDAPS アクセスは出来ないようです。
次のように -ZZ
を使えば STARTTLS には出来ます。
ldapwhoami -H ldap:///dc%3Dexample%2Cdc%3Dcom -x -D ore@example.com -W -ZZ
なお、この機能、そもそものところ PHP の LDAP 拡張では実装されていません、ちーん。
次のように SRV レコードを問い合わせても良いのかも?
$records = dns_get_record('_ldap._tcp.example.com', DNS_SRV); usort($records, function ($a, $b) { $cmp = $a['pri'] <=> $b['pri']; if ($cmp != 0) { return $cmp; } $cmp = random_int(0, $a['weight']) <=> random_int(0, $b['weight']); return -$cmp; }); foreach ($records as $rec) { $host = $rec['target']; $port = $rec['port']; try { // なにかする break; } catch (Throwable) { continue; } }