18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

BoundServiceAccountTokenVolumeを有効にしてみる

Last updated at Posted at 2020-12-17

BoundServiceAccountTokenVolumeとは

BoundServiceAccountTokenVolumeはv1.13からalphaの機能として提供されており、FeatureGateで有効にすることができます。これにより、これまでのSecretに書き込まれたトークンをマウントする方式に変わり、宛先や有効期限が指定されたトークンがProjectedVolumeによってPodに渡されるようになります。似たような機能としてTokenRequestProjectionがありますが、BoundServiceAccountTokenVolumeTokenRequestProjectionで明示的に指定する必要のあったパラメータを動的に設定してくれます。
BoundServiceAccountTokenVolumeTokenRequestTokenRequestProjectionRootCAConfigMapの機能に依存していますが、これらはv1.20時点ではBetaまたはGAになっています。

kube-apiserver(k8s v1.20)

$ kube-apiserver
  - --feature-gates=BoundServiceAccountTokenVolume=true
  - --service-account-issuer=https://kubernetes.default.svc.cluster.local
  - --service-account-key-file=/etc/kubernetes/pki/sa.pub
  - --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
  ...

kube-controller-manager(k8s v1.20)

$ kube-controller-manager
  - --feature-gates=BoundServiceAccountTokenVolume=true
  ...

しかしトークンが期限付きに切り替わるといっても、一度にすべてを切り替える必要は無さそうです。
v1.20時点においては、BoundServiceAccountTokenVolumeを有効にしても、SecretベースのレガシーなService Account トークンは引き続き新規でも発行される状態のようですし、kube-apiserverにて--service-account-extend-token-expirationtrueに設定すると、期限付きトークンの期限を1年間に延長することができます。

さらに、k8sではserviceaccount_legacy_tokens_totalというメトリックが提供されており、これを利用してレガシーなトークン(期限や宛先が設定されていない)を利用しているクライアントが存在していないか確認することができます。また、トークンの有効期限を延長している場合も同様にserviceaccount_stale_tokens_totalによってローテーションが正しく行われていないクライアントの存在を確認することができます。
これらを活用することで、確認しながら確実に切り替えを進めていくことができそうです。

動作確認

K8s v1.20にてFeatureGateでBoundServiceAccountTokenVolumeを有効にした状態でトークンの状況を確認してみます。

$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
EOF

Podのマニフェストを確認してみます。

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2020-12-17T05:55:31Z"
  generateName: nginx-deployment-66b6c48dd5-
  labels:
    app: nginx
    pod-template-hash: 66b6c48dd5
  managedFields:
    # 省略
  name: nginx-deployment-66b6c48dd5-79vdm
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: ReplicaSet
    name: nginx-deployment-66b6c48dd5
    uid: f9e5f1ef-5b48-4469-8165-a864422af47c
  resourceVersion: "3518"
  uid: b94c6894-1b49-40de-a3f7-6feabd58ccfa
spec:
  containers:
  - image: nginx:1.14.2
    imagePullPolicy: IfNotPresent
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-jz2lz
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: bound-token-test-worker
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: kube-api-access-jz2lz
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

上記のように何も指定していなくてもK8s APIサーバの証明書を検証するためのCA証明書やServiceAccount トークンがProjected Volumeを使ってPodに渡されるように設定されています。(.spec.volumes[0].projectedあたり参照)

レガシートークンの場合は以下のようになります。

  volumes:
  - name: default-token-2dq9k
    secret:
      defaultMode: 420
      secretName: default-token-2dq9k

Podに渡されたトークンの中身を見てみると以下のような値が設定されています。k8s v1.20ではデフォルトで--service-account-extend-token-expiration=trueに設定されているため、有効期限が1年後になっており、さらにkubernetes.io.warnafterというkubernetes固有のclaimが設定されています。これはおおよそ1時間後の本来の有効期限が設定されています。この値を超えてもトークンを使っているクライアントが存在する場合には先ほど説明したserviceaccount_stale_tokens_totalがカウントされます。
Token自体の有効期限(exp)が1年後になっていても、Podにマウントされているトークンは本来の期限内(1時間)でローテーションされるようです。また、多くのクライアントライブラリでは短時間で定期的にローテーションしているように見えました。

$ export TOKEN=$(kubectl exec nginx-deployment-66b6c48dd5-79vdm -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)

$ irb
irb(main):002:0> t = ENV['TOKEN'].split('.')
irb(main):003:0> puts Base64.urlsafe_decode64(t[1])
{"aud":["https://kubernetes.default.svc.cluster.local"],"exp":1639720531,"iat":1608184531,"iss":"https://kubernetes.default.svc.cluster.local","kubernetes.io":{"namespace":"default","pod":{"name":"nginx-deployment-66b6c48dd5-79vdm","uid":"b94c6894-1b49-40de-a3f7-6feabd58ccfa"},"serviceaccount":{"name":"default","uid":"7cd6f2df-e503-4509-be21-53a2e9edd40a"},"warnafter":1608188138},"nbf":1608184531,"sub":"system:serviceaccount:default:default"}
{
	"aud": ["https://kubernetes.default.svc.cluster.local"],
	"exp": 1639720531,
	"iat": 1608184531,
	"iss": "https://kubernetes.default.svc.cluster.local",
	"kubernetes.io": {
		"namespace": "default",
		"pod": {
			"name": "nginx-deployment-66b6c48dd5-79vdm",
			"uid": "b94c6894-1b49-40de-a3f7-6feabd58ccfa"
		},
		"serviceaccount": {
			"name": "default",
			"uid": "7cd6f2df-e503-4509-be21-53a2e9edd40a"
		},
		"warnafter": 1608188138
	},
	"nbf": 1608184531,
	"sub": "system:serviceaccount:default:default"
}
Subject system:serviceaccount:default:default
Audience https://kubernetes.default.svc.cluster.local
IssuedAt 1608184531 = 2020-12-17 14:55:31
Expiration Time 1639720531 = 2021-12-17 14:55:31
kubernetes.io.warnafter 1608188138 = 2020-12-17 15:55:38

