Bound Service Account Tokenへの切り替えにあたり確認しておくこと

## はじめに

Kubernetes v1.21.0からは`BoundServiceAccountVolume`の機能がデフォルトで有効になり、PodにマウントされるService Account TokenがこれまでのSecretベースのService Account TokenからBound Service Account Tokenへ切り替わります。

参考: [Bound Service Account Tokenとは何か](https://qiita.com/hiyosi/items/e4fb3d9188f8d6e650b2)

このエントリでは、Bound Service Account Tokenへの切り替えが進むにあたり、Service Account Tokenの利用パターン毎にそれぞれ確認しておくべきことをまとめたいと思います。

## 利用パターンと確認事項

利用パターンとしては、大きく送信側と受信側のワークロードに分けることができます。
それに加えて、対象のWorkloadがKubernetesクラスタの中に配置されているのか、外に配置されているのかも関係してきます。

### Kubernetesクラスタ内からService Account Tokenを利用する

これはKubenretesのPodがSerivce Account Tokenを扱う場合に該当します。

この場合、ワークロードはService Account Tokenをどのように扱っているかがポイントになります。SDKを経由してService Account Tokenを扱う場合、そのSDKがBound Service Account Tokenに対応していることを確認します。例えば、[client-go](https://github.com/kubernetes/client-go)の場合、`v11.0.0+`または`v0.15.0+`となっているかを確認します。

![read-token-via-sdk.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15164/6a1b3006-8de7-b087-7762-feed044a7375.png)


SDKなどを使わずワークロードがfilesystemから直接読み込んでいるような場合、ワークロード自身でService Account Tokenのリロードが必要になります。Bound Service Account Tokenはkubeletによって自動的にローテーションされてPodに反映されるので、ワークロードは有効期限が切れる前にfilesystemから再読込するような対応が必要です。

![directly-read-token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15164/628d1b75-0296-5789-7a05-95bb7acfd3e2.png)

正しくBound Service Account Tokenへの切り替えができているかどうかは、`serviceaccount_legacy_tokens_total`や`serviceaccount_stale_tokens_total`のメトリクスで確認することができます。

### Kubernetsクラスタ外からService Account Tokenを利用する

これはCI/CD、自動化のためのワークロードなどが該当しそうです。

このパターンでは、SecretベースのService Account Tokenを外部に持ち出し、クラスタ内のKubernetes APIサーバやクラスタ内外のワークロードに対する通信でクレデンシャルとして利用していると思います。

`BoundServiceAccountVolume`自体はPodにマウントされるService Account TokenがBound Service Account Tokenになるという話であるため、このパターンに直接的な影響はありません。現状では`BoundServiceAccountVolume`が有効化されても、SecretベースのService Account Tokenが無効化されたり、新規発行が停止されるようなことはないため、そのまま使い続けることは可能です。(**セキュリティ的な面としてSecretベースのService Account Tokenは無効化が難しいなどの問題はあります**)
ただしSecretベースのService Account Tokenを発行しているToken Controllerの削除も検討されているようであり、将来的には代替手段の検討が必要になってくると思われます。

参考: 
- https://github.com/kubernetes/kubernetes/issues/77599
- https://github.com/kubernetes/kubernetes/issues/77600

現時点で別の手段を検討するのであれば、[CSR API](https://kubernetes.io/docs/reference/access-authn-authz/certificate-signing-requests/)を使ったTLSクライアント証明書への移行や、Secretにバインドしたlong-livedなBound Service Account Token(`--service-account-max-token-expiration`が設定されている場合にはその値が最大となる)、クラウド基盤のクレデンシャル(AWS IAM Keypairのような)などが候補になりそうです。

![external-workload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15164/49b9abff-8edc-e364-d139-de5eedaf55d0.png)


#### 補足
Service Account Tokenをクラスタの外で扱うことの是非については、kubernetesコミュニティ内でも明確な結論は出ていないようです。

広義にはクラスタ外のワークロードも含まれると思いますが、
https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#user-accounts-versus-service-accounts では

> User accounts are for humans. Service accounts are for processes, which run in pods.

のような記述もあり、クラスタ外のワークロードに対する位置づけは微妙なところです。

#### e.g. Secretに紐付けたBound Service Account Token

```
$ curl \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d @token-req.json  \
  https://127.0.0.1:61399/api/v1/namespaces/default/serviceaccounts/default/token
```

```json
{
  "kind": "TokenRequest",
  "apiVersion": "authentication.k8s.io/v1",
  "spec": {
     "audience": [
     "https://kubernetes.default.svc"
    ],
    "expirationSeconds": 157680000, 
    "boundObjectRef": {
      "kind": "Secret",
      "apiVersion": "v1",
      "name": "default-token-xxx"
    }
  }
}
```

これで得たTokenは紐づくSecretを削除すると無効化されます。

### Kubernetesクラスタ内でSerivce Account Tokenを受け取る

送信元の認証のためにBearer Tokenとして送られてきたService Account Tokenを検証する場合などのパターンです。

Bound Service Account Tokenの検証にはKubernetesの[TokenReview API](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#tokenreview-v1-authentication-k8s-io)を使った方法や、v1.21.0でGAに昇格した[ServiceAccountIssuerDiscovery](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-issuer-discovery)の機能を使ってAPIサーバからJWKsを取得し、署名を検証する方法があります。どちらの方法を使っても問題は無さそうですが、JWKsエンドポイントから取得する場合には、Service Account Tokenの検証フローがOIDCでいうID Tokenの検証に近いものとなるためOIDCのエコシステムの恩恵を受けやすくなったり、APIサーバの負荷軽減に繋がるなどの利点があります。

TokenReview APIまたはOIDC Configuration/JWKsエンドポイントに対してアクセス制御が設定されており、クレデンシャルとしてService Account Tokenを利用する場合には、先述の[Kubernetesクラスタ内からService Account Tokenを利用する](https://qiita.com/hiyosi/private/e24b65c11d9be7de9a97#kubernetes%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E5%86%85%E3%81%8B%E3%82%89service-account-token%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)の内容を確認してください。

![verify-token).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15164/d695c21b-1ee2-0d0b-d8f7-a81e043d9ecf.png)


### Kubernetesクラスタ外でService Account Tokenを受け取る

基本的にクラスタ内で受け取る場合と同様で、どちらの機能も利用できますがTokenReview APIまたはOIDC Configuration/JWKsエンドポイントに対してアクセス制御が設定されている場合には、[Kubernetesクラスタ外からService Account Tokenを利用する](https://qiita.com/hiyosi/private/e24b65c11d9be7de9a97#kubernets%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E5%A4%96%E3%81%8B%E3%82%89service-account-token%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)の内容を確認してください。

クラスタ外からのアクセスになるため、JWKsをキャッシュしておける分`ServiceAccountIssuerDiscovery`の機能を使ったほうが可用性は高そうです。また、`ServiceAccountIssuerDiscovery`の機能では`system:service-account-issuer-discovery`のClusterRoleに対して`system:unauthenticated`を紐付けることで認証なしにJWKsエンドポイントから検証鍵が取得できるため、外部のワークロードにクレデンシャルを渡さなくてすむようなことも可能です。ただし外部から認証なしにアクセスできてしまうことになるのでセキュリティ要件等、注意は必要です。

![external-verify-token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15164/a6450948-aa3b-0edf-5161-f4b61653d87a.png)