ここのところKubernetes関連の情報をキャッチアップできておらず、今月作ったk8sクラスタで期待する動作をせずにプチハマりしたので、トークン取得に関連した確認した内容の備忘録。
LegacyServiceAccountTokenNoAutoGeneration
feature gateがベータとなり、デフォルトで有効化されます。有効化されるとService account tokenを含むSecret APIオブジェクトは自動生成されません。service account tokenを取得するためのTokenRequest APIを利用するか、もし期限切れを起こさないトークンが必要な場合は、このガイドにしたがってService account tokenを追加するためのtoken controllerのためのSecret APIオブジェクトを作成してください。(#108309, @zshihang)
[KEP 2799][beta] Secret-based ServiceAccount トークンの⾃動⽣成の停⽌
Feature gate: LegacyServiceAccountTokenNoAutoGeneration, デフォルト有効
- これまで ServiceAccount を作成すると⾃動的に⽣成されていたトークンを含む Secret オブジェクトが作成されなくなる
- 有効期限のないトークンが必要な場合は Token Request API を使⽤してBound ServiceAccount トークンを⽣成するか、いくつかのステップを踏んで Secret-based トークンを⽣成できる
- Bound ServiceAccount Token を⽣成するのに直接 Token Request API を使うのは⾯倒なので 1.24 で kubectl にトークンを⽣成を作成するコマンドが追加された
- Bound ServiceAccount Token の詳細はBound Service Account Tokenとは何か - Qiita を参照。
k8s v1.24ではトークンのSecretが自動で生成されなくなった
上で引用した通りで、デフォルトの動作が変化しました。
v1.24における実際の動作は以下の通り。
[zaki@cloud-dev2 kind]$ kubectl get node NAME STATUS ROLES AGE VERSION kind-control-plane Ready control-plane 4m7s v1.24.0
Namespaceの作成。
[zaki@cloud-dev2 kind]$ kubectl create ns zzz namespace/zzz created [zaki@cloud-dev2 kind]$ kubectl get secret -n zzz No resources found in zzz namespace.
ServiceAccountの作成。
[zaki@cloud-dev2 kind]$ kubectl create sa sauser -n zzz serviceaccount/sauser created [zaki@cloud-dev2 kind]$ kubectl get secret -n zzz No resources found in zzz namespace. [zaki@cloud-dev2 kind]$ kubectl get sa -n zzz NAME SECRETS AGE default 0 30s sauser 0 6s
このように、NamespaceやServiceAccountを作成しても、トークンのSecretリソースが生成されない動作になっている。
ServiceAccountのトークンを取得する
kubectl create token コマンド使用
トークンを生成するためのTokenRequest APIを扱うコマンドがあるので、それを実行すれば対象ServiceAccount用のトークンを生成できる。
まずは検証用のServiceAccountとRoleBindingを生成。
使用したマニフェストファイルはこちら。
[zaki@cloud-dev2 tmp]$ kubectl apply -f rbac-sample.yml namespace/zzz created serviceaccount/sauser created role.rbac.authorization.k8s.io/sample-role created rolebinding.rbac.authorization.k8s.io/sample-rolebinding created [zaki@cloud-dev2 tmp]$ kubectl get secret -n zzz No resources found in zzz namespace.
従来バージョンであればこの時点でトークンのSecretが生成されるけど、v1.24では生成されないので、次のコマンドを実行してトークンを取得する。
kubectl create token <ServiceAccountのユーザー名>
実行結果が以下。
[zaki@cloud-dev2 tmp]$ kubectl create token sauser -n zzz eyJhbGciOiJSUzI1NiIsImtpZCI6IlpEY3dZVW82dURkRU9QckFEQTdEV2RwYTVOZ1NXSUFBTzByYllHM3ZLcDgifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjU4NzYyMjUwLCJpYXQiOjE2NTg3NTg2NTAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ6enoiLCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoic2F1c2VyIiwidWlkIjoiNmU5NmNhZmYtNGYxZS00NjY1LWI1YTYtZTg2ODllMjFlZmY3In19LCJuYmYiOjE2NTg3NTg2NTAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDp6eno6c2F1c2VyIn0.tJzQKH2ll_TsN6iFWAP4vL-HZ7nCwUFa1qqeFunceH6i-FQHPcFQOnNlL5wdMoyMetavdiHcJWNsA21wQfDaRIajUBTiA-GnO7LnNcfstJ6Z6acnLzLLz00oS4MvFxPw7-VDEUJvBkLjNrEXnMMzAtL3i1Q1tRsYxw4btgtla0VkS-AMuZAPwzGbUd3Hf8eJl4ZLx7a2bGA-dFZ0gkNMUr2fj3p4NxyqWFkWQcPzWFPCQPRQyx3Q7wLtFu7sjQryNHx1guXosr-BwMWPwxW2PgbaEXZtiiwGRc9C3vUOoxpOcFdmrhwYjH424PEyLXVKtqZqseApX5C6iCtvxCp2fw
取れてますね。実行のたびにトークンは再作成される。
このトークンを使ってRESTを叩くサンプルは以下。
[zaki@cloud-dev2 tmp]$ TOKEN=$(kubectl create token sauser -n zzz) [zaki@cloud-dev2 tmp]$ curl -k -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1:38253/api/v1/namespaces/zzz/pods { "kind": "PodList", "apiVersion": "v1", "metadata": { "resourceVersion": "7087" }, "items": [] }
Podが何も無くて分かりづらいので、サンプルをデプロイして確認。
[zaki@cloud-dev2 tmp]$ kubectl apply -f https://raw.githubusercontent.com/zaki-lknr/k8s-samples/master/sample-web/httpd-nodeport/sample-http.yaml -n zzz deployment.apps/sample-http created service/sample-http created
[zaki@cloud-dev2 tmp]$ kubectl get pod -n zzz NAME READY STATUS RESTARTS AGE sample-http-776d7585c9-4l4xt 1/1 Running 0 46s sample-http-776d7585c9-kmb96 1/1 Running 0 46s
こんな感じ。
[zaki@cloud-dev2 tmp]$ curl -k -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1:38253/api/v1/namespaces/zzz/pods { "kind": "PodList", "apiVersion": "v1", "metadata": { "resourceVersion": "7254" }, "items": [ { "metadata": { "name": "sample-http-776d7585c9-4l4xt", "generateName": "sample-http-776d7585c9-", "namespace": "zzz", "uid": "ed3004ef-d897-433c-9d62-9b2916e16626", "resourceVersion": "7241", "creationTimestamp": "2022-07-25T14:23:00Z", "labels": { "app": "sample-http", "pod-template-hash": "776d7585c9" }, ...
この通り、Podの情報を取得できている。
トークンの期限
kubectl create token
コマンドを使うと(従来のServiceAccountのSecretトークンと異なり)期限が存在し、期限が切れると無効になる。
ドキュメントを見ても期限のデフォルトがよくわからず。。。
helpを見る限りでは、期限は--duration
オプションで制御できそう。
[zaki@cloud-dev2 ~]$ kubectl create token --help : --duration=0s: Requested lifetime of the issued token. The server may return a token with a longer or shorter lifetime. :
0s
(0秒)を指定すると無期限になりそうに見えるけど、実は(少なくとも手元の環境では)そんな事はなさそうで、コマンドの実行時のRESTのやり取りを-v
オプションでログレベルを上げて確認すると以下の通り。
[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz --duration=0s -v=8 I0726 22:25:32.205677 433932 loader.go:372] Config loaded from file: /home/zaki/.kube/config I0726 22:25:32.206785 433932 request.go:1073] Request Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"audiences":null,"expirationSeconds":null,"boundObjectRef":null},"status":{"token":"","expirationTimestamp":null}} : : I0726 22:25:32.218442 433932 request.go:1073] Response Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"sauser","namespace":"zzz","creationTimestamp":"2022-07-26T13:25:32Z","managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"authentication.k8s.io/v1","time":"2022-07-26T13:25:32Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:expirationSeconds":{}}},"subresource":"token"}]},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":3600,"boundObjectRef":null},"status":{"token":"eyJhbGciOi ...
この通り、0s
指定時は"expirationSeconds":3600
というレスポンスになっており、1時間の期限になってるように見える。
ちなみにこれは、--duration
未指定時の場合と動作が同じ。
[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz -v=8 I0726 22:27:22.681313 434460 loader.go:372] Config loaded from file: /home/zaki/.kube/config I0726 22:27:22.682016 434460 request.go:1073] Request Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"audiences":null,"expirationSeconds":null,"boundObjectRef":null},"status":{"token":"","expirationTimestamp":null}} : : I0726 22:27:22.692562 434460 request.go:1073] Response Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"sauser","namespace":"zzz","creationTimestamp":"2022-07-26T13:27:22Z","managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"authentication.k8s.io/v1","time":"2022-07-26T13:27:22Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:expirationSeconds":{}}},"subresource":"token"}]},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":3600,"boundObjectRef":null},"status":{"token":"eyJhbGciOi ...
この場合も、レスポンスは"expirationSeconds":3600
となっている。
もしかするとリクエストに0s
が渡っておらずnull
になっているので、コマンド側に不備がある可能性もあるけど、現状0s
指定は効力がなさそう。
API仕様側を確認する限りでは、expirationSeconds
に指定する期限(秒)の値はint64とある。
expirationSeconds (int64)
ExpirationSeconds is the requested duration of validity of the request. The token issuer may return a token with a different validity duration so a client needs to check the 'expiration' field in a response.
なんだけど、実際に動かすと以下の通りunsigned int32っぽい…
[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz --duration=4294967297s -v=8 : : error: failed to create token: TokenRequest.authentication.k8s.io "" is invalid: spec.expirationSeconds: Invalid value: 4294967297: may not specify a duration larger than 2^32 seconds
少なくとも手元の環境でMAXは4294967296秒(ざっくり136年)
[zaki@cloud-dev2 ~]$ kubectl create token sauser -n zzz --duration=4294967296s -v=8 : : I0726 22:37:35.590377 438330 request.go:1073] Response Body: {"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"sauser","namespace":"zzz","creationTimestamp":"2022-07-26T13:37:35Z","managedFields":[{"manager":"kubectl","operation":"Update","apiVersion":"authentication.k8s.io/v1","time":"2022-07-26T13:37:35Z","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:expirationSeconds":{}}},"subresource":"token"}]},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":4294967296,"boundObjectRef":null},"status":{"token":"eyJhbGciOiJSU ...
期限があるとはいえ、この値であれば期限が切れるのは自分が死んだ後なので知ったこっちゃねーや実質無期限として使用できそう。
Secretベースのトークンを作成する
kubectl create token
コマンドの実行でなく、従来と同じようにトークンを保持するSecretリソースを作成することもできる。
ServiceAccount名を指定したアノテーションを含むSecretリソースを明示的に作成することで、トークンが含まれる内容のSecretリソースが生成される。
ServiceAccount名がsauser
の場合、具体的なマニフェストは以下の通り。
--- apiVersion: v1 kind: Secret metadata: name: sa-token-sample namespace: zzz annotations: kubernetes.io/service-account.name: "sauser" type: kubernetes.io/service-account-token
以下のアノテーションで、sauser
というServiceAccount名を指定することで、sa-token-sample
という名前でSecretリソースが生成される。
annotations: kubernetes.io/service-account.name: "sauser" # <- ここ
[zaki@cloud-dev2 ~]$ kubectl apply -f tmp/rbac-sample.yml secret/sa-token-sample created [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz NAME TYPE DATA AGE sa-token-sample kubernetes.io/service-account-token 3 2s
内容は以下の通り。
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o yaml apiVersion: v1 data: ca.crt: LS0tLS1CRUd ... namespace: enp6 token: ZXlKaGJHY2lPaU ... kind: Secret metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{"kubernetes.io/service-account.name":"sauser"},"name":"sa-token-sample","namespace":"zzz"},"type":"kubernetes.io/service-account-token"} kubernetes.io/service-account.name: sauser kubernetes.io/service-account.uid: e373e307-33c8-4af9-b9b7-9cb09b681126 creationTimestamp: "2022-07-26T14:16:26Z" name: sa-token-sample namespace: zzz resourceVersion: "119765" uid: 678149ac-1a46-41b2-badd-3a68d7921d18 type: kubernetes.io/service-account-token
このトークンであれば従来と同じく期限がない(とドキュメントには書かれてる)ので、永続的に使用可能。
v1.24のSecretトークンが従来と動作が違う点
以下は手元の環境で試したらこうだった、という話。
仕様なのか環境固有なのかまでは確認してない。
削除時に自動再生成されない
旧バージョンの自動で生成されるSecretのトークンは、Secretリソースを削除すると自動で再生成される。
ubuntu@oci-g-a1-ubuntu:~$ kubectl get node NAME STATUS ROLES AGE VERSION oci-g-a1-ubuntu Ready control-plane,master 129d v1.22.7+k3s1 ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample NAME TYPE DATA AGE default-token-twg8z kubernetes.io/service-account-token 3 24s sample-account-token-dkubectl89 kubernetes.io/service-account-token 3 24s ubuntu@oci-g-a1-ubuntu:~$ kubectl delete secret -n rbac-sample sample-account-token-dkubectl89 # ここでトークンのSecretを削除 secret "sample-account-token-dkubectl89" deleted ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample NAME TYPE DATA AGE default-token-twg8z kubernetes.io/service-account-token 3 30s sample-account-token-fkt6g kubernetes.io/service-account-token 3 2s # 新しいトークンのSecretが生成されている
これに対して、v1.24でアノテーションを使って生成したトークンのSecretリソースは、削除しても自動で再生成はされない。
[zaki@cloud-dev2 ~]$ kubectl delete secret -n zzz sa-token-sample secret "sa-token-sample" deleted [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz No resources found in zzz namespace. # 削除すると消えたまま
Secretリソース再生成時にトークンがリフレッシュされない
上記の再生成時の動作として、従来バージョンの場合はトークンの内容がリフレッシュされる。 (むしろ、トークン漏洩などで無効化したい場合に、Secretリソースを削除して入れ替える、みたいな使い方…だったはず)
ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample sample-account-token-fkt6g -o jsonpath='{.data.token}' | sha256sum e980d99fb8ed5b95dd58bbd4913825ef6f8e44e93f5392f3b334f68bbbed455f - # 削除前のトークンのハッシュ値 (比較確認用) ubuntu@oci-g-a1-ubuntu:~$ kubectl delete secret -n rbac-sample sample-account-token-fkt6g secret "sample-account-token-fkt6g" deleted ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample NAME TYPE DATA AGE default-token-twg8z kubernetes.io/service-account-token 3 4m13s sample-account-token-dql7l kubernetes.io/service-account-token 3 7s ubuntu@oci-g-a1-ubuntu:~$ kubectl get secret -n rbac-sample sample-account-token-dql7l -o jsonpath='{.data.token}' | sha256sum 33b60565e646431fef1dbf1d46f95f86d2278bc9a21ea3fd363472d6901a47db - # 削除前のトークンのハッシュ値と異なっている
これに対してv1.24の場合、トークンのSecretリソースの削除前と削除後の再生成で、トークンの内容は変化しなかった。
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o jsonpath='{.data.token}' | sha256sum 5d4e5cf27eb93e7843c49c1df1ac6e2dc08a810b3efa0980006792f01010f04b - # 削除前のトークンのハッシュ値と異なっている [zaki@cloud-dev2 ~]$ kubectl delete secret -n zzz sa-token-sample secret "sa-token-sample" deleted [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz No resources found in zzz namespace. [zaki@cloud-dev2 ~]$ [zaki@cloud-dev2 ~]$ [zaki@cloud-dev2 ~]$ kubectl apply -f tmp/rbac-sample.yml secret/sa-token-sample created [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o jsonpath='{.data.token}' | sha256sum 5d4e5cf27eb93e7843c49c1df1ac6e2dc08a810b3efa0980006792f01010f04b - # 削除前のトークンのハッシュ値と同一 [zaki@cloud-dev2 ~]$
この通り、トークンの内容(のハッシュ値)が変化せず、削除前の値を保持している。 トークンをリフレッシュしたい場合は、ServiceAccountから再生成すると変化し、削除したトークンは無効になる。
まずはServiceAccountを削除(ServiceAccountを削除すると、紐づいてるトークンのSecretも自動で削除される)
[zaki@cloud-dev2 ~]$ kubectl get sa -n zzz NAME SECRETS AGE default 0 9h sauser 0 9h [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz NAME TYPE DATA AGE sa-token-sample kubernetes.io/service-account-token 4 2m33s [zaki@cloud-dev2 ~]$ kubectl delete sa -n zzz sauser serviceaccount "sauser" deleted [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz No resources found in zzz namespace. # Secretは明示的に削除してないが、ServiceAccount削除に連動して消える
ServiceAccountとSecretを再作成
[zaki@cloud-dev2 ~]$ kubectl apply -f tmp/rbac-sample.yml serviceaccount/sauser created secret/sa-token-sample created [zaki@cloud-dev2 ~]$ kubectl get secret -n zzz NAME TYPE DATA AGE sa-token-sample kubernetes.io/service-account-token 3 4s
これでトークンの内容が変化する。
[zaki@cloud-dev2 ~]$ kubectl get secret -n zzz sa-token-sample -o jsonpath='{.data.token}' | sha256sum ed4c28d94720340f07ba7c041c221fce780a7a047a2148640062ca9b44bfa551 - # 削除前のトークンのハッシュ値(5d4e5cf2...)と異なっている
環境
確認した環境は以下の通り。
v1.24 (kindで構築)
[zaki@cloud-dev2 ~]$ kind --version kind version 0.14.0 [zaki@cloud-dev2 ~]$ cat /etc/redhat-release Fedora release 35 (Thirty Five) [zaki@cloud-dev2 ~]$ kubectl version --short Flag --short has been deprecated, and will be removed in the future. The --short output will become the default. Client Version: v1.24.3 Kustomize Version: v4.5.4 Server Version: v1.24.0
v1.22 (k3sで構築)
ubuntu@oci-g-a1-ubuntu:~$ k3s --version k3s version v1.22.7+k3s1 (8432d7f2) go version go1.16.10 ubuntu@oci-g-a1-ubuntu:~$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=20.04 DISTRIB_CODENAME=focal DISTRIB_DESCRIPTION="Ubuntu 20.04.4 LTS" ubuntu@oci-g-a1-ubuntu:~$ kubectl version --short Client Version: v1.22.7+k3s1 Server Version: v1.22.7+k3s1
トークンを使って何をするかの例はこちら。