PostgreSQLをバックエンドとするdexidp/dexのデプロイ

# はじめに

自前のKubernetes(以下、K8s)クラスターではDex([github.com/dexidp/dex](https://github.com/dexidp/dex))を、OIDCのIdPとして利用しています。

https://github.com/dexidp/dex

現状はLDAPをバックエンドとして、ユーザー本人と所属グループを識別することで細かな権限管理を実現しています。

これまで設定は静的に設定ファイル中で実施してきましたが、gRPCを利用してAPIによる動的な管理を行うことにしました。

gRPCを本格的に使うのは始めてなので、少し不安はありますが、とりあえずPostgreSQLをバックエンドとしてDexを構成できるように準備を進めていきます。

# 参考資料

資料はそれほど多くはりません。特にgRPCについてはapi.protoファイルを直接確認する必要がありました。

https://dexidp.io/docs/configuration/api/

https://github.com/dexidp/dex/blob/master/api/v2/api.proto

gRPCを利用してAPIにアクセスするサンプルコードは"The Dex API"のドキュメント以外にも、githubのリポジトリにも含まれています。

https://github.com/dexidp/dex/tree/master/examples/grpc-client

# テスト環境の構築

まず空いているk8sクラスターを利用して、PostgreSQLとdexを稼動させていきます。

```plantuml:
actor User
agent Nginx

[User] -> [Nginx] : https://example.com/dex/.well-known/openid-configuration

node "Kubernetes" {
  agent Ingress
  agent PostgreSQL
  agent Dex
  note left: Multiple instances
  agent "PostgreSQL Operator"
  storage "Rook/Ceph"
  note left: Storage service
}

[Nginx] .. [Ingress] : <<TLS>>
[Ingress] .. [Dex]
[Dex] .. [PostgreSQL]
[PostgreSQL Operator] -> [PostgreSQL]
[PostgreSQL] .. [Rook/Ceph]

[User] -> [Dex] : <<gRPC>> configure
```

K8sクラスターはあるものとして、PostgreSQL-Operatorはzalandoのoperatorを利用しています。

https://github.com/zalando/postgres-operator

PostgreSQLは次のような設定で構成しています。

```yaml:01.pgcluster.yaml
---
apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
  labels:
    team: dex-team
  name: pgcluster
  namespace: dex
spec:
  allowedSourceRanges: []
  databases:
    dexdb: pguser
  numberOfInstances: 2
  postgresql:
    version: '16'
  resources:
    limits:
      cpu: 500m
      memory: 1500Mi
    requests:
      cpu: 100m
      memory: 100Mi
  teamId: dex-team
  users:
    pguser:
      - superuser
      - createdb
  volume:
    size: 50Gi
    storageClass: rook-ceph-block
```

パスワードは動的に構成されるのでSecretオブジェクトから自動的に表示するようshell関数を定義しています。

```bash:pguserのパスワードを表示するbashのshell関数
$ type passwd-pguser
passwd-pguser is a function
passwd-pguser () 
{ 
    sudo kubectl -n dex get secret pguser.pgcluster.credentials.postgresql.acid.zalan.do -o jsonpath='{.data.password}' | base64 -d;
    echo ""
}
```

次にDexの設定をConfigMapに準備します。

これまではClientIDなどを登録するのにも利用していましたが、今回はSQLite3に代えてPostgreSQLへの変更とgRPC関連の設定を新たに追加しています。

なおPostgreSQLへの接続はOperatorが自動的にTLSを有効化しているので、``mode: require``である必要があります。

LDAPはPosixAccountを利用しているオーソドックスな構成ですが、ADなどとは違うので環境によって変更してください。

```yaml:02.cm-dexconf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dex-conf
  namespace: dex
data:
  config-ldap.yaml: |
   issuer: "https://example.com/dex"
   storage:
     type: postgres
     config:
       host: pgcluster
       port: 5432
       database: dexdb
       user: pguser
       password: BKL9RfMpCZFDC27GX63PnQ34ZL0gctBWUj3q5CpgYfvX6NHsecqx5FRFO7uDTuvD
       ssl:
         mode: require
   web:
     http: 0.0.0.0:8080
   grpc:
     addr: 0.0.0.0:8088
     tlsCert: /tls/tls.crt
     tlsKey: /tls/tls.key
     # tlsClientCA: /tls-ca/ca.crt
     reflection: true
   logger:
     level: "debug"
     format: "text"
   connectors:
   - type: ldap
     name: OpenLDAP
     id: ldap
     config:
       host: ldap.example.com:636

       # No TLS for this setup.
       insecureNoSSL: false

       usernamePrompt: LDAP-ID

       userSearch:
         baseDN: ou=People,ou=Proxy,dc=example,dc=com
         filter: "(objectClass=posixAccount)"
         username: uid
         # "DN" (case sensitive) is a special attribute name. It indicates that
         # this value should be taken from the entity's DN not an attribute on
         # the entity.
         idAttr: DN
         emailAttr: mail
         nameAttr: gecos
         preferredUsernameAttr: uid

       groupSearch:
         baseDN: ou=MailGroup,ou=Proxy,dc=example,dc=com
         filter: "(objectClass=groupOfNames)"

         # A user is a member of a group when their DN matches
         # the value of a "member" attribute on the group entity.
         userAttr: DN
         groupAttr: member

         # The group name should be the "cn" value.
         nameAttr: cn
```

ここで設定しているgRPCのTLS接続用の証明書ファイルなどはコマンドラインから設定しています。

TLS証明書の準備については別の記事を参照してください。

https://qiita.com/YasuhiroABE/items/806a0947abdb828eef1f

:::note
上記記事中に記載していますが、easyrsaコマンドの実行時には``--san="DNS:..."``オプションが必須です。
:::

必要なファイルをconf/ディレクトリに配置してから次のようにkubectlコマンドを実行しました。

```bash:Secretオブジェクトの設定用
$ sudo kubectl -n dex create secret tls grpc-tls --cert=./conf/tls.crt --key=./conf/tls.nopass.key
# $ sudo kubectl -n dex create secret generic grpc-tls-ca --from-file=./conf/ca.crt
```

ca.crtはクライアント認証を行う場合に必要(``# tlsClientCA: /tls-ca/ca.crt``のコメントを外す事)です。

今回のテーマはPostgreSQLをバックエンドにしたdexの設定方法にあるので、この``02.cm-dexconf.yaml``の内容までで、説明は終っています。

## この他の設定について

この他の準備としてIngressから接続するためのHTTP接続用のServiceオブジェクトも定義しておきます。

```yaml:03.svc-dex.yaml
---
apiVersion: v1
kind: Service
metadata:
  namespace: dex
  name: issuer
spec:
  type: ClusterIP
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app: issuer
```

いよいよDeploymentオブジェクトを配置していきますが、まだdex-serverコンテナは公開設定になっていないので、``bin/dex serve /config/config-ldap.yaml`` を実行するようなrun.shを配置すれば動作します。

```yaml:04.deploy-dex.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: dex
  name: issuer
  labels:
    app: issuer
spec:
  replicas: 2
  selector:
    matchLabels:
      app: issuer
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: issuer
    spec:
      imagePullSecrets:
      - name: regcred
      containers:
      - name: webapi
        image: docker.io/yasuhiroabe/dex-server:latest
        ports:
        - containerPort: 8080
          name: issuer
        volumeMounts:
        - name: config
          readOnly: false
          mountPath: /config
        - name: grpc-certs
          readOnly: true
          mountPath: /tls
        - name: grpc-cacert
          readOnly: true
          mountPath: /tls-ca
      volumes:
      - name: config
        configMap:
          name: dex-conf
          items:
            - key: config-ldap.yaml
              path: config-ldap.yaml
      - name: grpc-certs
        secret:
          secretName: grpc-tls
      - name: grpc-cacert
        secret:
          secretName: grpc-tls-ca
```

最後にgRPC用のYAMLファイルを展開します。

```yaml:05.svc-grpc.yaml
---
apiVersion: v1
kind: Service
metadata:
  namespace: dex
  name: grpc
spec:
  type: LoadBalancer
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8088
  selector:
    app: issuer
```

LoadBalancerを指定していますが、最終的には設定用のアプリケーションを作成してnamespace:dex内部で動作させるつもりなので、将来的にはClusterIPになります。

# 稼動確認

問題はgRPCを経由して設定するための実施例がそれほど多くないという点です。

公式ガイドに掲載されているコードは例だとしても、注意が必要です。

1. 一度クライアントID等を登録をすると自動的には上書きされないので、変更するにはまず消すための機能を実装する必要がある
1. TLS証明書を準備する必要がある (Common Nameは無視されるので、subjectAltNameを適切に指定しなければいけない)
1. gPRCのdex側設定例では``tlsClientCA``を有効化しているので、クライアントコードではTLSの設定が省略されている

例えば既存の``dex-client``と設定されたClientSecretやRedirectURLsを削除するには次のようなコードを追加しています。

```go:
	delete_req := &api.DeleteClientReq{
		Id: "dex-client",
	}
	if resp, err := client.DeleteClient(context.TODO(), delete_req); err != nil {
		log.Warnf("failed deleting existing dex-client setting: %v", err)
	} else {
		log.Printf("client deleted successfully: %v", resp)
	}
 ```

ClientTLSに対応するためのコード例はdexのソースコードのexamplesディレクトリ内では記載があるので、そちらを参照すれば解決すると思います。

テスト環境を作ろとした時に、比較的本番に近い構成の環境ではないとホスト名の有無やIPアドレスの直指定の有無などによって本番でいきなりエラーになるようなことが想定されるので注意が必要かもしれません。

結局は慣れと経験値の差でスムーズに利用できるか分かれるのでしょうけれど、ちょっと難しいかもと感じました。

## サンプルコードの実行手順

公式ガイドに掲載されているコードは``package main``が宣言されているので、適当な作業ディレクトリで``main.go``というファイル名で保存しておきます。

このままでは``go build main.go``を実行してもエラーになってしまうので、go.modファイルを準備します。

```bash:
## go.modファイルがない初回だけinitを実行
$ go mod init example.com/m

## 新しいmoduleをimportした場合には次のコマンドを都度実行
$ go mod tidy

## コードを実行する場合には以下の手順
$ go build main.go
$ ./main
```

``example.com/m``の部分はダミーですが、このまま実行できます。

自分のGitHubリポジトリがあれば、自分が管理できる適当なプロジェクトに変更してください。

# gRPC API経由のサーバー設定について

クライアント設定のCRUD操作(Create, Read, Update, and Delete)が出来れば良いので、protoファイルもあるのでRubyを使って簡単にサービスを追加しようかなと思います。

ここから先はまた別の記事にまとめようと思います。

```plantuml:
actor "利用者"
usecase "新規登録"
note right: ClientSecretは不変として十分にランダムな数値を割り当てる。
usecase "RedirectURLの変更・追加"
usecase "登録の削除"

"利用者" -> "新規登録"
"利用者" -> "RedirectURLの変更・追加"
"利用者" -> "登録の削除"
```

ただ不特定多数が利用するとなると、どうやって操作の許可を出すのか決めておく必要がありそうです。

単純なネーミングルールでは少し弱いと思われるので、管理台帳を別にしてdexへの直接の操作は行わないような仕組みの方がいいのかもしれません。

```plantuml:
actor "バッチジョブ"
database "構成情報"
usecase "定期的な健全性チェック"

"バッチジョブ" .. "構成情報" : <<use>>
"バッチジョブ" -> "定期的な健全性チェック"
```

# さいごに

やってみればそれほど難しくはないのですが、これまでバックエンドを準備するのが面倒でsqlite3を使って1つのPodだけでサービスを提供してきました。

規模的にはそれでも困らないのですが、自分以外の利用者も自由に登録できるようにするには静的な設定では限界があるので良い機会でした。

Dexの汎用的な管理ツールはあっても良いと思うのですが、dexctlのようなCLIツールの構想はあっても、少なくとも公開されているものはなさそうです。

gRPCで自由に準備してください、ということだとは思うのですが、Webアプリにしたいものの当面は基本機能をCLIで実装するような形式になりそうです。