KubernetesにADFSを連携させてOIDC認証を実装する

## 背景
Kubernetes (K8s)基盤の構築にあたり「認証基盤として既存のActive Directoryを活用したい」というオーダーが。

[認証 | Kubernetes](https://kubernetes.io/ja/docs/reference/access-authn-authz/authentication/#openid-connect%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3)
K8sの認証方式は複数の選択肢があるが、今回のオーダーについてはOpenID Connectトークン方式がマッチしそう。
その線で実現可能性を調査したところ、ADFS (Active Directory Federation Services)と連携させればかなり見込みがありそう。

またそれだけだとトークンの取得とクライアントツールへの設定を手作業で行う必要があって非常に面倒なため、簡略化のためのプラグインである[kubelogin](https://github.com/int128/kubelogin)の導入も合わせて行う。

というわけで実装フローは大きく分けて5つ。

[1. AD設定](#1-ad設定)
[2. ADFS構築](#2-adfs構築)
[3. kube-apiserver設定追加](#3-kube-apiserver設定追加)
[4. kubelogin導入](#4-kubelogin導入)
[5. kubeconfig設定](#5-kubeconfig設定)

## 1. AD設定
K8sで認証に使う対象ユーザがまだいない場合は作成する。

権限の紐付けをグループ単位で行う場合は必要に応じてセキュリティグループを作成して、そこにユーザを所属させる。
(基本的にはユーザ単位よりグループ単位で権限制御したいことが多いと思う)
![スクリーンショット 2024-11-27 10.53.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/c25360ab-7cd8-41d3-47ce-2e4b4dffaa9e.png)
![スクリーンショット 2024-11-27 10.54.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/e0938efb-6abe-8881-88d0-784472a8a74d.png)


注意点として、ドメインのスコープはグローバルである必要がある。
ドメインローカルだとセキュリティグループ情報がトークンに乗ってこなかった。
(ユニバーサルは試していない)

## 2. ADFS構築
アプリケーショングループを作成する。
![スクリーンショット 2024-11-27 10.56.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/aa29acf4-4252-b925-c2ad-e0baeebd5fca.png)

続いてアプリケーショングループにサーバー アプリケーションとWeb APIを追加する。

### 2-1. サーバー アプリケーション
クライアントIDは自動生成で、リダイレクトURIは後述のkubeloginとの兼ね合いのため`http://localhost:8000`を指定。
![スクリーンショット 2024-11-27 10.56.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/7adbb2e5-8355-e1c9-6afa-023111780bdf.png)

クライアントシークレットを作成してメモっておく。
![スクリーンショット 2024-11-27 10.56.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/8f8afae6-eb44-963b-c13d-db79a80f7dcf.png)

### 2-2. Web API
証明書利用者の識別子はサーバー アプリケーションのクライアントIDを追加しておく。
![スクリーンショット 2024-11-27 10.57.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/9fcd051f-7163-c571-5275-96ce84c8b258.png)

アクセス制御ポリシーはこんな感じ(デフォルトからそのまま)
![スクリーンショット 2024-11-27 10.57.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/54f8f5d2-06a7-6353-ddd7-88a759f1da5d.png)

グループ単位で権限制御する場合はトークンにグループ情報を含めるよう要求する設定を追加する。
![スクリーンショット 2024-11-27 10.57.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/a50ae08c-091a-d0f7-5ca5-2a47e039b63b.png)
![スクリーンショット 2024-11-27 10.57.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/5ba90856-a80a-1b45-d48a-989b8d9e9b40.png)

スコープには`allatclaims`と`openid`を許可するように。
![スクリーンショット 2024-11-27 10.58.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555024/a6ed879d-e27d-c741-54a1-004b89422edf.png)

## 3. kube-apiserver設定追加
kube-apiserverにOIDC認証のための設定を追加する。

kubeadmで構築した場合、通常はコントロールプレーンの`/etc/kubernetes/manifests/`配下にマニフェストファイルがあるのでそこに追記する形。
static Podなのでマニフェストを更新すると自動で再起動がかかり反映されるはず。
(もし反映されてるか怪しい場合はPodを一度deleteして起動させると良いかも)

具体的にはspecのcontainersフィールド内、commandフィールドに下記を追記する。
```
spec:
  containers:
  - command:
    - kube-apiserver

中略

    - --oidc-issuer-url=https://<ADFSサーバのFQDN>/adfs
    - --oidc-client-id=<クライアントID>
    - "--oidc-username-prefix=oidcuser:" # Kubernetesの既存の認証と区別するためのプレフィックス。ユーザ名の前にこの値が付与される。
    - "--oidc-groups-prefix=oidcgroup:" # 用途は同上。グループ名の前にこの値が付与される。
    - --oidc-username-claim=upn
    - --oidc-groups-claim=group
```

※ちなみに検証用途などでADFSサーバ側で自己署名証明書を使っていたりすると、検証のために`oidc-ca-file`でそのCAを指定する必要がある。
　(検証時はこの辺りでかなり難航した)
```
    - --oidc-ca-file=<CA証明書のパス>
```

## 4. kubelogin導入
続いて認証作業簡略化のため、プラグインを導入する。

kubectlが導入済みの環境で、下記を行なう。

### 4-1. プラグインマネージャ (Krew) インストール
下記ドキュメントの手順に従ってインストールする。
https://krew.sigs.k8s.io/docs/user-guide/setup/install/

### 4-2. OIDC用プラグイン (kubelogin) インストール

下記コマンドを実行する。
`kubectl krew install oidc-login`

環境によっては他の手順が適してるかもなので下記参照。
https://github.com/int128/kubelogin?tab=readme-ov-file#setup

# 5. kubeconfig設定
kubeconfigのusersフィールドに、OIDC認証で用いるアクセス情報を追記する。
```
users:
- name: <ユーザ名(というより認証情報名)> # contextの紐付けに用いる認証情報を識別するためのものなので、ADのユーザ名と同一である必要はない
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: kubectl
      args:
      - oidc-login
      - get-token
      - --oidc-issuer-url=https://<ADFSサーバのFQDN>/adfs
      - --oidc-client-id=<クライアントID>
      - --oidc-client-secret=<シークレット>
```
※kube-apiserver側と同じく、検証用途などでADFSサーバ側で自己署名証明書を使っていたりすると`certificate-authority`でそのCAを指定する必要がある。
```
    - --certificate-authority=<CA証明書のパス>
```

# まとめ
ここまでミスや漏れなく設定すれば認証できるようになるはず。

kubectlでリクエストを実行すると端末上でブラウザが開き、ADFSの認証画面が表示される。
認証に成功すると、トークン情報の保存など面倒なことを裏で自動的にやってくれて、レスポンスが返ってくる。

## おまけ: 権限付与
本筋ではないので省略したが、(Cluster)Roleと(Cluster)RoleBindingによる権限の紐付けを行わないと、
何の権限もないのでリクエストは拒否されるはず。

ユーザ単位で権限制御を行う場合はユーザに権限を紐付ける。
(`oidcuser:`というプレフィックスは[上述](#3-kube-apiserver設定追加)の通り今回の手順では使用したが、必須ではないので注意)
```
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: oidc-binding
subjects:
- kind: User
  name: oidcuser:<ADのユーザ名>@<ADのドメイン名>
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: <ロール名>
  apiGroup: rbac.authorization.k8s.io
```

同様にグループ単位で権限制御を行う場合はグループに権限を紐付ける必要がある。
```
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: oidc-binding
subjects:
- kind: Group
  name: oidcgroup:<グループ名>
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: <ロール名>
  apiGroup: rbac.authorization.k8s.io
```

---
参考文献
* [Configure Kubernetes API Authentication using Active Directory Federation Services (ADFS) and OpenID Connect](https://medium.com/@kevintim/configure-kubernetes-api-authentication-with-adfs-and-openid-connect-b233eb989983)