Rocket.ChatでのLDAP認証設定

# はじめに

RocketChatをk8sクラスターにhelmで導入したみたので、その作業のメモを残します。

今回のhelm用rocketchatパッケージのバージョンは2.0.10(Rocket.Chat 3.6.0)で、前提となる導入については、次のような作業を行なっています。

```bash:helmでのrocketchatの導入概要
## Helm v3対応版に書き換え
$ helm repo update
$ helm fetch stable/rocketchat
$ tar xvzf rocketchat-2.0.2.tgz
$ vi rocketchat/values.yaml
## mongodb.mongodbRootPassword, mongodb.mongodbPassword, *.storageClass などの設定を行ない、helm --set は利用しない
$ ( cd rocketchat ; sudo helm install --namespace rocketchat myrocketchat . )
```

## helm listの動作について

特定のnamespaceで稼動している場合には、``-namespace``オプションを利用する必要があります。

```bash
$ sudo helm list --namespace rocketchat
```

## 最新版Rocket.Chatへの更新について

Helmで導入後に、values.yamlの利用するDockerコンテナ設定を変更し、現時点で最新版のv3.16.0に変更後、``helm upgrade``しています。

```bash:upgrade手順
$ ( cd rocketchat ; sudo helm upgrade myrocketchat . --namespace rocketchat)
```

# Refernces

