KerberosのバックグラウンドにOpenLDAPを使う

# ネタ振り
この記事で目標とするサービスの概要は、以下の通りです。

環境: Debian GNU/Linux 9 (stretch) on Google Cloud Platform
<img src=https://qiita-image-store.s3.amazonaws.com/0/126131/b51838a4-616c-ef2b-c77f-93cf816f21a2.png width=60% />
この他、DNSとしてBIND9を立てます。

以下の説明では、${xxx}となっているところは、xxxに従って、自分の環境に合わせて設定してください。また、説明が不十分だと思われた方は、https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_ldap.html をみると良いかもしれません。

では、いってみよう。

# インストール
## Kerberos関係

```shell
apt install krb5-admin-server krb5-kdc krb5-kdc-ldap
```
>
krb5-admin-server/stable,now 1.15-1+deb9u1 amd64 [installed]
  MIT Kerberos master server (kadmind)
</br>
krb5-kdc/stable,now 1.15-1+deb9u1 amd64 [installed]
  MIT Kerberos key server (KDC)
</br>
krb5-kdc-ldap/stable,now 1.15-1+deb9u1 amd64 [installed]
  MIT Kerberos key server (KDC) LDAP plugin
(apt searchの結果より引用)

## OpenLDAP関係

```shell
apt install ldap-utils slapd
```
>
ldap-utils/stable,now 2.4.44+dfsg-5+deb9u2 amd64 [installed]
  OpenLDAP utilities
</br>
slapd/stable,now 2.4.44+dfsg-5+deb9u2 amd64 [installed]
  OpenLDAP server (slapd)
(apt searchの結果より引用)

インストール時に色々設定を聞かれますが、以下のように答えてください。

- DNS dmain name: ${domain}
- Organization name: ${org}
- Administrator password: ${admin passwd}
- Database background: MDB

設定を間違えたときは、

```shell
dpkg-reconfigure slapd
```

で変更できます。

## BIND9関係

```shell
apt install bind9
```