APIサーバの/metricsを参照してみると以下のようになっていました。

# TYPE serviceaccount_legacy_tokens_total counter
serviceaccount_legacy_tokens_total 0
# HELP serviceaccount_stale_tokens_total [ALPHA] Cumulative stale projected service account tokens used
# TYPE serviceaccount_stale_tokens_total counter
serviceaccount_stale_tokens_total 0
# HELP serviceaccount_valid_tokens_total [ALPHA] Cumulative valid projected service account tokens used
# TYPE serviceaccount_valid_tokens_total counter
serviceaccount_valid_tokens_total 2264

このとき、Authorizationヘッダにレガシーなトークン(Secretに書き込まれているもの)をセットしてみると、 serviceaccount_legacy_tokens_totalが増えることが確認できます。

$ curl  https://kubernetes.default.svc/metrics -H "Authorization: Bearer ${LEGACY_TOKEN}" --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt |grep "serviceaccount_"

# TYPE serviceaccount_legacy_tokens_total counter
serviceaccount_legacy_tokens_total 1
# HELP serviceaccount_stale_tokens_total [ALPHA] Cumulative stale projected service account tokens used
# TYPE serviceaccount_stale_tokens_total counter
serviceaccount_stale_tokens_total 0
# HELP serviceaccount_valid_tokens_total [ALPHA] Cumulative valid projected service account tokens used
# TYPE serviceaccount_valid_tokens_total counter
serviceaccount_valid_tokens_total 2309

ちなみにservice-account-extend-token-expiration=falseの状態では以下のようにトークンの有効期限は1時間に設定されます。

irb(main):002:0> t = ENV['TOKEN'].split('.')
=> ["eyJhbGciOiJSUzI1NiIsImtpZCI6Ik54Tm55TjlWeXhqWXJzRi04c05KNjktMDJRWDdaaHptOHRHX2NnUW9LeVEifQ", "eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjA4MTkyNzE5LCJpYXQiOjE2MDgxODkxMTIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJuZ2lueC1kZXBsb3ltZW50LTY2YjZjNDhkZDUtanFuOWciLCJ1aWQiOiJmMDcyNWI1Ni1iNDg5LTQ1NTItOGYxZi00NjI2OTJmOGFlYWYifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJlZGRiNTNmZi1iZGVkLTRiOWUtOGUzZS1jYWIzNTYyMWQ3NjYifX0sIm5iZiI6MTYwODE4OTExMiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9", "PD0zRGev0XThGfd2kQqinxHpTKareo0BU5qXBQztK4J2J-zXyXqU3hzEnCpeVeTpVtAzFrisYnbBnF1cm29OwJpefvjbb1Y3q1zPboMz-5G3RYnLvkd8hi6SPtNQ5B8QRAhXGNrKlb6fChUnJtsfdt_9bOjtPDdJKi5UP6Yf2ho23bqvHd95epvGQKb8iRaGSGPyyeboCSFpzRaKEJBuJ_BGyH_-VsbILAJjs5q8QY-aZQelGX4YZAFcUSa7COerDBWfSDiiv1vs9SuwYmviJzgpGoEBWRZFf2OWCOAKc5GIaOKwK7I26np4SyN8Zpa71GNowvsQRTXvfPYx_hkUrg"]
irb(main):003:0> puts Base64.urlsafe_decode64(t[1])
{"aud":["https://kubernetes.default.svc.cluster.local"],"exp":1608192719,"iat":1608189112,"iss":"https://kubernetes.default.svc.cluster.local","kubernetes.io":{"namespace":"default","pod":{"name":"nginx-deployment-66b6c48dd5-jqn9g","uid":"f0725b56-b489-4552-8f1f-462692f8aeaf"},"serviceaccount":{"name":"default","uid":"eddb53ff-bded-4b9e-8e3e-cab35621d766"}},"nbf":1608189112,"sub":"system:serviceaccount:default:default"}
=> nil
Subject system:serviceaccount:default:default
Audience https://kubernetes.default.svc.cluster.local
IssuedAt 1608189112 = 2020-12-17 16:11:52
Expiration Time 1608192719 = 2020-12-17 17:11:59
kubernetes.io.warnafter N/A

まとめ

BoundServiceAccountTokenVolumeは現在ベータリリースに向けた準備が行われており、v1.21でベータ、v1.22でGAのスケジュールで進められているようです。
https://github.com/kubernetes/enhancements/blob/master/keps/sig-auth/1205-bound-service-account-tokens/README.md#graduation-criteria

トークンの切り替えには各種ライブラリの対応を含めて、多くのコードで変更が必要になることが予想されるため、早い段階からやるべきことを認識して備えておくのが良さそうです。

18
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?