ICP上でOpenLDAPをPodとして動かして連携させる

IBM Cloud Privateでビルトイン管理者ユーザー以外を使用するためには外部にLDAPが必要だが、外部にLDAPサーバーを立てるのではなく、ICPのKubernetesクラスタ内でOpenLDAPをPodとして稼働させてICPのユーザー認証に使用する方法をICP v3.1.2で確認。

Podではなく普通に外部にOpenLDAPを立てる場合は以下が参考になりそう。

- [IBM Cloud Private (ICP) をOpenLDAPで認証する](https://qiita.com/teruq/items/9ccdbdf168c24cb1a858)

## 使用するHelmチャートについて

この目的で使えるチャートと手順が以下のリポジトリで提供されている。今回はこのチャートを利用する。

- [Helm chart for installing openldap on ICP ](https://github.com/ibm-cloud-architecture/icp-openldap)

このチャートは[ibm-charts](https://github.com/IBM/charts)ではなく、あまりメンテナンスされていないようにみえる。phpLDAPadminのPodを一緒にデプロイしてくれたり、初期データをロードする機能もあり、テスト用途としては便利。PersistentVolumeを使っておらず、データは永続化されない。リポジトリのREADMEにもデモ用と記載がある。

ICP用ではなく一般的なOpenLDAPのHelmチャートもあるので、本格的に利用する場合はこちらを利用するべきと思われる。こちらはPersistentVolumeを利用してデータの永続化が可能。どちらも[osixia/openldap](https://hub.docker.com/r/osixia/openldap/)のDockerイメージを利用している。

- [OpenLDAP Helm Chart](https://github.com/helm/charts/tree/master/stable/openldap)

## OpenLDAPのデプロイ

### リポジトリのクローン

リポジトリをクローンする。

```shell:コマンド
git clone https://github.com/ibm-cloud-architecture/icp-openldap.git
```

```shell-session:実行例
$ git clone https://github.com/ibm-cloud-architecture/icp-openldap.git
Cloning into 'icp-openldap'...
remote: Enumerating objects: 2, done.
remote: Counting objects: 100% (2/2), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 22 (delta 0), reused 1 (delta 0), pack-reused 20
Unpacking objects: 100% (22/22), done.
$
```

### チャートのアップロード（参考）

チャートをICPに登録する場合は以下の手順で行うが、今回はローカルでチャートを編集する必要があるのでやらない。

チャートをパッケージする。

```shell:コマンド
helm package icp-openldap
```

```shell-session:実行例
$ helm package icp-openldap
Successfully packaged chart and saved it to: /home/sotoiwa/workspace/icp-openldap-0.1.5.tgz
$
```

ICPにログインする。

```shell
cloudctl login \
  -a https://mycluster.icp:8443 \
  --skip-ssl-validation \
```

Helmチャートをロードする。

```shell:コマンド
cloudctl catalog load-helm-chart --archive icp-openldap-0.1.5.tgz
```

```shell-session:実行例
$ cloudctl catalog load-helm-chart --archive icp-openldap-0.1.5.tgz
Loading helm chart
Loaded helm chart

Synch charts
Synch started
OK
$
```

ICPコンソールでカタログに追加されたことを確認する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/2932d10f-5346-f9bf-28fd-78fb918b93b5.png)

### イメージのプライベートレジストリへの登録

今回、ICPがオフライン環境にあるため、使用するイメージをICPのプライベートレジストリに登録する。インターネットにつながる環境の場合はこの手順は不要。

ICPはイメージはNamespaceに紐付ける必要があるため、イメージをpushする先のNamespaceを作成する。
GUIでNamespaceを作成する場合は`ibm-anyuid-hostpath-psp`を割り当てること。NamespaceをCLIで作成した場合は、あとでPodSecurityPolicyをバインドするClusterRoleBindingの作成が必要。

```shell
kubectl create ns openldap
```

チャート内のマニフェストを確認し、使用しているイメージをpullしてICPにpushする。busyboxはタグが指定されていなかったが指定するようにした。

```shell
docker pull osixia/openldap:1.1.10
docker pull osixia/phpldapadmin:0.7.0
docker pull busybox:1.30.0
docker tag osixia/openldap:1.1.10 mycluster.icp:8500/openldap/openldap:1.1.10
docker tag osixia/phpldapadmin:0.7.0 mycluster.icp:8500/openldap/phpldapadmin:0.7.0
docker tag busybox:1.30.0 mycluster.icp:8500/openldap/busybox:1.30.0
docker push mycluster.icp:8500/openldap/openldap:1.1.10
docker push mycluster.icp:8500/openldap/phpldapadmin:0.7.0
docker push mycluster.icp:8500/openldap/busybox:1.30.0
```

### valuesファイルの作成とチャートの修正

valuesファイルをコピーする。

```shell-session
cp icp-openldap/values.yaml icp-openldap-values.yaml
```

valuesファイルでイメージのリポジトリとタグをを書き換えるが、マニフェスト・テンプレートにイメージのリポジトリとタグがハードコードされてしまっているので、テンプレート自体も修正が必要。OpenLDAPのイメージ、初期化コンテナとして利用しているbusyboxのイメージ、phpLDAPadminのイメージについて、プライベートレジストリにpushしたイメージと、valuesとテンプレートで指定されているイメージの整合性がとれるようにする。

（追記）プルリクしてマージしてもらったので、マニフェスト・テンプレートの修正は不要でvaluesファイルの修正だけでよいはず。

なお、デフォルトで`user1`〜`user4`を作るようになっているが必要に応じてユーザーをカスタマイズ可能。管理者のパスワード、ユーザーのパスワードも必要に応じてカスタマイズする。

```yaml:icp-openldap-values.yaml
OpenLdap:
  Image: "mycluster.icp:8500/openldap/openldap"
  ImageTag: "1.1.10"
  ImagePullPolicy: "Always"
  Component: "openldap"

  Replicas: 1

  Cpu: "512m"
  Memory: "200Mi"

  Domain: "local.io"
  AdminPassword: "admin"
  Https: "false"
  SeedUsers:
    usergroup: "icpusers"
    userlist: "user1,user2,user3,user4"
    initialPassword: "ChangeMe"

PhpLdapAdmin:
  Image: "mycluster.icp:8500/openldap/phpldapadmin"
  ImageTag: "0.7.0"
  ImagePullPolicy: "Always"
  Component: "phpadmin"

  Replicas: 1

  NodePort: 31080

  Cpu: "512m"
  Memory: "200Mi"
```

```yaml:templates/openldap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{.Release.Name}}-seedusers
data:
  seedusers.ldif: |
{{- include (print .Template.BasePath "/_seedusers2.ldif.tpl") . | indent 4 }}
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: {{.Release.Name}}
  name: {{.Release.Name}}
spec:
  ports:
    - port: 389
  selector:
    app: {{.Release.Name}}
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{.Release.Name}}
  labels:
    app: {{.Release.Name}}
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: {{.Release.Name}}
    spec:
      initContainers:
        - name: init-copy
          image: mycluster.icp:8500/openldap/busybox:1.30.0
          command: ['sh','-c','cp /config-map/* /config-storage; ls -lrt /config-storage']
          volumeMounts:
          - name: {{.Release.Name}}-seedusers
            mountPath: /config-map
          - name: config-storage
            mountPath: /config-storage
      containers:
        - name: {{.Release.Name}}
          image: {{.Values.OpenLdap.Image}}:{{.Values.OpenLdap.ImageTag}}
          args: ["--loglevel", "debug"]
          volumeMounts:
            - name: config-storage
              mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom
            - name: ldap-certs
              mountPath: /container/service/slapd/assets/certs
          ports:
            - containerPort: 389
            - containerPort: 636
          env:
            - name: LDAP_LOG_LEVEL
              value: "256"
            - name: LDAP_ORGANISATION
              value: "Example Inc."
            - name: LDAP_DOMAIN
              value: "{{.Values.OpenLdap.Domain}}"
            - name: LDAP_ADMIN_PASSWORD
              value: "{{.Values.OpenLdap.AdminPassword}}"
            - name: LDAP_REMOVE_CONFIG_AFTER_SETUP
              value: "false"
      volumes:
        - name: config-storage
          emptyDir: {}
        - name: ldap-certs
          hostPath:
            path: "/data/ldap/certs"
        - name: {{.Release.Name}}-seedusers
          configMap:
            name: {{.Release.Name}}-seedusers
```

### RoleBindingの作成

ICP v3.1.1以降ではデフォルトではコンテナは`root`で起動できないが、OpenLDAPのコンテナは`root`で起動する。また、hostPathをマウントする。そのため、`root`での起動が可能でhostPathをマウントできるPodSecurityPolicyを利用可能なClusterRoleを、Podを起動するServiceAccoutにバインドする必要がある
（GUIでNamespaceを作成する場合は、作成時にPodSecurityPolicyを選択すると、自動的にRoleBindingが作成される）。

ICPでは以下の事前定義されたPodSecurityPolicyが存在し、特に指定しなければ`ibm-restricted-psp`が利用される。`root`で起動し、hostPathをマウントするため`ibm-anyuid-hostpath-psp`を割り当てる。

それぞれのPodSecurityPolicyについては以下のリンク先に説明がある。

- [IBM Cloud Pak Pod Security Policy Definitions](https://github.com/IBM/cloud-pak/tree/master/spec/security/psp)

```shell-session
$ kubectl get psp
NAME                        PRIV    CAPS                                                                                                                  SELINUX    RUNASUSER          FSGROUP     SUPGROUP    READONLYROOTFS   VOLUMES
ibm-anyuid-hostaccess-psp   false   SETPCAP,AUDIT_WRITE,CHOWN,NET_RAW,DAC_OVERRIDE,FOWNER,FSETID,KILL,SETUID,SETGID,NET_BIND_SERVICE,SYS_CHROOT,SETFCAP   RunAsAny   RunAsAny           RunAsAny    RunAsAny    false            *
ibm-anyuid-hostpath-psp     false   SETPCAP,AUDIT_WRITE,CHOWN,NET_RAW,DAC_OVERRIDE,FOWNER,FSETID,KILL,SETUID,SETGID,NET_BIND_SERVICE,SYS_CHROOT,SETFCAP   RunAsAny   RunAsAny           RunAsAny    RunAsAny    false            *
ibm-anyuid-psp              false   SETPCAP,AUDIT_WRITE,CHOWN,NET_RAW,DAC_OVERRIDE,FOWNER,FSETID,KILL,SETUID,SETGID,NET_BIND_SERVICE,SYS_CHROOT,SETFCAP   RunAsAny   RunAsAny           RunAsAny    RunAsAny    false            configMap,emptyDir,projected,secret,downwardAPI,persistentVolumeClaim
ibm-privileged-psp          true    *                                                                                                                     RunAsAny   RunAsAny           RunAsAny    RunAsAny    false            *
ibm-restricted-psp          false                                                                                                                         RunAsAny   MustRunAsNonRoot   MustRunAs   MustRunAs   false            configMap,emptyDir,projected,secret,downwardAPI,persistentVolumeClaim
$
```

`ibm-anyuid-psp`のPodSecurityPolicyが利用可能な`ibm-anyuid-clusterrole`というClusterRoleが定義されているのでこのClusterRoleを利用する。ちなみに他の`ibm-privileged-psp`などのPodSecurityPolicyもそれを利用可能なClusterRoleが定義されている。

```shell-session
$ kubectl get clusterrole | grep ibm
ibm-anyuid-clusterrole                                                 5d
ibm-anyuid-hostaccess-clusterrole                                      5d
ibm-anyuid-hostpath-clusterrole                                        5d
ibm-cert-manager-cert-manager                                          5d
ibm-privileged-clusterrole                                             5d
ibm-restricted-clusterrole                                             5d
$
```

今回`openldap`というNamespaceにデプロイするので、このNamespaceの全てのServiceAccoutに`ibm-anyuid-hostpath-clusterrole`のClusterRoleをバインドするためのRoleBindingを作成する。

```shell-session
kubectl create rolebinding ibm-anyuid-hostpath-clusterrole-rolebinding \
  --clusterrole=ibm-anyuid-hostpath-clusterrole \
  --group=system:serviceaccounts:openldap \
  -n openldap
```

### チャートのインストール

準備ができたので、チャートをインストールする。

```shell:コマンド
helm install --tls --name icp-openldap \
  --namespace openldap \
  -f icp-openldap-values.yaml \
  icp-openldap
```

```shell-session:実行例
$ helm install --tls --name icp-openldap \
>   --namespace openldap \
>   -f icp-openldap-values.yaml \
>   icp-openldap
NAME:   icp-openldap
LAST DEPLOYED: Sun Feb 17 15:12:54 2019
NAMESPACE: openldap
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME                TYPE       CLUSTER-IP    EXTERNAL-IP  PORT(S)       AGE
icp-openldap        ClusterIP  10.0.139.191  <none>       389/TCP       0s
icp-openldap-admin  NodePort   10.0.253.179  <none>       80:31080/TCP  0s

==> v1beta1/Deployment
NAME                DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
icp-openldap        1        1        1           0          0s
icp-openldap-admin  1        1        1           0          0s

==> v1/Pod(related)
NAME                                 READY  STATUS             RESTARTS  AGE
icp-openldap-69dcc4d564-zxfcv        0/1    Init:0/1           0         0s
icp-openldap-admin-59547dd5f8-f74d9  0/1    ContainerCreating  0         0s

==> v1/ConfigMap
NAME                    DATA  AGE
icp-openldap-seedusers  1     0s


$
```

Podが稼働していることを確認する。

```shell-session
$ kubectl get po -n openldap
NAME                                  READY   STATUS    RESTARTS   AGE
icp-openldap-69dcc4d564-zxfcv         1/1     Running   0          51s
icp-openldap-admin-59547dd5f8-f74d9   1/1     Running   0          51s
$
```

後で使用するのでServiceのIPアドレスを確認する。

```
$ kubectl get svc -n openldap
NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
icp-openldap         ClusterIP   10.0.139.191   <none>        389/TCP        114s
icp-openldap-admin   NodePort    10.0.253.179   <none>        80:31080/TCP   114s
$
```

ICPコンソールで「管理 > IDおよびアクセス > 認証」からLDAP接続を作成する。

|項目|値|備考|
|---|---|---|
|接続名|openldap||
|サーバー・タイプ|カスタム||
|基本DN|`dc=local,dc=io`||
|バインドDN|`cn=admin,dc=local,dc=io`||
|バインドDNパスワード|`admin`||
|LDAPサーバー|`ldap://icp-openldap.openldap:389`||
|グループ・フィルター|`(&(cn=%v)(objectclass=groupOfUniqueNames))`||
|ユーザー・フィルター|`(&(uid=%v)(objectclass=person))`|事前入力値から変更が必要|
|グループIDマップ|`*:cn`||
|ユーザーIDマップ|`*:uid`||
|グループ・メンバーIDマップ|`groupOfUniqueNames:uniquemember`||

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/fb3b8c63-e4a1-8840-5741-78f4bfb115f9.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/440d8470-dea7-a9bc-8961-a2435a4084d1.png)

以上でLDAPの接続は完了。

「管理 > IDおよびアクセス > チーム」からチームを作成し、チームにユーザーまたはグループを追加することでLDAP内のユーザーでもログインできるようなる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/7f76fbd2-4a91-bfcb-1e8d-f42b52d04e8c.png)


## LDAP側でのユーザーの管理

LDAP側でユーザーを管理するためには、phpLDAPadminが利用できる。NodePort Serviceとして公開されているので、Serviceのポートを確認してアクセスする。

```shell-session
$ kubectl get svc -n openldap
NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
icp-openldap         ClusterIP   10.0.139.191   <none>        389/TCP        27m
icp-openldap-admin   NodePort    10.0.253.179   <none>        80:31080/TCP   27m
$
```

ログインDNは`cn=admin,dc=local,dc=io`、パスワードは`admin`でログインする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/f2fe7668-c089-2b6c-b779-7e35a20c9dff.png)

## 補足

LDAPに接続できなくなるとデフォルトの管理者ユーザーでもログインできなくなる。そのような場合の復旧は以下を参照。

- [Cluster administrator cannot log in to the management console](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_3.1.2/troubleshoot/ldap_superadmin.html)