はじめに
組織の認証管理に利用しているLDAPに登録されているID/Passwordを利用したいので、OpenID Providerのdex, https://github.com/dexidp/dex, を利用しています。
今回は数名で利用するKubernetesのmaster nodeにログインしたユーザーが/etc/kubernetes/admin.confを共有している状況がまずいかなと思ったので、利用者を個別に識別するためdexを利用したいと思います。
【2020/04/24追記】Kubespray v2.12.5 (kubernetes v1.16.8) でも稼動しています。
【2021/04/15追記】Dexを更新した際に遭遇した問題について記述しています。
【2024/08/28追記】Kubespray v2.25.0 (kubernetes v1.29.5) でも稼動しています。
前提
- Kubespray: v2.8.5 → v2.12.5 → v2.15.0 → v2.25.0
dexが次のURLで稼動しているものとして説明します。
- URL: https://dex.example.com/dex/.well-known/openid-configuration
- client-id: example-app
参考資料
- dex(OpenID Connect Provider)をLDAP認証で動かしてみる
- Kubernetes authentication via GitHub OAuth and Dex Mediumの記事。Tokenを発行する独自WebアプリとDex, k8sとの関係について説明している
- Authenticating kubernetes.io の公式ガイド
- Kubernetes authentication through dex github上のdexガイド
- https://github.com/johnw188/dex-example
- https://qiita.com/shoichiimamura/items/91208a9b30e701d1e7f2
- https://banzaicloud.com/blog/k8s-rbac/
- https://int128.hatenablog.com/entry/2018/01/12/113745
- https://github.com/dexidp/dex/issues/1336
作業の流れ
参考資料のMediumの記事とは違い、dexに付属しているexample-appを利用していきます。
このため実際の利用には、手動で~/.kube/configを更新する手間がかかりますが、動きを理解する上では有用だと思います。
実際の利用ではexample-appアプリに相当する専用アプリを設計し、.kube/configファイルを更新するスクリプトを準備する方が利便性も向上すると思います。
まずは次の順序で設定を行なっていきます。
- example-appをWebアプリとして実行するdockerコンテナの作成とk8s環境へのデプロイ
- kubesprayで構築されているk8s環境へのOIDC設定
- 各ユーザー毎に必要なkubectlを実行するまでの流れ
example-appを実行するdockerコンテナの作成とk8s環境へのデプロイ
現在利用しているイメージはalpineをベースにした上で、multi stage buildを利用して約40MBほどのサイズになっています。サーバーにもクライアントにも使えるように、examplesもビルドしていて、/dex/bin/dex, /dex/bin/example-app が利用できます。
FROM docker.io/library/golang:1.22-alpine as dex
ARG DEX_TAGNAME=v2.39.1
RUN apk --no-cache add git make gcc libc-dev patch
RUN mkdir /work
WORKDIR /work
RUN git clone --depth 1 --branch $DEX_TAGNAME 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
COPY patch/20240523-v2.39.1.patch /v2.39.1.patch
RUN patch -p1 < /v2.39.1.patch
RUN make build && make examples
FROM docker.io/library/alpine:3.19
MAINTAINER YasuhiroABE <yasu-abe@u-aizu.ac.jp>
RUN apk update && apk add --no-cache bash ca-certificates
RUN mkdir -p /dex/bin
COPY --from=dex /work/dex/bin/. /dex/bin/.
COPY run.sh /run.sh
RUN chmod +x /run.sh
WORKDIR /dex
## Server Settings
EXPOSE 5556
ENV CONFIG_FILEPATH="/config/config-ldap.yaml"
## Client Settings
EXPOSE 5555
ENV DEXC_LISTENURL="http://0.0.0.0:5555"
ENV DEXC_REDIRECTURL="http://192.168.1.1:5555/callback"
ENV DEXC_ISSUERURL="http://192.168.1.2:5556/dex"
ENV DEXC_CLIENT_ID="example-app"
ENV DEXC_CLIENT_SECRET="ZXhhbXBsZS1hcHAtc2VjcmV0"
VOLUME ["/config", "/data"]
ENTRYPOINT ["/run.sh"]
次のようなrun.shファイルをENTRYPOINTに指定しています。
#!/bin/bash -x
exec bin/example-app \
--client-id "${DEXC_CLIENT_ID}" \
--client-secret "${DEXC_CLIENT_SECRET}" \
--listen "${DEXC_LISTENURL}" \
--redirect-uri "${DEXC_REDIRECTURL}" \
--issuer "${DEXC_ISSUERURL}"
Dockerfileに記述しているpatch/20240523-v2.39.1.patchファイルの内容は次のようになっています。
これはMediawikiのように言語設定によってリダイレクトURLに日本語が含まれるような場合にデコードされたURLを適切に扱えないDexの問題を回避するためのものです。詳細はDexのPRを確認してください。
diff --git a/server/handlers.go b/server/handlers.go
index ccd534d9..85335972 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -880,7 +880,11 @@ func (s *Server) calculateCodeChallenge(codeVerifier, codeChallengeMethod string
func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client storage.Client) {
ctx := r.Context()
code := r.PostFormValue("code")
- redirectURI := r.PostFormValue("redirect_uri")
+ redirectURI, err := url.QueryUnescape(r.PostFormValue("redirect_uri"))
+ if err != nil {
+ s.tokenErrHelper(w, errInvalidRequest, "No redirect_uri provided.", http.StatusBadRequest)
+ return
+ }
if code == "" {
s.tokenErrHelper(w, errInvalidRequest, `Required param: code.`, http.StatusBadRequest)
なおDexサーバーとして稼動しているコンテナのrun.shは次のようになっています。
#!/bin/bash -x
bin/dex serve "${CONFIG_FILEPATH}"
config-ldap.yamlの内容は環境によって違うため個別に設定が必要ですが、サーバー側は次のような設定にしています。
...
staticClients:
- id: example-app
redirectURIs:
- http://127.0.0.1:8000/callback
name: 'Example App'
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
example-appが動作するalpineのイメージは DockerHubで yasuhiroabe/dex-webapp の名前で公開しています。(実際に使用しているDockerfileは少し違います)
このイメージを使うためのコマンドラインは次のようなものです。
dexサーバー側で、client-id, client-secretを変更している場合には、適宜加えてください。
$ podman run -it --rm \
--env DEXC_LISTENURL="http://0.0.0.0:5555" \
--env DEXC_REDIRECTURL="http://127.0.0.1:8000/callback" \
--env DEXC_ISSUERURL="https://dex.example.com/dex" \
-p 8000:5555 \
docker.io/yasuhiroabe/dex-webapp
このサービスに http://127.0.0.1:8000/ などでログインして稼動確認を行います。
DEXC_REDIRECTURLのURLは、このコンテナ自身を指すものですが、k8sのような実際の環境ではサービス用のingressやloadBalancerで割り当てているホスト名やreverse-proxyのホスト名になると思います。あらかじめDexサーバー側の設定で、ここに指定するリダイレクトURLをexample-appのredirectURIsのリストに加えておく必要があります。
DEXC_ISSUERURLはDexサーバーのURLを指定します。あらかじめDexが稼動しているものとして、この説明ではサーバーの構築は省略しています。Dexサーバー環境の構築については下記の記事を参考にしてください。
kubesprayで構築されたk8s環境へのOIDC設定
マニュアルどおに、inventory/sample/ を inventory/mycluster/ にコピーしているものとして進めます。
diff --git a/inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.yml b/inventory/mycluster/group_vars/k8s-c
luster/k8s-cluster.yml
index 8338a0eb..97a9e718 100644
--- a/inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.yml
+++ b/inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.yml
@@ -36,7 +36,7 @@ kube_log_level: 2
credentials_dir: "{{ inventory_dir }}/credentials"
## It is possible to activate / deactivate selected authentication methods (oidc, static token auth)
-# kube_oidc_auth: false
+kube_oidc_auth: true
# kube_token_auth: false
@@ -44,12 +44,15 @@ credentials_dir: "{{ inventory_dir }}/credentials"
## To use OpenID you have to deploy additional an OpenID Provider (e.g Dex, Keycloak, ...)
# kube_oidc_url: https:// ...
+kube_oidc_url: https://example.com/dex
# kube_oidc_client_id: kubernetes
+kube_oidc_client_id: example-app
## Optional settings for OIDC
# kube_oidc_ca_file: "{{ kube_cert_dir }}/ca.pem"
# kube_oidc_username_claim: sub
+kube_oidc_username_claim: email
# kube_oidc_username_prefix: oidc:
-# kube_oidc_groups_claim: groups
+kube_oidc_groups_claim: groups
# kube_oidc_groups_prefix: oidc:
## Variables to control webhook authn/authz
v2.15.0では明示的にkube_oidc_groups_claimを指定しないと、グループによる制御ができませんでした。
この内容をupgrade-cluster.ymlで反映させておきます。
$ ansible-playbook upgrade-cluster.yml -b -i inventory/mycluster/hosts.ini
反映が無事に終わると、masterノードの /etc/kubernetes/kubeadm-config.v1alpha3.yaml に内容が反映されます。
$ ansible kube-master -i inventory/mycluster/hosts.ini -b -m command -a 'grep oidc /etc/kubernetes/manifests/kube-apiserver.yaml'
node2 | CHANGED | rc=0 >>
oidc-issuer-url: http://dex.example.com/dex
oidc-client-id: example-app
oidc-username-claim: email
oidc-groups-claim: groups
node1 | CHANGED | rc=0 >>
oidc-issuer-url: http://dex.example.com/dex
oidc-client-id: example-app
oidc-username-claim: email
oidc-groups-claim: groups
oidc-client-id は、dexサーバー側の設定で staticClients: に id: として登録されている名称である必要があります。
Dexで認証した権限でkubectlを実行する
クラスター全体で管理者になりたければcontrol-planeノードでrootになって /root/.kube/config を利用すれば良いので、ここでは個人の$USER名と同じnamespace(NS)があるとして、そのNS内で自由に活動できる権限のあるRoleBindingを生成することをゴールにしてみます。
example-app から必要な情報を入手する
まず kubespray で oidc-issuer-url で指定したDexに接続しているexample-appにアクセスし、"ID Token" を取得します。
~/.kube/config の生成
続いて公式ガイドに従って ~/.kube/config ファイルを生成します。
次のようなスクリプトを準備しました。
#!/bin/bash
YA_NAME=user01@example.com
YA_ISSURL="https://dex.example.com/dex"
YA_CLIENT_ID="example-app"
YA_REF_TOKEN="ChlmNHZoM2s1....mhuZndt"
YA_ID_TOKEN="eyJ....Mxqg_ZQ"
kubectl config set-credentials "${YA_NAME}" \
--auth-provider=oidc \
--auth-provider-arg=idp-issuer-url="${YA_ISSURL}" \
--auth-provider-arg=client-id="${YA_CLIENT_ID}" \
--auth-provider-arg=refresh-token="${YA_REF_TOKEN}" \
--auth-provider-arg=id-token="${YA_ID_TOKEN}"
この状態では ~/.kube/config が生成されますが、あらかじめ /root/.kube/config などをコピーしていると内容が重複します。
clusterやcontextの設定はそのまま流用して、kubernetes-adminの定義を削除するなどして、全体は次のような状態になっています。
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: BS0tBS...EDtKS0tBQo=
server: https://192.168.1.65:6443
name: cluster.local
contexts:
- context:
cluster: cluster.local
user: user01@example.com
name: user01@example.com@cluster.local
current-context: user01@example.com@cluster.local
kind: Config
preferences: {}
users:
- name: user01@example.com
user:
auth-provider:
config:
client-id: example-app
id-token: eyJ....Mxqg_ZQ
idp-issuer-url: https://dex.example.com/dex
refresh-token: ChlmNHZoM2s1....mhuZndt
name: oidc
refresh-token: は実際には使われないと思うので省略しても良いかもしれません。
kubectlの実行
デバッグ情報を出力させて、API Serverの反応を確認します。
$ kubectl -v=8 get node
最後の方のログに認証後の権限チェックに失敗している様子が分かります。
...
I0703 00:29:57.111323 7807 helpers.go:201] server response object: [{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "nodes is forbidden: User \"user01@example.com\" cannot list resource \"nodes\" in API group \"\" at the cluster scope",
"reason": "Forbidden",
"details": {
"kind": "nodes"
},
"code": 403
}]
...
API ServerがOIDCに対応するように設定されていない場合は、401 Unauthorizedエラーになります。
API ServerがOIDCに対応しているものの、~/.kube/config の内容が正しくない場合には、ID Tokenが検証できないというメッセージが表示されるはずです。
...
F0703 00:35:40.958201 9261 helpers.go:119] Unable to connect to the server: ID Token is not a valid JWT
単純に権限が不足しているようであれば、RoleBinding, ClusterRoleBinding の機能を利用していきます。
必要な権限の付与 (RBAC)
まずはユーザー毎にNamespace(NS)を分割するという想定で、特定のNSで操作可能にします。
Role/RoleBindingの設定
rootになって、user01@example.com に対応するNS "user01"での操作を許可します。
$ sudo kubectl create ns user01
次に k8s公式ガイドの例を参考にNS "user01" で可能な操作を定義していきます。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: user01
name: default
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods","services"]
verbs: ["create", "delete", "get", "watch", "list"]
これをapplyすると次のようになります。
# kubectl apply -f role-default.yaml
role.rbac.authorization.k8s.io/default created
次にこのRole定義を、user01@example.com と紐付けます。
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: default
namespace: user01
subjects:
- kind: User
name: user01@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: default
apiGroup: rbac.authorization.k8s.io
これも同様にapplyで適用します。
$ sudo kubectl apply -f rolebinding-user01.yaml
rolebinding.rbac.authorization.k8s.io/default created
kubectl による pod /svc の操作
ここからは一般ユーザーに戻って、RoleBindingで許可された操作が可能になっているか確認していきます。
この状態では、pods(pod), services(svc)の操作が可能になっていますが、他の操作は許可されていないので、とりあえず deployments(deploy) などは利用できません。
とりあえず nginx を動かしてみます。
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: user01
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
対応する service を定義します。
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: user01
spec:
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
app: nginx
これらをapplyで適用すると、pod,svcの定義が作成でき、とりあえずPodIP(10.233.78.161)経由でアクセスができます。
$ kubectl apply -f 01.create-nginx.yaml
$ kubectl apply -f 02.create-svc.yaml
$ kubectl -n user01 get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.233.19.133 192.168.1.101 80:31936/TCP 4m38s
$ curl http://192.168.1.101:80/
<!DOCTYPE html>
...
しかし、pod,svc以外には許可していないため、get allのようなコマンドでは権限が不足している旨のエラーが出力されます。
$ kubectl -n user01 get all
NAME READY STATUS RESTARTS AGE
pod/nginx 1/1 Running 0 43m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.233.19.133 192.168.1.101 80:31936/TCP 5m18s
Error from server (Forbidden): replicationcontrollers is forbidden: User "user01@example.com" cannot list resource "replicationcontrollers" in API group "" in the namespace "user01"
Error from server (Forbidden): daemonsets.apps is forbidden: User "user01@example.com" cannot list resource "daemonsets" in API group "apps" in the namespace "user01"
Error from server (Forbidden): deployments.apps is forbidden: User "user01@example.com" cannot list resource "deployments" in API group "apps" in the namespace "user01"
Error from server (Forbidden): replicasets.apps is forbidden: User "user01@example.com" cannot list resource "replicasets" in API group "apps" in the namespace "user01"
Error from server (Forbidden): statefulsets.apps is forbidden: User "user01@example.com" cannot list resource "statefulsets" in API group "apps" in the namespace "user01"
Error from server (Forbidden): horizontalpodautoscalers.autoscaling is forbidden: User "user01@example.com" cannot list resource "horizontalpodautoscalers" in API group "autoscaling" in the namespace "user01"
Error from server (Forbidden): jobs.batch is forbidden: User "user01@example.com" cannot list resource "jobs" in API group "batch" in the namespace "user01"
Error from server (Forbidden): cronjobs.batch is forbidden: User "user01@example.com" cannot list resource "cronjobs" in API group "batch" in the namespace "user01"
get nsにも失敗して、あまりにも不便なので、閲覧ぐらいは許可してみます。
ClusterRoleBindingで閲覧許可(view)を付与してみる
ここから、またrootになって作業を進めていきます。
ClusterRoleは既に登録されているviewを利用します。
apiVersion: rbac.authorization.k8s.io/v1
# This cluster role binding allows anyone in the "manager" group to read secrets in any namespace.
kind: ClusterRoleBinding
metadata:
name: user:user01
subjects:
- kind: User
name: user01@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
これをrootユーザーの権限で適用します。
# kubectl apply -f clusterrolebinding4user01.yaml
この後で一般ユーザーで、先ほどエラーになった get all を実行してみます。
$ kubectl -n user01 get all
NAME READY STATUS RESTARTS AGE
pod/nginx 1/1 Running 0 56m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginx NodePort 10.233.12.39 <none> 80:31546/TCP 18m
登録されている ClusterRole の view では、node に対する処理が記述されていないため、get node はエラーになりますが、概ね期待する動作になりました。
実用上の問題
前述のRoleBinding, ClusterRoleBindingは実用上はまだ課題があります。
cluster-adminとほぼ同じだけど閲覧のみ可能な権限を付与したい
k8s公式ガイドでは、デフォルトのClusterRole(system:basic-user,system:discovery,system:public-info-viewer,cluster-admin,admin,edit,view)について説明されています。
k8sクラスター全体の状況を把握させるために、閲覧権限は付けたいけれど、勝手にdeleteやcreateをしてくれると困る、といった場合には、cluster-adminとほぼ同様の権限で、"get","list","watch"などに限定したロールを付与する方が良い場合もあるかもしれません。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dex-admin
rules:
- apiGroups:
- '*'
resources:
- '*'
verbs: ["get", "list", "watch"]
- nonResourceURLs:
- '*'
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin:user01
subjects:
- kind: User
name: user01@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: dex-admin
apiGroup: rbac.authorization.k8s.io
セキュリティ上の問題など、いろいろ考慮しなければいけませんが、教育用のk8sクラスターでは便利な設定です。
特定のNamespace(NS)で利用可能な権限を付与したい
特定のNS内の権限を決定するRoleBindingを利用する場合には、Role内で'*'のようなワイルドカード指定はできません。
それでも先ほど説明したデフォルトのClusterRole(admin,edit,view)がRoleBindingで利用できます。
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: admin
namespace: user01
subjects:
- kind: User
name: user01@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
オブジェクトによって、Clusterの管理下なのか、それ以外のNamespaceの管理下なのか違ってくるので、少し面倒ですが、公式のAPI ReferencesのCluster APIsのセクションを確認するのが確実だろうと思います。
遭遇した問題
kubesprayが/etc/kubernetes/manifests/kube-apiserver.yamlを再生成してくれない
kubesprayの設定からOIDC関連のコメントを外して upgrade-cluster.yaml を実行しましたが、kube-apiserver.yamlは再生成してくれませんでした。
このため手動で必要なオプションを加えてapiserverのPODを削除することで設定を有効にしたapiserverのPODを再起動しています。
kubesprayを使った場合には、特定の設定を削除することが難しい場面があるため、手動でシステムを変更しなければいけない場面が時々あります。
以上