# OpenLDAPの設定
OpenLDAPの設定方法が最近変更されました。古い記事では、slapd.confで設定しているのですが、現在は非推奨です。この記事では、現在推奨されている、dynamic runtime configuration engineを使う方法で設定していきます。
(参考: https://www.openldap.org/doc/admin24/slapdconf2.html)

この方法では、設定を(少なくともクライアントからは)他のデータと同じようにデータベースに格納します。そのときに使われるAttributeはolc(OpenLDAP Configuration)というprefixで始まることが多いです。

## TLSの有効化
まずは、TLSに使う証明書を作成します。Debian系のLinuxのパッケージからOpenLDAPをインストールすると、OpenLDAPはOpenSSLでなくGnuTLSを使ってコンパイルされていることが多いので、今回はGnuTLS系のコマンドのcertutilを使って鍵を作成します。

TLSの設定に関しては、
https://qiita.com/JhonnyBravo/items/b18341b4cf20b9d865e4#tls
または、
https://www.openldap.org/doc/admin24/tls.html
を見てください。

参考までに、僕が使ったldifファイルを置いておきます。

```tls.ldif
dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/ca.crt
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/ldap.crt
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/ldap.key
-
add: olcTLSDHParamFile
olcTLSDHParamFile: /etc/ldap/dhparam.pem
-
add: olcTLSVerifyClient
olcTLSVerifyClient: allow
```

上手くいかないときは、以下のポイントを確認してみてください。

- Subjectのcnがサーバーのドメインと一致しているか

例えば、

```shell
openssl x509 -text -noout -in ${server cert} | grep Subject
# Subject: CN = ${domain}, ...
```

のとき、\${domain}がサーバーのドメインと一致していればOK。

- CAの公開鍵のKey Usageにcaが入っているか

例えば、

```shell
openssl x509 -text -noout -in ${ca cert} | grep CA
# CA=True
```
ならOK。

- Server証明書がSelf-signedでないか

例えば、

```shell
openssl x509 -text -noout -in ${server cert} | grep Issuer
openssl x509 -text -noout -in ${server cert} | grep Subject
```
の結果が一致していなければOK。

本当は、TLSでClient認証もできるらしいのですが、gnu_tls周りの処理が上手くいかなかった*ので省略します。Client認証では、Client証明書を電子署名に使うので、Key Usageにdigital signitureを追加しておくと良いです。

\* サーバー側で出る`gnutls_certificate_verify_peers2 failed -49`というエラーの解決法が分かりませんでした。調べたところ、ldapsearch等のクライアントが証明書をサーバーに渡していないようなので、サーバー側の問題ではなく、クライアントの問題のようです。そこまでは分かったのですが、詳細は不明です。手がかりとなる情報を求めています。jxplorerで試そうとも思いましたが、鍵の登録がうまくいかず、結局断念しました。

## LDAPサーバーの起動

```shell
systemctl enable slapd # 次回以降のブート時に起動
systemctl start slapd # 今すぐ起動
```

を実行してください。

ファイヤーウォール等を設定している人は、設定の変更をしてください。(OpenLDAPに限らず)LDAPサーバーが用いるスキーマにはldapとldaps(ldap over TLS)およびldapiがあります。

|schema|port|
|:-:|:-:|
|ldap|389|
|ldaps|636|

この記事ではTLSを有効化した上でOpenLDAPと接続するので、636番ポートはクライアントからアクセスできるようにしてください。

ldapiスキーマについては、[余談 ldapiスキーマ](#ldapiスキーマ)を参照してください。

## LDAPクライアントのインストール

OpenLDAPにデータを登録するのは、コマンドラインからでもできるのですが、GUIクライアントもたくさんあるので、ここではGUIで登録を行います。そのために、まずクライアントソフトウェアをインストールします。

クライアントはなんでも良いのですが、僕はjxplorerを選びました。ちなみに、僕のクライアント用環境はmacOS High Sierraです。jxplorerの使い方についての詳細は割愛しますが、接続するときに使うユーザーについてだけ書いておきます。

OpenLDAPクライアントに接続するユーザーはDN(distinguished name)で選択するのですが、それは

```shell
sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b cn=config olcDatabase={1}mdb 2>/dev/null | grep olcRootDN
# olcRootDN: ${admin dn}
```

を実行したときに表示される\${admin dn}を用いてください。パスワードはインストール時に設定した\${admin passwd}になります。

## Kerberos用schemaの読み込み

```shell
sudo ldapadd -H ldapi:/// -Y EXTERNAL -f /etc/ldap/schema/kerberos.ldif
```

## Kerberos用エントリーの作成
KerberosサーバーがOpenLDAPにアクセスするときに使う(bindする)エントリーを作成します。adminサーバー用のエントリー(dn: \${kadmin dn})と、kdcサーバー用のエントリ(dn: \${kdc dn})を作成します。それぞれのエントリーが満たすべき条件は、

- simpleSecurityObject ObjectClassを含むこと
- userPassword attributeが設定されていること

です。ここで設定したuserPasswordはKerberosを設定するときに使います。

また、Kerberosが情報を格納するのに使う、containerを作っておきます。要件は、

- krbContainer ObjectClassを含むこと

です。

さらに、後でユーザーを追加したときにそのユーザーの情報が格納される場所を作ります。要件は、

- containerっぽいObjectClassを含むこと(organizationとか)

です。
 
## ACL(Access ControL)の設定

ACL設定を書き込むためのldifファイルを作ってください。その際、先ほど作った\${kadmin dn}と\${kcd dn}がKerberos containerとrealmのsubtreeに対するwrite権限を持つようにしてください。

ACL設定の評価方法としては、一番初めにマッチしたものが採用されます。このことに注意してファイルを作ってください。複雑な設定を作ろうとすると結構ハマります。

例えば、最も簡単な場合は、以下のようなファイルになります。

```
dn: olcDatabase={1}mdb,cn=config
changetype: delete
delete: olcAccess

dn: olcDatabase={1}mdb,cn=config
changetype: add
add: olcAccess
olcAccess: {0}to attrs=shadowLastChange
           by self write
           by * read
olcAccess: {1}to dn.subtree="${krb container}"
           by dn.exact="${kadmin dn}" write
           by dn.exact="${kdc dn}" write
           by * none
olcAccess: {2}to attrs=userPassword
           by self write
           by anonymous auth by * none
olcAccess: {3}to * by * read
```

これを任意の場所に拡張しldifを付けて保存します(\${ldif file})。次に、以下のコマンドを実行し、LDAPデータベースを更新します。

```shell
sudo ldapmodify -H ldapi:/// -Y EXTERNAL -f ${ldif file}
```

以上でACLの設定は終わりです。参考までに、僕のLDAPエントリを載せておきます。

```
# sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b cn=config olcDatabase={1}mdb

dn: olcDatabase={1}mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcSuffix: dc=${domain}
olcLastMod: TRUE
olcRootDN: cn=admin,dc=${domain}
olcRootPW: {SSHA}xxxxxxxxxxxxxxxxxxxxxxx
olcDbCheckpoint: 512 30
olcDbIndex: objectClass eq
olcDbIndex: cn,uid eq
olcDbIndex: uidNumber,gidNumber eq
olcDbIndex: member,memberUid eq
olcDbMaxSize: 1073741824
olcAccess: {0}to attrs=shadowLastChange by self write by * read
olcAccess: {1}to attrs=userPassword
           by self write by anonymous auth
           by * none
olcAccess: {2}to dn.base="dc=gc"
           by dn.exact="cn=admin_server,dc=gc" write
           by dn.exact="cn=kdc,dc=gc" write
           by * break
olcAccess: {3}to dn.subtree="cn=kerberos,dc=gc"
           by dn.exact="cn=admin_server,dc=gc" write
           by dn.exact="cn=kdc,dc=gc" write
           by * break
olcAccess: {4}to dn.subtree="o=main,dc=gc"
           by dn.exact="cn=admin_server,dc=gc" write
           by dn.exact="cn=kdc,dc=gc" write
           by * break
olcAccess: {5}to dn.subtree="o=test,dc=gc"
           by dn.exact="cn=admin_server,dc=gc" write
           by dn.exact="cn=kdc,dc=gc" write
           by * break
olcAccess: {6}to * by * read
```

ACLについては、[OpenLDAP Software 2.4 Administrator's Guide](https://www.openldap.org/doc/admin24/access-control.html#Access%20Control%20via%20Dynamic%20Configuration)を参照。breakはそこには書いてないので、https://www.openldap.org/faq/data/cache/454.html を参照。

# BINDの設定
## named.conf
僕の環境では、デフォルトでnamed.confはいくつかのファイルに別れており、named.confファイルでは、それらのファイルをincludeして纏めているようでした。

ファイル名と設定内容の組み合わせは以下の通りにしました。

|ファイル名|設定内容|
|:--|:--|
|named.conf.options|global option|
|named.conf.locals|zone情報|
|named.conf.acl|access control(自作)|
|named.conf.logging|log設定(自作)|

セキュリティ上、気にすべき点がいくつかあります。

- DNS cache poisoning対策
  - DNSSEC
  - 返答portのランダム化
  - recursionリクエスト送信元の限定
- DNS amplified Dos対策
  - リクエスト送信元の限定

個人でBINDを立ち上げるときは、ローカルネットワーク用であることが多いと思いますので、そもそも外部公開しないようにするのも効果があります。また、好き勝手ドメイン名を決められるので、トップレベルドメインを操作することもできなくないため、DNSSEC対応が簡単です。

ローカルネットワークに限定する方法として、BIND9には受信NIC(正確にはリクエストが送られてきたIPアドレス)によるアクセス制御ができるので、ローカルネットワークに接続しているNICからのみリクエストを許可するといいでしょう。このような仕組みを提供するオプションは、基本的に"on"で終わる名前です。

受信NICによるアクセス制御について、もう少し深掘りしてみました。([余談 BIND9 受信IP](#bind9-受信ip))

DNSSECはまだ導入していないので、ここでは省略します。

## zone情報
サーバーのドメイン情報を登録します。ここでは、AレコードとSRVレコードを登録します。SRVレコードについて、サービス名は[IANAのサイト](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml)で検索してみてください。

以下に、僕が使っているゾーン情報を載せておくので、テンプレート代わりにしてください。注意点としては、mainとldap-master、krb-masterのIPアドレスが同じだからといって、CNAMEにしてはならないことです。SRVレコードに使うドメイン名がCNAMEであることは禁止されています(ただし、実際には動くことが多いようですが)。

```db.gc
$TTL	604800
@	IN	SOA	ns root.gc. (
			      1		; Serial
			 604800		; Refresh
			  86400		; Retry
			2419200		; Expire
			 604800 )	; Negative Cache TTL
;
@	IN	NS	ns
ns	IN	A	172.16.0.2
main	IN	A	172.16.0.2
ldap-master	IN	A	172.16.0.2
krb-master	IN	A	172.16.0.2

_ldap._tcp 	IN 	SRV 	0 5 389 	ldap-master
_ldaps._tcp	IN	SRV	0 5 636		ldap-master
_kerberos._tcp	IN	SRV	0 5 88		krb-master
_kerberos-adm._tcp	IN	SRV	0 5 749	krb-master
```

## 起動

```shell
systemctl enable bind9
systemctl start bind9
```

## 使用するDNSサーバーの設定

どのDNSサーバーを使うかの設定は、今までresolv.confファイルで行なっていましたが、最近はNetworkManagerの台頭とともに、nmcliコマンドで行うことも多くなりました。そこで、この記事ではnmcliコマンドで行おうと思います。とはいえ、普通は簡単ですが。。

```shell
nmcli c down ${nic}
nmcli c m ipv4.ignore-auto-dns yes ipv4.dns ${dns IP-addr}
nmcli c up ${nic}
service network-manager restart
```

以上です。resolv.confファイルを見てみて、上手く設定できているか確認してください。

上手く設定できていない方は、普通とは違う特別コースを行く必要があります。例えば、先ほどのコマンドの\${nic}という部分が実はnicの名前ではないとかいうことを意識する必要があるかもしれません。それについては、[nmcliコマンドの隅をつつく](https://qiita.com/babyron64/items/b7b8eca17202386d27e3)を参考にしてください。

# Kerberosの設定

さて、やっと最後の設定です。

## krb5.conf

重要なところだけピップアップしました。詳しくは、https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html を見てください。

```
[libdefaults]
	default_realm = ${default domain}

[realms]
   ${realm domain} = {
        kdc = ${kdc domain} # なくても良い
        admin_server = ${kadmin domain}
        database_module = openldap_ldapconf
        default_domain = ${domain}
   }

[domain_realm]
	{domain} = {realm domain}

[dbmodules]
    openldap_ldapconf = {
        db_library = kldap
        ldap_kdc_dn = ${kdc dn}
        ldap_kadmind_dn = ${kadmin dn}
        ldap_kerberos_container_dn = ${krb container dn}
        ldap_service_password_file = /etc/krb5kdc/service.keyfile
        ldap_servers = ldaps://{ldap domain}
        ldap_conns_per_server = 5
    }
```

realmに関する設定のうち、kdcのドメイン名は設定しなくても良いです。これは、BIND9についての説明通りSRVレコードを設定していれば、そのレコードを使って自動取得してくれるからです。ただし、そのためにはrealmの名前とドメイン名とが[domain_realm]で対応づけられている必要があります。

理論的にはadmin_serverのほうも不要ですが、Kerberosの実装が不完全らしく、現在のところは書かなくてはならないそうです。

> Note that the admin_server entry must be in the krb5.conf realm information in order to contact kadmind, because the DNS implementation for kadmin is incomplete.
(https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html より)

## krb5kdc.conf

```
[kdcdefaults]
    kdc_ports = 750,88

[realms]
    ${realm domain} = {
        admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
        key_stash_file = /etc/krb5kdc/stash
        acl_file = /etc/krb5kdc/kadm5.acl
        max_life = 10h 0m 0s
        max_renewable_life = 7d 0h 0m 0s
        default_principal_flags = +preauth
    }
```

とくに説明はいらないでしょう。詳しくは、https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/kdc_conf.html を見てください。

## LDAPサーバーに対する認証
KerberosサービスがLDAPにアクセスするときに、いちいちパスワードを入力するのはあまりに不便なので、事前にパスワードをstashして(Kerberosサーバーに保持させて)おきます。

```shell
kdb5_ldap_util stashsrvpw ${kadmin dn}
kdb5_ldap_util stashsrvpw ${kdc dn}
```

## Realmの作成
```shell
kdb5_ldap_util -D ${admin dn} create -subtrees ${princ container dn} -r ${realm domain} -s
```

\${admin dn}はLDAPデータベースの管理ユーザーのDNです。

今作ったrealmに新しくprincipalを作ると、\${princ container dn}以下にそのprincipalの情報が保存されます。

## 管理者principalの作成

kadmin.localコマンドを使って、Kerberosの管理者用principalを作ります。似たコマンドに、kadminというのがありますが、これはリモート用で、ローカルの場合はkadmin.localを使うという使い分けをします。管理者用principalを設定する前は、kadminは使えないと思います。

まず、管理者用principalを作ります。

```shell
kadmin.local # 対話モードで起動
kadmin.local: add-princ ${name}/admin
kadmin.local: exit
```

## kadm5.acl

次に、今作ったprincipalの権限を設定します。それには、kadm5.aclファイルを書き変えます。

```kadm5.acl
*/admin@*   *
```

この設定で、全てのrealmにおいて、*/adminにマッチするprincipalはそのrealmにおける全権限をもつということを設定できました。つまり、管理者ということです。

## firewallの設定

firewallを使用している場合は、次のポートを開けてください。

|port|サービス名([IANA](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml))|用途|
|:--|:--|:--|
|88|kerberos|kdc server|
|749|kerberos-adm|admin server|
|464|kpasswd|kpasswd server|

## サーバーの起動

kadminサーバーとkdcサーバーを起動させます。

```shell
sudo systemctl enable krb5-kdc
sudo systemctl start krb5-kdc

sudo systemctl enable krb5-admin-server
sudo systemctl start krb5-admin-server
```

接続をテストします。

```shell
kadmin -p ${name}/admin
```

対話モードに入れたら成功です。

これで、サーバーのセットアップは終了です。長くなってしまったので、この記事はここまでとします。サービスに対するkerberos認証の提供方法は、また今度の記事でということにします。

# 余談
## ldapiスキーマ
記事の中で、ldapスキーマとldapsスキーマについては触れたが、ldapiスキーマについては触れていない。だが、記事で最も多く登場したのがldapiスキーマだと思います。

```shell
sudo ldapsearch -H ldapi:/// -Y EXTERNAL ...
```

ここに登場してたのです。これはどういうスキーマかというと、ldap通信をUnixソケットを使って行うものです。実は、デフォルトの状態では、通常のTCPを使う方法でcn=config下の設定ファイルを変更することができません。必ずldapiスキーマを使う必要があります。さらに、かならずrootとして実行する必要があります。

それを確認するために、まずはcn=config下のデータベースのACLがどうなっているかを確認しましょう。それには、

```shell
sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b olcDatabase={0}config,cn=config -s base
```

を実行してみます。すると、

```
dn: olcDatabase={0}config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: {0}config
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external
 ,cn=auth manage by * break
olcRootDN: cn=admin,cn=config
```

のような結果が得られます。このうち、olcAccess attributeがconfig下のアクセス制御を行なっています。見ると、DNがgidNumber=0+uidNumber=0,cn=peercred,cn=external
 ,cn=authであるユーザーはmanage権限(全権)を持つことがわかります。このDNこそ、ldapiスキーマでrootとしてアクセスしたときに付与されるDNです。このことは、先ほどのコマンドを実行したときに、

```
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
```

と表示される(今までは省略していましたが)ことから分かります。そして、olcAccess attributeはそのDNにのみ権限を与えているので、それ以外の方法でcn=config下のデータベースにアクセスすることはできないと分かります(最後についているbreakについては記事に参考リンクを載せてあります)。

## ldap-utilsに入っているクライアントとTLS

```
ldapsearch -H ldaps:/// -D ${admin dn} -W -b ${base dn}
```

としないように注意してください。これではサーバー認証が通りません。

```
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
```

と怒られます。エラーがあまりにざっくりとしているので、サーバー認証が通っていないことが分からず、デバッグに苦戦することが多いです(実体験)。

また前でも書きましたが、ldap-utilsに入っているクライアントは、TLSを用いたクライアント認証時に、なぜかクライアント認証鍵をサーバーに送ってくれないという現象に困っています。情報を持っている方はコメントをお願いします。

## BIND9 受信IP
受信IPによるアクセス制御は、allow-query-on系とlisten-on系の二つがあります。

|系|使える場所|弾かれた時|
|:-:|:-:|:-:|
|allow-query-on|zone, view, global option|refusedが帰ってくる|
|listen-on|global option|タイムアウトする|

`ss -ul`で見ると分かりますが、listen-onで弾かれたNICはもはやlistenしていません。

気を付けないといけないのは、受信IPによるアクセス制御で127.0.0.1へのリクエストの許可を意識していないと、テストでびっくりすることがあることです(経験済)。`dig @localhost domain`がタイムアウトする!!。

これと似たような関係のものに、deny-answer-addressとblackholeオプションがあります。こちらは、送信元のIPベースでアクセス制御を行います。