KibanaにMFA(多要素認証)の経路を作る

# はじめに
自宅のkubernetes上にデプロイしているkibanaをインターネットに外部公開しようとしましたが、デフォルトの認証ではなく多要素認証にしてみようと考えました。
どうせならアプリ側で都度認証の実装をするのではなく、OpenShiftのようにkubernetes上で汎用的な認証サービスをリバースプロキシで提供しようと考えました。
実装検証も兼ねているので、keycloak+mariadbとリバースプロキシ用のhttpdのpodを作成します。

# 前提環境
- kubernetes 1.28.1 (aarch64 : Raspberry Pi 4B)
- Synology NAS (Synology CSI Driver for Kubernetes)
- Elasticsearch / Kibana 8.11.1

# keycloak + mariadb (pod) の作成
keycloakをSSL化する際の証明書を作成してconfigmapにしておきます。(※お作法としてはsecretの方が良いです)

```
# kubectl create ns keycloak
# openssl req -x509 -nodes -newkey rsa:4096 -keyout keycloak.key -out keycloak.crt -days 365
# kubectl create configmap keycloak-crt -n keycloak --from-file=keycloak.crt --from-file=keycloak.key
```

mariadb用のPVCを用意します。永続ストレージはご自宅の環境に合う内容にしてください。

```yaml:iscsi-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: iscsi-keycloak-pvc
  namespace: keycloak
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: synology-sc
```

(※AGEは無視してね)
```
# kubectl apply -f iscsi-pvc.yaml
# kubectl get pvc -n keycloak
NAME                 STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
iscsi-keycloak-pvc   Bound    pvc-5b2f2957-f26f-4d49-bf11-1f7ca292e7ee   10Gi       RWO            synology-sc    56d
```

mariadbとkeycloakのコンテナでpodを作成します。分割してマイクロサービスにした方が良いです。
(※自宅でDBはElasticsearchを使っていて、mariadbはkeycloak専用です)

```yaml:keycloak-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: keycloak
  namespace: keycloak
  labels:
    app: keycloak
spec:
  containers:
  - name: mariadb
    image: docker.io/mariadb:10.5
    env:
    - name: MARIADB_DATABASE
      value: "kcdb"
    - name: MARIADB_ROOT_PASSWORD
      value: "rootpass1105"
    ports:
    - containerPort: 3306
    volumeMounts:
    - name: data
      mountPath: /var/lib/mysql
  - name: keycloak
    image: quay.io/keycloak/keycloak:22.0.5
    env:
    - name: KC_DB
      value: "mariadb"
    - name: KC_DB_URL
      value: "jdbc:mariadb://localhost:3306/kcdb?characterEncoding=UTF-8"
    - name: KC_DB_USERNAME
      value: "root"
    - name: KC_DB_PASSWORD
      value: "rootpass1105"
    - name: KEYCLOAK_ADMIN
      value: "kcadmin"
    - name: KEYCLOAK_ADMIN_PASSWORD
      value: "kcadmin0420!"
    - name: KC_PROXY
      value: "edge"
    - name: KC_HTTP_ENABLED
      value: "true"
    - name: KC_HTTPS_PORT
      value: "8443"
    - name: KC_HTTPS_CERTIFICATE_FILE
      value: "/opt/keycloak/tls/keycloak.crt"
    - name: KC_HTTPS_CERTIFICATE_KEY_FILE
      value: "/opt/keycloak/tls/keycloak.key"
    args: ["start-dev"]
    ports:
    - name: http
      containerPort: 8080
    - name: https
      containerPort: 8443
    volumeMounts:
    - name: crtvol
      mountPath: /opt/keycloak/tls
  volumes:
  - name: crtvol
    configMap:
      name: keycloak-crt
  - name: data
    persistentVolumeClaim:
      claimName: iscsi-keycloak-pvc
```

バージョンは個人的な理由があり、mariadbは10.5でkeycloakはquay.ioの22.0.5を使います。
mariadb側の設定はpod内部の事なので割愛しますが、keycloakの管理ユーザーは「kcadmin」パスワードは「kcadmin0420!」にして、httpとhttpsの両方でアクセスできるようにしています。
小さい設定ファイルのためにストレージ切ったりコンテナイメージをビルドするのは嫌なので、configmapを活用しています。

```
# kubectl apply -f keycloak-pod.yaml
# kubectl get pods -n keycloak
NAME       READY   STATUS    RESTARTS   AGE
keycloak   2/2     Running   0          56d
```

keycloakはテキトーにNodePortで公開しておきます。

```yaml:keycloak-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: keycloak
  namespace: keycloak
  labels:
    app: keycloak
spec:
  type: NodePort
  ports:
    - name: keycloak
      port: 8080
      targetPort: 8080
      nodePort: 30808
    - name: keycloaks
      port: 8443
      targetPort: 8443
      nodePort: 30843
  selector:
    app: keycloak
```

```
# kubectl apply -f keycloak-svc.yaml
# kubectl get svc -n keycloak
NAME       TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)                         AGE
keycloak   NodePort   10.96.55.87   <none>        8080:30808/TCP,8443:30843/TCP   56d
```

