OIDCで認証できなかったユーザー向けにkubectlの設定ファイルを作成する

# はじめに

OpenID Connect (OIDC)を利用してKubernetesに接続するユーザーの管理を行っています。

OIDC Providerとして利用している[dexidp/dex](https://github.com/dexidp/dex)のLDAPコネクタにバグがあったため一部でパスワードが正しいにも関わらず認証できないユーザーが存在しました。

> 現在このバグはv2.39.1で修正されています。

kubectlコマンドが利用できないとkubernetesに関する操作が何もできなくなってしまうため、個別にconfigファイルを発行することにしました。

OIDCでユーザー認証をするはずだったユーザー個別に設定ファイルを準備したので、その顛末をまとめておきます。

# 参考資料

* https://aungzanbaw.medium.com/a-step-by-step-guide-to-creating-users-in-kubernetes-6a5a2cfd8c71
* https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/#normal-user

# 基本的な流れ

次のようなステップ毎に説明をしていきます。

1. 公開鍵認証方式による秘密鍵とCSRを準備する
1. KubernetesにCSRをCertificateSigningRequestオブジェクトとして登録する
1. 登録した情報を承認し、接続に必要な公開鍵を作成する
1. configファイルを作成し、ユーザーの~/.kube/configなどに配置して利用してもらう

## 公開鍵認証方式による秘密鍵とCSRを準備する

一般的なTLSに利用する証明書ファイルを準備する方法とほぼ一緒です。

ここでCommon Name(CN)を設定しますが、ここにUserIDとして認識させたい文字列(例: user01@example.org) などを指定します。参考資料に挙げたMediumの記事ではOなどを付与したDNを指定していますが、CNにUserIDだけを指定します。

また公式ガイドではgenrsaを利用していますが、いまさら感があるのでMediumの記事のようにed25519を利用します。

```bash:CSRの作成
$ openssl genpkey -out $(id -un).key -algorithm Ed25519
$ openssl req -new -key $(id -un).key -out $(id -un).csr -subj "/CN=$(id -un)@example.com"
```

"@example.com"の部分は実際に利用するユーザーIDとしてemail属性を参照するものとしています。uid属性を参照するのであればドメイン部分は不要だと思いますが、自分の環境に合わせてください。

## KubernetesにCSRをCertificateSigningRequestオブジェクトとして登録する

公式ガイドなどではヒヤドキュメントを利用したOne time jobとして描かれていますが、次のようなスクリプトを準備しました。

```bash:gen-csr.sh
#!/bin/bash

sed -e "s/__NAME__/$(id -un)/" -e "s/__CSR__/$(base64 < $(id -un).csr | tr -d '\n')/" <<EOF
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: __NAME__
spec:
  request: __CSR__
  signerName: kubernetes.io/kube-apiserver-client
  expirationSeconds: 31536000 # 365 days
  usages:
    - client auth
EOF
```

前の手順でUIDを元にファイルを準備していることを引き続き利用しています。

```bash:
$ bash gen-csr.sh > $(id -un).yaml
```

ここまでで、次のファイルが生成されました。

* $(id -un).key
* $(id -un).csr
* gen-csr.sh
* $(id -un).yaml

次に最後に生成した$(id -un).yamlファイルをapplyします。

Control Planeノードにログインするなどして管理者権限で次のように$(id -un).yamlファイルを適用します。

```bash:
$ kubectl apply -f $(id -un).yaml
certificatesigningrequest.certificates.k8s.io/ubuntu created
$ id -un
ubuntu
```

## 登録した情報を承認し、接続に必要な公開鍵を作成する

ここまでで登録したCSRはCONDITIONがPendingとなっていて、そのままでは利用できません。

利用するために必要な証明書を生成してもらうためにappoveします。

```bash:証明書を生成するためにapproveする
$ kubectl certificate approve $(id -un)
```

この前後でcsrオブジェクトの状態は次のようにPendingからApproved,Issuedに変化しています。

```bash:kubectl get csrの出力変化
$ kubectl get csr
NAME     AGE   SIGNERNAME                            REQUESTOR          REQUESTEDDURATION   CONDITION
ubuntu   89s   kubernetes.io/kube-apiserver-client   kubernetes-admin   365d                Pending

$ kubectl get csr
NAME     AGE     SIGNERNAME                            REQUESTOR          REQUESTEDDURATION   CONDITION
ubuntu   2m25s   kubernetes.io/kube-apiserver-client   kubernetes-admin   365d                Approved,Issued
```

```bash:証明書ファイルの抽出
$ kubectl get csr/$(id -un) -o jsonpath="{.status.certificate}" | base64 -d > $(id -un).crt
```

## configファイルを作成し、ユーザーの~/.kube/configなどに配置して利用してもらう

ここまでで接続に必要な情報が生成できたので、ユーザーに送付するためのconfigファイルを生成します。

Control Plane Nodeで作業している前提で、さらに次の情報を埋め込んでいます。

* 接続先のAPI Server: kubecamp.example.com:6443

次のようなスクリプトでユーザー用の設定ファイルを配布しています。

```bash:
#!/bin/bash

sed -e "s/__CA_CRT__/$(base64 < /etc/kubernetes/ssl/ca.crt | tr -d '\n')/" -e "s/__USER__/$(id -un)/g" <<EOF > config.$(id -un)
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: __CA_CRT__
    server: https://kubecamp.example.com:6443
  name: cluster.local
contexts:
- context:
    cluster: cluster.local
    user: __USER__@example.com
  name: kubecamp.example.com@cluster.local
current-context: kubecamp.example.com@cluster.local
kind: Config
preferences: {}
users:
EOF

kubectl --kubeconfig config.$(id -un) config set-credentials $(id -un)@example.com --client-key=$(id -un).key --client-certificate=$(id -un).crt --embe
d-certs=true
```

スクリプト中ではuser:やname:にメールアドレス形式でユーザーIDを埋め込んでいますが、この部分はマッチすれば何でも構いません。CSRに設定したCNをユーザーIDとして利用するため、この段階では単純に設定ファイル内での整合性が取れていれば何でも構いません。

最終的に **config.$(id -un)** のファイル名でユーザーに渡すための設定ファイルが生成されます。

```bash:使い方・検証方法
$ kubectl --kubeconfig config.$(id -un) get node
NAME    STATUS   ROLES           AGE      VERSION
node1   Ready    control-plane   2y230d   v1.28.6
node2   Ready    control-plane   2y230d   v1.28.6
node3   Ready    <none>          2y230d   v1.28.6
node4   Ready    <none>          2y230d   v1.28.6

$ kubectl --kubeconfig config.$(id -un) -n $(id -un) get cm
NAME               DATA   AGE
kube-root-ca.crt   1      34d

$ kubectl --kubeconfig config.$(id -un) -n kube-system get cm
Error from server (Forbidden): configmaps is forbidden: User "ubuntu@example.com" cannot list resource "configmaps" in API group "" in the namespace "kube-system"
```

ユーザーには自身のnamespace内にあるconfigmap(cm)やsecretのみにアクセスできるよう権限を付与しているので、正しく動作することを確認します。

# ユーザー毎のRoleBindingやClusterRoleBindingの設定

これらは本来OpenID Connectで認証できたとしてユーザー毎に個別に設定しているはずなので省略します。

ただ、公式ガイドにはkubectlコマンドを駆使した例が掲載されていますが、あまり汎用性がないように思えるので``kubectl get -n $(id -un) rolebinding``や``kbuectl get --all-namespaces rolebinding``のような方法で設定や確認をするべきだと思います。

少なくとも例に挙げられている``kubectl create role developer --verb=create --verb=get --verb=list --verb=update --verb =delete --resource=pods``を実行した結果が次のような定義に展開されるということは把握しておくべきだと思います。

```bash:role/developerの定義を確認する
kubectl get role developer -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: "2024-04-17T04:01:42Z"
  name: developer
  namespace: default
  resourceVersion: "307861872"
  uid: a9adf946-7eb7-48f7-96ed-e3e5dfa115d6
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - create
  - get
  - list
  - update
  - delete
```

Roleオブジェクトのスコープはnamespaceなので、ユーザー毎に作成する必要があって、kubectl の create roleを使った方法では少し細やかな設定をするのが難しくなるのではないかと思われます。

# さいごに

ここで作成したUser用の設定ファイルはOIDCで認証したUserと同じ権限を付与しますが、その他に有効期限を長期に設定したいバッチジョブなどを実行するプロセス用に利用することもできます。

Kubernetesを多人数で利用する際の手法としてOIDC認証は容易に導入できますが、少し使いにくい点があるのも事実です。

最初にMediumの記事だけを参考にした時には、組織属性(O=edit)が邪魔をして期待どおりに動作しませんでした。この記事がなにかしらの参考になれば幸いです。