TokenRequestProjectionについて調べてみた

PR: [kubernetes/community#1973](https://github.com/kubernetes/community/pull/1973)

今回はKubernetes v1.11でalpha版の機能として追加されたTokenRequestProjectionについて簡単に調べてみました。これはServiceAccountのトークンを動的に発行して、それをProjected Volumeを使ってPodにマウントすることができる機能のようです。

Projected VolumeとはSecrets,  ConfigMaps, Downward APIといった複数のVolumeソースを一つのVolumeにまとめてマウントすることができる機能であり、TokenRequestProjectionを有効にするとそのうちの一つとしてServiceAccountのトークンをPodのVolumeにマウントすることができるようになります。

k8s v1.11ではalpha版の機能のため有効にするにはkube-apiserverやkubeletに以下のフラグをセットします。

```
--feature-gates=TokenRequest=true,TokenRequestProjection=true
```

ここでTokenRequestも一緒に有効にしているのはTokenRequestProjectionでは新規トークンがリクエストされるとTokenRequest APIを呼び出してトークンを発行する仕組みになっているためです。既に発行済みの場合は自身(kubelet)のキャッシュから取得します。

TokenRequestが有効になるとserviceaccountsのAPIに/tokenエンドポイントが追加されます。
(e.g.,)

```
/api/v1/namespaces/default/serviceaccounts/default/token
````

TokenRequestは上記エンドポイントに対してTokenRequestのspecをPOSTするとトークンが発行されるような仕組みになっています。
(e.g.,)

```
POST /api/v1/namespaces/default/serviceaccounts/default/token
{
  "kind": "TokenRequest",
  "apiVersion": "authentication.k8s.io/v1",
  "spec": {
     "audience": [
     "https://kubernetes.default.svc"
    ],
    "validityDuration": "99999h",
    "boundObjectRef": {
      "kind": "Pod",
      "apiVersion": "v1",
      "name": "pod-foo-346acf"
    }
  },
  "status": {
    "token":
    "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJz[payload omitted].EkN-[signature omitted]",
    "expiration": "Jan 24 16:36:00 PST 3018"
  }
```

※ https://github.com/kubernetes/enhancements/blob/master/keps/sig-auth/1205-bound-service-account-tokens/README.md#example-flow 


TokenRequestを有効にするにはkube-apiserverに必要なフラグをセットする必要があります。

```
--service-account-key-file=/paht/to/cert.pem
--service-account-signing-key-file=/path/to/key.pem
--service-account-issuer=api
--service-account-api-audiences=api
```

話を戻しまして、TokenRequestProjectionを使ってServiceAccountトークンをPodのVolumeにマウントするには以下のようなマニフェストを作成します。今回はサンプルなので簡単にPodのマニフェストを用意します。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: sa-projection-test
spec:
  containers:
  - name: container-test
    image: busybox
    command:
    - sleep
    - "3600"
    volumeMounts:
    - name: tokenvol
      mountPath: "/var/run/secrets/tokens"
      readOnly: true
  volumes:
  - name: tokenvol
    projected:
      sources:
      - serviceAccountToken:
          audience: api
          expirationSeconds: 600
          path: token
```

上記のspecではPodが起動するとServiceAccountトークンは `/var/run/secrets/tokens/token` にマウントされます。TokenRequest APIで作成したトークンはPod情報を含んでいるのでトークンを受け取った側はどのPodからきたリクエストなのかを知ることができます。そのうえaudienceパラメータの値を指定することで発行するトークンのaud claimの値がセットされるため、リクエストを受け取る側はリクエスト元のサービスが意図したものかをチェックすることができます。expirationSecondsを指定することでトークンの有効期限を設定できるようになっていますのでトークン漏えい時のリスクも低減できます。

さらに素晴らしいことにトークンの有効期限が切れると自動的にリフレッシュしてくれるようになっており、Podが消えるとこのServiceAccountトークンは無効化されます。

これらをTokenRequestのみで実現しようとすると、何らかの仕組みによりTokenRequest APIを使ってServiceAccountトークンを発行した後でそのSecretをProjectedVolumeでマウントするしかできませんでしたが、TokenRequestProjectionによってより便利に動的にServiceAccountトークンをマウントすることができるようになりました。

実際に確認していきます。

```
❯ kubectl exec -it sa-projection-test /bin/sh
/ # cat /var/run/secrets/tokens/token
...
```

ServiceAccountトークンはJWT形式なのでpayload部をBase64デコードしてみると、以下のように意図した通りの内容になっていることが確認できます。

```json
{
  "aud":["api"],  #audienceで指定した値
  "exp":1533963641, #expirationSecondsで600を指定したのでiat(IssueAtから+600された値)
  "iat":1533963041,
  "iss":"api", #kube-apiserverの--service-account-issuerで設定した値
  "kubernetes.io":{
    "namespace": "default",
    "pod":{
      "name":"sa-projection-test",
      "uid":"1234abcd-1234-1234-12345abcdefgh"
    },
    "serviceaccount":{
      "name":"default",
      "uid":"9876abcd-3456-3456-abcd-9876abcdefgh"
    }
  },
  "nbf":1533963041,
  "sub":"system:serviceaccount:default:default" #今回はPodのspecにServiceAccountNameを指定していないのでデフォルト
}
```

今後はコンテナの識別子を追加することでコンテナ単位にバインドできるServiceAccountトークンを発行するような機能についての議論も進んでいます。(SIG-Auth)

最後に現時点ではまだまだalpha版の機能なので仕様変更が入る可能性もありますが、どんな機能なのかイメージが掴めるのではないかと思います。誰がどのようなトークンを発行できるかなどの制限はできないようですが、個人的にその部分は今後に期待したいと思います。この機能が早く安定してよりセキュアなk8sライフを送れるようになるといいなと思います。