# keycloak側での設定
keycloakは元々設定項目が多いのですが、バージョンによってメニューや設定項目名が変わっているので、追従するのは大変です。(元々そんなに理解もしていないのに...)

keycloakのサービスにアクセスして「Administration Console」を開きます。

![keycloak0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/1731ace7-1da7-f3d9-5e05-a5f33e461d84.png)

podで設定した管理ユーザー「kcadmin」でログインします。

![keycloak1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/9c649988-ff3e-d05a-30e1-5c5b8b51c089.png)

## Realmの作成
左上の「master」(レルム)がプルダウンメニューになっています。プルダウンから「Create Realm」ボタンを押します。

![keycloak2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/bdd1c708-0fa3-ecf9-c852-153b1b20b4a1.png)

以下の項目を設定して「Create」ボタンを押します。
Realm name : "MyLabo"

![keycloak3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/a599941c-0e4d-141d-786a-b9faf31fe2e7.png)

## Clientsの作成

左のメニューから「Clients」を選択して、「Create client」ボタンを押します。

![keycloak4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/1757c00a-6a24-201d-db47-34570a717a73.png)

以下の項目を設定して「Next」ボタンを押します。
Clinet ID : "kibana"

![keycloak5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/1f279e4a-e76c-edcd-e7a1-2c83c78ce7b4.png)

以下の項目を設定して「Next」ボタンを押します。
Client authentication : "On"

![keycloak6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/ba385742-bcbe-1695-ddad-05740f225469.png)

以下の項目を設定(リバースプロキシ予定のKibanaのURL)して「Save」ボタンを押します。
Valid redirect URIs : "http://192.168.1.9:31561/*"
(※とりあえず自宅内からアクセスできるURLにしてます。リバースプロキシを外部公開した場合は変更する個所です。)

![keycloak7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/a43637a4-a397-9c5a-e7b8-e9f5c88de4ee.png)

「Credentials」タブを開いて、Client secretをメモ帳かどこかにコピーしておきます。

![keycloak8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/5fbd2d85-b2e3-921d-45d1-b8ab03ef03bb.png)

もう一つ、左メニュー「Realm settings」の「Endpoints」から「OpenID Endpoint Configuration」のURLをコピーしておきます。

![keycloak9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/e521e423-8493-744f-ea48-d0c15696af6f.png)

## ユーザーの作成

左のメニューから「Users」を選択して、「Add user」ボタンを押します。

![keycloak10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/6ed605d9-99df-cc41-ddff-5363f18b6061.png)

ユーザー名は任意で、初回ログイン時にOTPの設定とパスワード再設定するように設定して「Create」ボタンを押します。
Required user actions : "Configure OTP" , "Update Password"
Username : "haomei"

![keycloak11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/87d57a00-e4b7-93a2-fedc-0fe842a464df.png)

Credentialsタブを開いて、「Set password」ボタンを押します。

![keycloak12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/df8f4f4a-7a7c-b28e-fa9f-bcddc1e6731f.png)

ユーザーの初期パスワードを入力して「Save」ボタンを押します。

![keycloak13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/ac12880d-6f9c-5fc5-3450-015214b4e21b.png)

![keycloak14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/bdc75e95-a3bd-5168-ae18-318ca1849bb5.png)

## 2FAの設定

左メニューの「Authentication」を選択して「Flows」タブの「browser」をクリックします。

![keycloak15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/6611933f-065e-88c7-7370-3e0108e09909.png)

「Browser - Conditional OTP」を「Required」にします。

![keycloak16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/1de1f5fe-edfa-a7b9-ae7a-c0bd70327f4a.png)

# リバースプロキシ(pod)の作成

httpdのコンテナイメージからカスタムイメージを作成します。

```dockerfile:Dockerfile
FROM httpd:latest

RUN apt-get update && apt-get install -y \
    libapache2-mod-auth-openidc \
    apache2-utils \
    && apt-get clean

RUN sed -i \
    -e 's/^#\(Include .*httpd-proxy.conf\)/\1/' \
    -e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' \
    -e 's/^#\(LoadModule .*mod_proxy_http.so\)/\1/' \
    conf/httpd.conf

RUN echo "Include conf/extra/proxy.conf" >> conf/httpd.conf
RUN echo "Include conf/extra/openidc.conf" >> conf/httpd.conf
```

dockerコマンドかpodmanコマンドでbuildして自宅のNodePortで公開しているコンテナレジストリにpushします。
自宅にコンテナレジストリが無い場合はkubernetesの各ノードにコピーしてください。

```
# podman build -t 192.168.1.9:30500/rproxy:v1.0 .
# podman push --tls-verify=false 192.168.1.9:30500/rproxy:v1.0
```

httpdの拡張設定(OIDC連携用)のファイルを作成します。
ほぼテンプレですが、以下の項目は今まで設定した内容や確認した内容で埋める必要があります。

OIDCProviderMetadataURL : keycloakの「OpenID Endpoint Configuration」のURL
OIDCClientID : keycloakの「Clinet ID」
OIDCClientSecret : keycloakの「Client secret」
OIDCRedirectURI : keycloakの認証が成功した後に遷移させたいURL
OIDCCryptoPassphrase : 何でも良かったはず...

