dex(OpenID Connect Provider)をLDAP認証で動かしてみる

# はじめに

IDをLDAPで統合管理しているため、ユーザー認証はLDAPを利用して組織内のパスワードを統合したいと考えています。
とはいえ生のLDAPでBASIC認証をかける時代でもないので、OpenID Connectのサーバーを構築することにしました。

とりあえず、kubernetesを使っていることもあるので、余っているAPU2を使って https://github.com/dexidp/dex を動かしてみることにしました。

> 【2021/04/13 追記】dexの最新コードでは、go getではなく、git clone & make を利用する方法が公式ガイドに記述されていたので、その記法に合わせました。そのためDockerfileの内容は大幅に変更されています。

# 環境

## テスト環境

* Ubuntu 20.04.2 (LTS) 64bit
* IP: 192.168.100.10
* go 1.17.5
* GOPATH="${HOME}/go"

## Dockerコンテナ

* Alpineイメージをベースに利用 (3.13系列)
* Multistage Buildによってイメージサイズを圧縮する

# 公式ガイドと違うところ

公式ガイドは同一マシンで、サーバーとWebブラウザの両方を動作させることを前提としていますが、今回はDexサーバーは別マシンで稼動させます。

このため127.0.0.1で動く事を前提にしているサンプルではうまく動かないので、オプションの設定など違いをメモしていきます。

# 動作の概要

サンプルのクライアントプログラム(bin/example-app)には、次のようなURLを伝える必要があります。

* example-appが待ち受けるネットワークアドレス・ポート
* 認証を依頼するdexサーバーのURL(アドレス・ポート・パス)
* dexサーバーから認証後にRedirectしてもらう待ち受けURL(同上)

## Building the dex binary

