zaki work log

作業ログやら生活ログやらなんやら

Kubernetes v1.24でServiceAccountのトークンを生成・取得する

ここのところKubernetes関連の情報をキャッチアップできておらず、今月作ったk8sクラスタで期待する動作をせずにプチハマりしたので、トークン取得に関連した確認した内容の備忘録。

qiita.com

LegacyServiceAccountTokenNoAutoGeneration feature gateがベータとなり、デフォルトで有効化されます。有効化されるとService account tokenを含むSecret APIオブジェクトは自動生成されません。service account tokenを取得するためのTokenRequest APIを利用するか、もし期限切れを起こさないトークンが必要な場合は、このガイドにしたがってService account tokenを追加するためのtoken controllerのためのSecret APIオブジェクトを作成してください。(#108309, @zshihang)

https://qiita.com/uesyn/items/90ca3789ce88cb9ea7a4

speakerdeck.com

[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 を参照。

https://speakerdeck.com/superbrothers/du-duan-topian-jian-dexuan-nda-kubernetes-1-dot-24-falsezhu-mu-ji-neng-tojin-hou?slide=9

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リソースが生成される。

kubernetes.io

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

トークンを使って何をするかの例はこちら。

zaki-hmkc.hatenablog.com