```:openidc.conf
LoadModule auth_openidc_module /usr/lib/apache2/modules/mod_auth_openidc.so

OIDCSSLValidateServer Off
OIDCProviderMetadataURL https://192.168.1.9:30843/realms/MyLabo/.well-known/openid-configuration
OIDCClientID kibana
OIDCClientSecret disSUTwote75wC7Ipho2c6JRik9YvB6a
OIDCRedirectURI http://192.168.1.9:31561/login
OIDCCryptoPassphrase hogehoge

<Location />
    AuthType openid-connect
    Require valid-user

    Define ES_USERNAME elastic
    Define ES_PASSWORD "password"
    Define CREDENTIALS ${ES_USERNAME}:${ES_PASSWORD}
    RequestHeader set Authorization "expr=Basic %{base64:${CREDENTIALS}}"
</Location>
```

次にリバースプロキシの設定ファイルを作成します。

```:proxy.conf
ServerName localhost

ProxyRequests Off
ProxyPass / http://kibana.elastic.svc.cluster.local:5601/
ProxyPassReverse / http://kibana.elastic.svc.cluster.local:5601/

<Location />
    ProxyPreserveHost On
    Require all granted
</Location>
```

設定ファイルをconfigmapにしておきます。

```
# kubectl create configmap extra-httpd-conf -n default --from-file=openidc.conf --from-file=proxy.conf
```

リバースプロキシのpodを作成します。imageはhttpdのコンテナイメージから作成したカスタムイメージを指定します。
設定ファイルはconfigmapをpod内のディレクトリにmountしているので、deploymentにすれば動的な設定変更の運用ができると思います。

```yaml:rproxy-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: rproxy
  labels:
    app: rproxy
spec:
  containers:
  - name: httpd
    image: localhost:30500/rproxy:v1.0
    ports:
    - containerPort: 80
    volumeMounts:
    - name: exconf
      mountPath: /usr/local/apache2/conf/extra
  volumes:
  - name: exconf
    configMap:
      name: extra-httpd-conf
```

```
# kubectl apply -f rproxy-pod.yaml
# kubectl get pods/rproxy
NAME     READY   STATUS    RESTARTS   AGE
rproxy   1/1     Running   0          3h44m
```

nodePortのポート番号は、openidc.conf の OIDCRedirectURI で指定しているポート番号に合わせてください。
また、MFA対応のkibanaもこのポート番号でのログインをする事になります。

```yaml:rproxy-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: rproxy
  labels:
    app: rproxy
spec:
  type: NodePort
  ports:
    - name: rproxy
      port: 80
      targetPort: 80
      nodePort: 31561
  selector:
    app: rproxy
```

```
# kubectl apply -f rproxy-svc.yaml
# kubectl get svc/rproxy
NAME     TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
rproxy   NodePort   10.96.6.229   <none>        80:31561/TCP   63d
```

リバースプロキシはURLベースにした方が便利だと思うのですが、kibana側の対応が必要になるので今回は見送ります。

# kibanaへのログイン

リバースプロキシのサービス http://192.168.1.9:31561/ へアクセスすると、keycloakの認証画面に遷移します。keycloakで追加したユーザー名と初期パスワードでログインします。

![login0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/2466ea47-e39e-499b-48ef-b1e449f1abf5.png)

OTPの初回認証画面になります。スマホにTOTPのアプリをインストールしてカメラでQRコードを読み取るか、ブラウザにTOTPの拡張機能をインストールして同様にQRコードをキャプチャして設定します。
(ここからはもたもたしているとkeycloak側がタイムアウトしてしまいますのでスムーズに！)

![login1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/6b27a6b5-e70e-11c0-24b5-d970ff3c82f4.png)

EdgeやChromeの場合は、Authenticator: 2FA Clientをインストールします。

![login3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/19317a4c-63b1-a3bd-b293-059264b68dda.png)

拡張機能からQRコードをスキャン(枠で囲む)して設定します。

![login5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/c34a049e-e1eb-9aad-2410-de7e3387f942.png)

OTPのコードが表示されるので、クリックしてコピーします。

![login7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/dea56c89-be54-9b09-dd3a-182b8d68616f.png)

「One-time code」はコピーしたOTPコードを貼り付けて「Device Name」はテキトーに入力して「Submit」ボタンを押します。

![login8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/b582ab52-1a56-900c-5a44-16bae2b0f522.png)

次にパスワードの再設定も促されるので、設定して「Submit」ボタンを押します。

![login9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/8dc823e5-e8ff-f6f8-3861-490e08c4639d.png)

うまく2要素認証でkibanaにログインできました。

![login10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/af679583-c8cf-7985-e9b8-a4fe687bc5d5.png)

# おわりに
長い手順でしたが、自宅サービスに2要素認証を取り入れる事ができそうです。
ユーザー追加の部分はkeycloakのAdmin CLIを使えばもっと簡素化できそうですね。

https://qiita.com/Y-Shikase/items/a091abeae4c282bf883d#%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%AE%E4%BD%9C%E6%88%90