手順は[公式サイトのGetting started](https://github.com/dexidp/dex/blob/master/Documentation/getting-started.md)と同じです。

```bash
$ git clone https://github.com/dexidp/dex.git
$ cd dex
$ make build
$ make examples
$ ls bin/
```
無事に終ると、bin/dex や bin/example-app バイナリが配置されているはずです。

ここから先のConfiguration & Running Clientの方法は、[公式ガイドのLDAPの項目](https://dexidp.io/docs/connectors/ldap/)に従っていきます。

## Configuration

ドキュメントでは、examplesディレクトリの設定ファイルを指定しています。```./bin/dex serve examples/config-ldap.yaml``` ここでは元のファイルをコピーして編集することにします。

```bash
$ cp examples/ldap/config-ldap.yaml config-ldap.yaml
$ vi config-ldap.yaml
```

## Running dex server

サーバー側を動かす前に、先ほどコピーして編集したconfig-ldap.yamlについて説明します。
変更部分のdiffは次のようになっています。

```diff
--- examples/config-ldap.yaml   2019-03-04 09:45:20.967690885 +0900
+++ config-ldap.yaml    2019-03-04 09:58:16.352010857 +0900
@@ -1,4 +1,4 @@
-issuer: http://127.0.0.1:5556/dex
+issuer: http://192.168.100.10:5556/dex
 storage:
   type: sqlite3
   config:
@@ -11,36 +11,36 @@
   name: OpenLDAP
   id: ldap
   config:
-    host: localhost:10389
+    host: ldap.example.com:389

     # No TLS for this setup.
     insecureNoSSL: true

     # This would normally be a read-only user.
-    bindDN: cn=admin,dc=example,dc=org
-    bindPW: admin
+    #bindDN: cn=admin,dc=example,dc=org
+    #bindPW: admin

-    usernamePrompt: Email Address
+    usernamePrompt: LDAP-ID

     userSearch:
@@ -48,6 +48,6 @@
 staticClients:
 - id: example-app
   redirectURIs:
-  - 'http://127.0.0.1:5555/callback'
+  - 'http://192.168.100.10:5555/callback'
   name: 'Example App'
   secret: ZXhhbXBsZS1hcHAtc2VjcmV0
```

### 変更箇所の概要

```issuer``` と```redirectURIs``` については、IPアドレスを変更しています。

```host``` の項目は別に準備しているLDAPサーバーについての情報に書き換えています。
これはTLS(ldaps,636)を使わず、接続にbindDNも必要としないので、```insecureNoSSL``` はtrueのまま、bindDNとbindPWはコメントアウトしています。

TLSを利用する場合には、host:行で636ポートを指定するなどし、insecureNoSSLはnoに設定します。

userSearchやgroupSearchのセクションは適宜修正しています。
この後、編集したファイルを指定してサービスを開始しています。

```bash
$ bin/dex serve config-ldap.yaml
```

## Running a client

ドキュメントでは```./bin/example-app```を実行しているだけ、最初に説明した3つの動きをさせるためにオプションを指定していきます。

```bash
$ ./bin/example-app --listen http://192.168.100.10:5555 \
     --issuer http://192.168.100.10:5556/dex \
     --redirect-uri http://192.168.100.10:5555/callback
```

* example-appが利用するアドレス・ポート番号 ```--listen http://192.168.100.10:5555```
* 認証を依頼するdexサーバーのURL(アドレス・ポート・パス) ```--issuer http://192.168.100.10:5556/dex```
* 認証後にRedirectしてもらうため、dexサーバーに伝えるclient側の待ち受けURL(同上) ```--redirect-uri http://192.168.100.10:5555/callback```

同一ホストでdexサーバーとクライアントを利用している場合、issuerにlocalhostを指定することはできますが、Dockerコンテナの場合には、localhostと指定してもホストには接続しないため動かない点に注意してください。

# 動作について

Getting startedにあるように、bin/example-appコマンドに必要な引数を指定して起動します。

Webブラウザからhttp://192.168.100.10:5555/ にアクセスして表示された画面の入力欄にはまず何も入れずに"Login"ボタンを押します。

画面がhttp://192.168.100.10:5556/dex にリダイレクトされ、LDAPのID, Passwordを入れて認証されると、bin/example-appコマンドが準備している http://192.168.100.10:5555/callback に戻されます。

最初の画面にIDとか入れるのかなと思いましたが、何も入れなくて問題なく動きました。
これがそのまま他のアプリにも適用できるのかは、これから確認していく予定です。

## example-appに表示される結果

認証が無事に終わり、http://192.168.100.10:5555/callback など指定したURLに戻されると次のような結果が画面に出力されます。

```text:bin/example-appがWebブラウザに表示する出力例
ID Token:

eyJhbGciOiJSUzI1NiIsImtpZCI6IjkxOWUzNTFmMGZhMTZhYjA4ZTEzNWQ3ZjYxNGE3ZTM4MTI1NDg3OWIifQ.eyJpc3MiOiJodHRwczovL29wbTAwaC51LWFpenUuYWMuanAvZGV4Iiwic3ViIjoiQ2pWMWFXUTllV0Z6ZFMxaFltVXNiM1U5VUdWdmNHeGxMRzkxUFZCeWIzaDVMR1JqUFhVdFlXbDZkU3hrWXoxaFl5eGtZejFxY0JJRWJHUmhjQSIsImF1ZCI6ImV4YW1wbGUtYXBwIiwiZXhwIjoxNTU4NjczNTgzLCJpYXQiOjE1NTg1ODcxODMsImF0X2hhc2giOiI1dllGMkxZOUZGMkdZckVpTnBOeVFnIiwiZW1haWwiOiJ5YXN1LWFiZUB1LWFpjnUuYWMuanAiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6Illhc3VoaXJvIEFiZSJ9.pRPgtewj7y42EWlVcv9OhyJmmO5969x2lb6IHRJTDFHvgVhCDFNO9_jZC2hLcHeLPId5BXeZtcNY-GCDxS89IxsjSKYG8cRdYOIHvBN-jB8DAtJYQxr225pU1SUybOdh9-0u3DeC0iPnUEwoLvyMStl-D3TuwMgsjgTWUWrfUthxnoricltixaITHjl0EpQmzxdZHmNuTRlTwQFh_zgw1NO4u6E-s7IAHvTnNeeDsx2yTR4RSwabc-GJ4HZ5OL_s6kAlloKTKAL6szTt-aMfo8B4hkVX8VjTCtyPClbdxOnO7KCAsfHqEJb691LBUQ8BBmHZjTJFKmOZ8cp7B1lyeQ

Access Token:

k7w3vkjm34e4rqdwhhspgzb5g

Claims:

{
  "iss": "http://192.168.100.10:5556/dex",
  "sub": "CjV1aWQ9eWFzdS1hYmUsb3U9UGVvcGtlLG91PVByb3h5LGRjPXUtYWl6dSxkYz1hYyxkYz1qcBIEbGRhcA",
  "aud": "example-app",
  "exp": 1558673583,
  "iat": 1558587183,
  "at_hash": "5vYF2LY9FF2GYrEiNpNyQg",
  "email": "yasu@yasundial.org",
  "email_verified": true,
  "name": "Yasuhiro ABE"
}

Refresh Token:

Chlsb3RydnNmaTJneWk3eDJ0Z2tpbDc0cDJuDhlsdmZqemZjbnF1Y3lycTIzeXhyamZ4Nmh4
```

# Kubernetesへのデプロイメント

自前のk8sの環境で動かしたので簡単なメモを残します。
ただし、ドメイン依存な箇所は適宜修正しているので、不足分は補う必要があります。

## Docker(Dockerhub)関連の設定ファイル

現在は次のようなAlpine用のDockerfileを利用していますが、コンテナのサイズは約90MBと少し大きいです。
自前のアイコンを準備しているので、あらかじめ images/logo.png にファイルを配置していますが、以下のDockerfileではコメントアウトしています。

```dockerfile:alipne用のDockerfile
FROM golang:alpine3.13 as dex

RUN apk --no-cache add git make gcc libc-dev

RUN mkdir /work
WORKDIR /work
RUN git clone https://github.com/dexidp/dex.git

WORKDIR /work/dex
# COPY image/logo.png web/themes/dark/logo.png
# COPY image/logo.png web/themes/light/logo.png
RUN make build
RUN make examples

FROM alpine:3.13

RUN apk update && apk add --no-cache bash ca-certificates

RUN mkdir -p /dex/bin
COPY --from=dex /work/dex/bin/. /dex/bin/.
WORKDIR /dex

COPY run.sh /run.sh
RUN chmod +x /run.sh

RUN mkdir /config

EXPOSE 5556

ENV DEX_CONFIG_TEMPLPATH="/config/config-ldap.yaml.templ"
ENV DEX_ISSUER="http://127.0.0.1:5556/dex "
ENV DEX_APP01_REDIRECTURI="http://example.app:5555/callback"

RUN mkdir /data

VOLUME ["/config", "/data"]

ENTRYPOINT ["/run.sh"]
```

### run.shファイル

実際に利用しているファイルとは少し違いますが、次のような構成のスクリプトからサーバープロセスを起動しています。

```bash:run.sh
#!/bin/bash -x
CONFIG_FILEPATH="${CONFIG_FILEPATH:-/config/config-ldap.yaml}"
cat "${CONFIG_FILEPATH}"
bin/dex serve "${CONFIG_FILEPATH}"
```

## Dockerコンテナのビルド

Dockerfileとrun.shファイルを配置したら、次のような要領でコンテナを作成します。

```bash:dockerコンテナのビルド
$ sudo docker build . --tag dex-server
```

## Kubectl用の設定ファイル

01-configmap.yamlには実際に利用する/config/config-ldap.yamlファイルの内容を記載してください。

```yaml:01-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: dex-conf
  namespace: dex
data:
  config-ldap.yaml: |
   issuer: ...
   ...
```

```yaml:02-service.yaml
---
apiVersion: v1
kind: Service
metadata:
  namespace: dex
  name: issuer
spec:
  type: LoadBalancer
  loadBalancerIP: "192.168.100.35"
  ports:
    - port: 5556
      protocol: TCP
  selector:
    app: issuer
```

ここではPVCを確保するために、Rook/Cephを利用しています。

```yaml:03-deploy-dex.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: dex
  name: issuer
  labels:
    app: issuer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: issuer
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: issuer
    spec:
      imagePullSecrets:
      - name: dockerhubreg
      containers:
      - name: issuer
        image: .../dexidp-dex:0.1
        ports:
        - containerPort: 5556
          name: issuer
        volumeMounts:
        - name: config
          mountPath: /config
        - name: dex-data
          mountPath: /data
      volumes:
      - name: config
        configMap:
          name: dex-conf
          items:
            - key: config-ldap.yaml
              path: config-ldap.yaml
      - name: dex-data
        persistentVolumeClaim:
          claimName: dex-data-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dex-data-pvc
  namespace: dex
spec:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: rook-ceph-block
  resources:
    requests:
      storage: 2Gi
```

image:行や、secret/dockerhubreg はあらかじめ編集・作成しておくようにしてください。

以上