* [公式ガイド - Configuring SSL Reverse Proxy](https://rocket.chat/docs/installation/manual-installation/configuring-ssl-reverse-proxy/)
* [GitHub Issues - Mass delete users #8324](https://github.com/RocketChat/Rocket.Chat/issues/8324)
* [公式ガイド - Running in a sub folder with Apache](https://docs.rocket.chat/installing-and-updating/manual-installation/running-in-a-sub-folder)
* [公式APIガイド - developer.rocket.chat/api/rest-api](https://developer.rocket.chat/api/rest-api)
* [公式ガイド - Two Factor Authentication](https://developer.rocket.chat/rocket.chat/two-factor-authentication)

# Nginx ReverseProxyの追加設定

Android Appから接続した際には、Websocketを有効にするようメッセージが表示されました。
通常のTLS設定を有効にしたProxy設定だけでは不十分で次のような設定を追加しています。

```nginx:X-Forwarded_protoからlocationの間に追加した設定
server{
    listen 443 ssl;
    server_name         chat.example.com;
    ssl_certificate     /etc/ssl/certs/chat.example.com.pem;
    ssl_certificate_key /etc/ssl/private/chat.example.com.nopass.key;

    client_max_body_size 0;
    tcp_nodelay on;
    gzip on;
    sendfile on;

    proxy_set_header    Host $host;
    proxy_set_header    X-Real-IP $remote_addr;
    proxy_set_header    X-Forwarded-Host      $host;
    proxy_set_header    X-Forwarded-Server    $host;
    proxy_set_header    X-Forwarded-For       $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto https;

   # rocket.chat websocket related setting
   proxy_http_version  1.1;
   proxy_set_header    Upgrade $http_upgrade;
   proxy_set_header    Connection "upgrade";
   proxy_set_header    X-Nginx-Proxy true;

   location / {
      proxy_pass    http://192.168.100.140/;
   }
}
```

この他にTLS設定を有効にしておきます。

# Rocket.ChatのLDAP設定

設定したReverseProxyサーバーに接続し、初回の登録処理を行ないます。

この時に登録する管理者IDに指定するe-mailアドレスはLDAPに登録されているユーザーとは異なるもの(GmailアドレスやLDAPに登録されているものの利用しないユーザーのアドレス等）にします。

LDAPに登録されている一般ユーザーでログインした際に、mail:フィールドが既に登録済みだとログインすることができません。

管理者IDでログインし、"Administration"→"LDAP"に進み、次のような設定を行なっている。
考慮点は次のとおり。

1. ログから認証に失敗する原因として、LDAP検索でPaginationが有効に機能しない場合があるため、対応のため"Search Page Size"は0に設定していること (Referencesのリンクを参照)
2. AuthenticationはBind ID/Passwordを利用するか、しないか、でOn/Offを切り替えること
3. Sync/Importは(LDAPサーバーに負荷を与えないための)一括読み取りをするかどうかに関わらず、設定を行なう必要があること
4. User Group Filterの設定は、ldapsearchコマンドなどで利用できることを確認した上で設定すること
5. LDAP Group Channel MapはGroupを利用してデフォルトのチャネルを設定できるので便利


## LDAP (General Settings)

利用するTLSの証明書が自己証明書でなければ、CA情報を入力する必要はありません。

```text:設定値
Enable: On
Login Fallback: Off
Find user after login: Off
Host: ldap.example.com
Port: 636
Reconnect: On
Encryption: SSL/LDAPS
CA Cert:
Reject Unauthorized: On
Base DN: ou=proxy,dc=example,dc=com
Internal Log Level: Info
```

## LDAP - Authentication

bindDN,bindPWを利用しない場合は、offに設定しておきます。

```text:設定値
Enable: Off
```

## LDAP - Sync/Import

User Group Filterでは、``#{userdn}``という指定が利用できるので、以前の``uid=#{username},ou=people,...``という指定から置き換えています。

Channel Adminはデフォルトの、rocket.chatボットを指定しています。

```text:設定値
Username Field: uid
Unique Identifier Field: objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber
Default Domain: example.com
Merge Existing Users: Off
Sync User Data: On
User Data Field Map: {"gecos":"name", "mail":"email"}

Sync LDAP Groups: On
User Group Filter: (&(cn=#{groupName})(member=#{userdn}))
LDAP Group BaseDN: ou=MailGroup,ou=Proxy,dc=example,dc=com
User Data Group Map: { }
Auto Sync LDAP Groups to Channels: On
Channel Admin: rocket.chat
LDAP Group Channel Map: { "all-group1": "general", "all-group2": "general", "techsupport": [ "helpdesk", "support" ] }
Auto Remove Users from Channels: Off
Sync User Avatar: On
Background Sync: Off
Background Sync Interval: every 24 hours
Background Sync Import New Users: Off
Background Sync Update Existing Users: On
```

``LDAP Group Channel Map:``を利用する場合には、LDAPグループ(上の例では"rocket-admin","tech-support")に対応するチャネル・プライベートグループ等(上の例では"group1", "group2")が存在している必要があります。

存在していないチャネル等を指定している場合には、ログイン自体に失敗する点に注意してください。

## LDAP - Timeouts

```text:設定値
Timeout (ms): 60000
Connection Timeout (ms): 1000
Idle Timeout (ms): 1000
```

## LDAP - User Search

考慮点にも挙げていますが、環境によっては``Search Page Size: 0``を設定する必要があります。

```text:設定値
Filter: (objectclass=*)
Scope: sub
Search Field: uid
Search Page Size: 0
Search Size Limit: 2000
```

## LDAP - User Search (Group Validation)

Group Nameを指定することで、特定のグループメンバーだけがログインするように変更できます。
ここでは利用していませんが

```text:設定値
Enable: Off
ObjectClass: groupOfNames
Group ID Attribute: cn
Group Member Attribute: member
Group Member Format: uid=#{username},ou=people,ou=proxy,dc=example,dc=com
Group Name: group1
```

# 遭遇した問題

## Background Syncで登録されてしまったユーザーの一括削除

Background Sync: Onにしていると、Background Sync Import New Users: Offでも、LDAPに登録されているユーザー全員をImportしてしまいます。一般的な利用者だけではなく、システムユーザーも登録されてしまったので、これらのユーザーを削除しようと思いました。以下のissuesで方法が説明されています。

* https://github.com/RocketChat/Rocket.Chat/issues/8324 

Helmでインストールしているので、mongodbのPodに入って、mongoコマンドでサーバーに接続します。

```bash
$ kubectl exec -it myrocketchat-mongodb-primary-0 bash
I have no name!@myrocketchat-mongodb-primary-0:/$ mongo
rs0:PRIMARY> use rocketchat
rs@:PRIMARY> db.auth("rocketchat", "secret")
1

rs@:PRIMARY> db.users.remove({"ldap": true});
WriteResult({ "nRemoved" : 501 })

rs0:PRIMARY> db.users.find()
{ "_id" : "rocket.cat", "createdAt" ... }
{ "_id" : "a97Pm5DxbkHsW9MSq", "createdAt" : ....}
```

LDAP経由でログインした経験のあるユーザー情報を含めて全てが削除されますが、関連するデータは残っているため、Merge Existing UsersがOffである場合(当初の設定)、LDAPユーザーでログインした際にエラーが発生する可能性があります。

```text:Sync/Import設定の変更
Merge Existing Users: On
```

きれいにデータベースを操作する方法が分からなかったので、この方法で対応しています。

## '/'以外のsub folderで稼動させるための方法

Helmを利用すると、ROOT_URLを変更する方法が提供されていないため、'/'以外のpathでは稼動させることができません。
Ingressに対応する設定はあるように見えますが、実際には動作しません。

* https://docs.rocket.chat/installing-and-updating/manual-installation/running-in-a-sub-folder

context-rootを変更するためには、ROOT_URL変数に手を加える必要があるので、次の2点の変更が必要です。

* values.yamlファイルの``host:``にサービス用(フロントエンドのReverseProxy)のホスト名を指定する
* templates/deployment.yamlファイルのROOT_URLを修正

```diff:templates/deployment.yamlファイルの変更箇所
diff --git a/rocket.chat/rocketchat/templates/deployment.yaml b/rocket.chat/rocketchat/templates/deployment.yaml
index d1eecb7..56e5000 100644
--- a/rocket.chat/rocketchat/templates/deployment.yaml
+++ b/rocket.chat/rocketchat/templates/deployment.yaml
@@ -63,7 +63,7 @@ spec:
               key: mongo-oplog-uri
         {{- if .Values.host }}
         - name: ROOT_URL
-          value: https://{{ .Values.host }}
+          value: https://{{ .Values.host }}/chat
         {{- end }}
         {{- if .Values.smtp.enabled }}
         - name: MAIL_URL
@@ -81,7 +81,7 @@ spec:
         {{- if .Values.livenessProbe.enabled }}
         livenessProbe:
           httpGet:
-            path: /api/info
+            path: /chat/api/info
             port: http
           initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
           periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
@@ -92,7 +92,7 @@ spec:
         {{- if .Values.readinessProbe.enabled }}
         readinessProbe:
           httpGet:
-            path: /api/info
+            path: /chat/api/info
             port: http
           initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
           periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
```

実際にはvalues.yamlに独自項目を追加し、pathを直接打ち込んでいる状態は避けた方が無難です。

この後、``service/myrocketchat-rocketchat``への接続を行なう必要があります。type:LoadBalancerに変更し、EXTERNAL-IPを割り当てる方法もありますが、ここはkubesprayで導入したingressから接続させていきます。


### namespace: ingress-nginx での作業

Ingressは ingress-nginx というnamespaceで稼動しているので、そのままでは別namespaceのrocketchatには接続できないので、Ingressオブジェクトを作成し、Service側でExternalNameを割り当てていきます。

既にIngressを利用していれば不要な設定もありますが、ここではIngress-ControllerのDaemonSet定義だけが行なわれていて、Podが各Nodeで稼動しているだけの状態から設定していきます。

まず目指すべき最終形態での、``kubectl -n ingress-nginx get all``の出力を載せておきます。

```bash:ingress-nginxでの最終的な設定状況
NAME                                 READY   STATUS    RESTARTS   AGE
pod/ingress-nginx-controller-knrzd   1/1     Running   0          5d4h
pod/ingress-nginx-controller-mvgnp   1/1     Running   0          5d4h
pod/ingress-nginx-controller-vj5s5   1/1     Running   0          5d4h
pod/ingress-nginx-controller-zlgnr   1/1     Running   0          5d4h

NAME                               TYPE           CLUSTER-IP     EXTERNAL-IP                                  
          PORT(S)        AGE
service/ingress-nginx-controller   LoadBalancer   10.233.52.44   192.168.100.168                                 
          80:31345/TCP   7h53m
service/rocketchat-svc             ExternalName   <none>         myrocketchat-rocketchat.rocketchat.svc.cluste
r.local   <none>         7h30m

NAME                                      DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR  
          AGE
daemonset.apps/ingress-nginx-controller   4         4         4       4            4           kubernetes.io/o
s=linux   5d4h
```

get allでは表示されないIngressオブジェクトは次のような設定になっています。

```yaml:ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  labels:
    group: ingress-nginx
  namespace: ingress-nginx
spec:
  rules:
  - http:
      paths:
      - backend:
          service:
            name: rocketchat-svc
            port:
              number: 80
        path: /chat
        pathType: Prefix
```

初期状態では、一切のserviceオブジェクトが定義されていないので、次のような設定を行なっています。

```yaml:既にIngressのPodに接続するための定義があれば、必要ありません。
---
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  type: LoadBalancer
  loadBalancerIP: "10.1.200.168"
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app.kubernetes.io/name: ingress-nginx
```

続いて、rocetchatに接続するためのexternalNameを設定していきます。

```yaml:externalName用の設定(nameはingressに設定しているservice.nameと一致している必要があります)
---
apiVersion: v1
kind: Service
metadata:
  name: rocketchat-svc
  labels:
    group: ingress-nginx
  namespace: ingress-nginx
spec:
  type: ExternalName
  externalName: myrocketchat-rocketchat.rocketchat.svc.cluster.local
```

externalNameは、``<service name>.<namespace>.svc.cluster.local``のように接続先のホスト名を指定します。

## LDAP Groupに対応したチャネルを準備したい

ActiveDirectoryなどと違って、Userのディレクトリには所属グループの情報(memberOf)は存在していないため、グループのディレクトリ(objectclass=groupOfNames)に対して、member=${userDN}なフィルターをかけないと所属グループが分からないようになっています。

いろいろ調べてみましたが、デフォルトでは``LDAP Group Channel Map``に対応するためのグループをあらかじめ準備する良い方法はなさそうです。

REST APIを利用して、必要なチャネルを準備する方法について検討していきます。

* [公式APIガイド - developer.rocket.chat/api/rest-api](https://developer.rocket.chat/api/rest-api)

単純にチャネルを1つ追加するだけであれば、curlを利用して次のようにできるとドキュメントに書かれています。

```bash:curlによるRocket.ChatのAPIの利用
curl -H "X-Auth-Token: 9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq" \
     -H "X-User-Id: aobEdbYhXfu5hkeqG" \
     -H "Content-type: application/json" \
     http://localhost:3000/api/v1/channels.create \
     -d '{ "name": "channelname" }'
```

``X-Auth-Token``と``X-User-Id``に対応する値は、管理者権限でログインしたユーザーの設定画面から、2要素認証を無視するにチェックを入れた状態で取得することができます。

```bash:適宜書き換えたコマンドによる
$ ./add-rocketchat-channel.sh | jq .
{
  "channel": {
    "_id": "....",
    "fname": "channelname",
    "customFields": {},
    "name": "channelname",
    "t": "c",
    "msgs": 0,
    "usersCount": 1,
    "u": {
      "_id": ".....",
      "username": "admin"
    },
    "ts": "2021-06-30T02:02:45.487Z",
    "ro": false,
    "_updatedAt": "2021-06-30T02:02:45.494Z"
  },
  "success": true
}
```

あとはスクリプトを工夫すれば複数のチャネルを作成することはできそうです。

もう一つ必要な作業があって、LDAPグループChannelマップにLDAPグループと対応するチャネル等を指定する必要があります。今回はLDAPグループ名とチャネル名を同じにするので、次のような設定をAPIを通じて行なう必要があります。

```json:追加したいLDAPグループChannelマップ設定
{
    "channelname": "channelname",
    ....
}
```

あまり変更頻度が低いのであれば手動でコンソールから追加するでも良さそうなのですが、3桁台のグループが存在しているので、間違いを防ぐためにも自動化したいところです。

問題は先ほどのリンク先をみても、LDAPに対応するREST APIのガイドは存在しません。

githubでcloneしたコードをみると、``LDAP_Sync_User_Data_Groups_AutoChannelsMap``というIDが割り当てられて、内容はJSONフォーマットであることが分かるので、これを流し込めば良いという事は分かります。

そう思ってAPIを良くみていくと、``/api/v1/settings/:_id``からPOSTメソッドによる設定内容の更新ができることが分かります。ガイドの中で、コードの``this.add()``メソッドの引数からIDを確認するという記述があるので、なんとかなりそうです。

* https://developer.rocket.chat/api/rest-api/endpoints/settings/update

これをもとに、テスト用のcurlのコマンドラインを作っていきます。

まず、どんな変数をPOSTすれば良いのか分からないので、GETメソッドによる値の確認を行ないます。

```bash:認証情報は適宜変更してください
curl -H "X-Auth-Token: 9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq" \
     -H "X-User-Id: aobEdbYhXfu5hkeqG" \
     http://localhost:3000/api/v1/settings/LDAP_Sync_User_Data_Groups_AutoChannelsMap
```

これを実行すると次のような結果が返ってきました。

```json:jqコマンドによる整形
{
  "_id": "LDAP_Sync_User_Data_Groups_AutoChannelsMap",
  "value": "{\n    \"employee\": \"general\" \n}",
  "success": true
}
```

基本的には、{ "value": "...." } の形式で内容を指定して、POSTするようなので、これに合わせてコマンドラインを準備します。

```bash:curlコマンドによるLDAP_GROUP_MAPのPOST
$ curl -H "X-Auth-Token: 9HqLlyZOugoStsXCUfD_0YdwnNnunAJF8V47U3QHXSq" \
     -H "X-User-Id: aobEdbYhXfu5hkeqG" \
     -H "Content-type:application/json" \
     http://localhost:3000/api/v1/settings/LDAP_Sync_User_Data_Groups_AutoChannelsMap \
     -d '{ "value": "{ \"channelname\": \"channelname\" }" }'
```

これを実行した時の戻り値は次のようになりました。

```json:curlコマンド実行の結果
{"success":true}
```

この状態でコンソールでは設定の反映（保存）が完了しています。自分のLDAPユーザーで改めてログアウト・ログインすることで、自動的に作成したグループに含まれました。

これで無事に自動化する目処が立ちました。

### POST時に発生するTOTPエラーについて

v3.16.1を使用したところ、同一のToken/IDを利用していて、LDAP_Sync_User_Data_Groups_AutoChannelsMap へのPOSTを実行した時だけ、TOTPが必要だというエラーメッセージ(リターンコード:400)になる現象に遭遇しました。

管理者権限を持つユーザーのPersonal Access Tokenを利用していますが、これを生成する際に、"Ignore Two Factor Authentication"チェックボックスにチェックを入れる必要がありました。

ドキュメントをみると、この部分の機能はまだこれから実装が進みそうなので、エラーメッセージが変化するなど、バージョンによって挙動が異なる可能性があります。

以上