[Kubernetes] Vaultに格納された機密情報を永続ストレージとしてマウント

# 概要
　Kubernetes環境において、CI/CDによる自動化を行おうと試みた時など、パスワードやアクセストークンなどの機密情報をどうやって保存しようかを悩んだことがある人は多いのではないでしょうか。パスワードを書いたManifestをGitHubなどにPushするのは、抵抗があるかと思います。今回はHashiCorp社が提供している機密情報を管理するためのソフトウェア[Vault](https://www.hashicorp.com/products/vault/)に格納された機密情報をKuberntes上のストレージとしてマウントする[secrets-store-csi-driver](https://github.com/deislabs/secrets-store-csi-driver)の動作検証を行います。
　なお、secrets-store-csi-driverはKubeCon 2019 EUにてHashiCorp社とMS社のメンバによって発表されています。その時の資料は[こちら](https://kccnceu19.sched.com/event/MPdZ/secrets-store-csi-driver-bring-your-own-enterprise-secrets-store-to-k8s-rita-zhang-microsoft-anubhav-mishra-hashicorp)です。

# 検証環境

- Kubernetes v1.14.1
- secrets-store-csi-driver v0.0.3

# 事前準備

事前準備として、動作検証で利用するVaultのサーバを立ち上げます。

### ダウンロード

GitHubのリポジトリから[secrets-store-csi-driver](https://github.com/deislabs/secrets-store-csi-driver)をダウンロードします。

```
$ git clone git@github.com:deislabs/secrets-store-csi-driver.git
$ cd secrets-store-csi-driver
```

Vaultコマンドも[ダウンロードサイト](https://www.vaultproject.io/downloads.html)からダウンロードし、パスの通ったディレクトリにコピーします。

## 検証用Vaultのデプロイ

次に、検証用に利用するVaultをKubernetes上にデプロイします。
このVaultは、データを永続していないため、Podが削除されるとともに格納したデータは全て削除されます。あくまで検証用として利用してください。

```
$ kubectl apply -f pkg/providers/vault/examples/vault.yaml
```

デプロイすると、以下のようにVaultのPodが起動します。

```
$ kubectl get pod -l app=vault  
NAME                    READY   STATUS    RESTARTS   AGE
vault-f585568d9-9sbp2   1/1     Running   1          3m17s

$ kubectl get service -l app=vault
NAME    TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
vault   NodePort   10.101.58.66   <none>        8200:31899/TCP   67m
```

VaultをKubernetesクラスタの外部からアクセスできるようにport forwardを行います。

```
$ kubectl port-forward vault-f585568d9-9sbp2 8200:8200 &
```

実際に、Vaultにアクセスし、起動していることを確認を行います。

```
$ export VAULT_ADDR="http://127.0.0.1:8200"
$ export VAULT_TOKEN="root"
i$ vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.1.1
Cluster Name    vault-cluster-692252b2
Cluster ID      60c527f1-2e28-70c5-88fc-55afcc01ec43
HA Enabled      false
```

これで検証に利用するVault(サーバ)のセットアップが終わりました。

# 動作検証

VaultにKubernetesからのアクセスできるように認証設定したのち、secrets-store-csi-driverをデプロイしていきます。なお、今回の検証では、Kubernetesの`default`ネームスペースを利用します。

## Kubernetesでのサービスアカウントの作成

まず、Kubernetes上にVaultへのアクセス用のサービスアカウントを作成します。

```
$ kubectl create serviceaccount vault-auth
$ kubectl apply -f - <<EOH
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: role-tokenreview-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
- kind: ServiceAccount
  name: vault-auth
  namespace: default
EOH
```

## VaultにKubernetesの認証情報を設定

次に、Vaultに、Kubernetesの認証情報を設定します。
まず初めに、必要な証明書やトークンなどの情報をKubernetesの各種リソースから取得します。

```
$ CLUSTER_NAME="$(kubectl config view -o json | jq -r .clusters[].name)"

$ SECRET_NAME="$(kubectl get serviceaccount vault-auth \
  -o go-template='{{ (index .secrets 0).name }}')"

$ TR_ACCOUNT_TOKEN="$(kubectl get secret ${SECRET_NAME} \
  -o go-template='{{ .data.token }}' | base64 --decode)"

$ K8S_HOST="$(kubectl config view --raw \
  -o go-template="{{ range .clusters }}{{ if eq .name \"${CLUSTER_NAME}\" }}{{ index .cluster \"server\" }}{{ end }}{{ end }}")"

$ K8S_CACERT="$(kubectl config view --raw \
  -o go-template="{{ range .clusters }}{{ if eq .name \"${CLUSTER_NAME}\" }}{{ index .cluster \"certificate-authority-data\" }}{{ end }}{{ end }}" | base64 --decode)"

```

なお、幾つかの値は、KuberntesのConfig情報から取得しています。
Config情報に複数のKubernetesクラスタが登録している場合は、正しいKuberntesクラスタの情報が取得されているかを確認してください。
次に、Vaultの認証で`kubernetes`を有効にします。

```
$ vault auth enable kubernetes
```

続いて、`kubernetes`の認証情報として、先に取得した証明書やトークンの情報を設定します。

```
$ vault write auth/kubernetes/config \
  kubernetes_host="${K8S_HOST}" \
  kubernetes_ca_cert="${K8S_CACERT}" \
  token_reviewer_jwt="${TR_ACCOUNT_TOKEN}"
```

続いて、今回の動作検証で利用する秘密情報のアクセスポリシーをVaultに設定します。

```
$ echo 'path "secret/data/foo" {
  capabilities = ["read", "list"]
}

path "sys/renew/*" {
  capabilities = ["update"]
}' | vault policy write example-readonly -
```

```
$ vault write auth/kubernetes/role/example-role \
  bound_service_account_names=csi-driver-registrar \
  bound_service_account_namespaces=default \
  policies=default,example-readonly \
  ttl=20m
```

これでVaultの認証情報の設定は終わりです。
次に、動作検証で利用するサンプルの機密情報を`foo`に格納します。

```
$ vault kv put secret/foo bar=hello
```

## secrets-store-csi-driverのデプロイ

secrets-store-csi-driverをデプロイします。

```
$ kubectl apply -f deploy/crd-csi-driver-registry.yaml
$ kubectl apply -f deploy/rbac-csi-driver-registrar.yaml
$ kubectl apply -f deploy/rbac-csi-attacher.yaml
$ kubectl apply -f deploy/csi-secrets-store-attacher.yaml
$ kubectl apply -f pkg/providers/vault/examples/secrets-store-csi-driver.yaml
```

`default`ネームスペース以外のNamespaceにデプロイする場合は，Manifestを編集する必要があります。
secrets-store-csi-driverがデプロイされたことを確認します。

```
$ kubectl get sts,ds
NAME                                          READY   AGE
statefulset.apps/csi-secrets-store-attacher   1/1     6m42s

NAME                                     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR              AGE
daemonset.extensions/csi-secrets-store   2         2         2       2            2           <none>          112s
```


## Vaultに格納された機密情報をPV,PVCでマウント

Vaultに格納された機密情報をマウントするためのPVのManifest(`pv-vault-csi.yaml`)を作成します。

```pv-vault-csi.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-vault
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  csi:
    driver: secrets-store.csi.k8s.com
    readOnly: true
    volumeHandle: kv
    volumeAttributes:
      providerName: "vault"
      roleName: "example-role"
      vaultAddress: "http://10.101.58.66:8200"  # VaultのURL
      vaultSkipTLSVerify: "true"
      objects:  |
        array:
          - |
            objectPath: "/foo"  # Vaultのセットアップにて格納したSecretのパス名
            objectName: "bar"   # 上記Secretに格納したKey名
            objectVersion: ""
```

続いて、PV(`pv-vault`)と対になるPVCのManifest(`pvc-vault-csi-static.yaml`)を作成します。

```pvc-vault-csi-static.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pvc-vault
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 1Gi
  volumeName: pv-vault
  storageClassName: ""
```

定義したPVとPVCをデプロイします。

```
$ kubectl apply -f pv-vault-csi.yaml
$ kubectl apply -f pvc-vault-csi-static.yaml
```

PVCとPVが`Bound`されたのを確認します。

```
$ kubectl get pvc,pv
NAME                              STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pvc-vault   Bound    pv-vault   1Gi        ROX                           84s

NAME                              CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                      STORAGECLASS    REASON   AGE
persistentvolume/pv-vault         1Gi        ROX            Retain           Bound    default/pvc-vault                                 2m34s
```

次に、デプロイしたPVC,PVをマウントするPodのManifestを作成します。

```nginx-pod-vault.yaml
kind: Pod
apiVersion: v1
metadata:
  name: nginx-vault
spec:
  containers:
  - image: nginx
    name: nginx-vault
    envFrom:
    - configMapRef:
        name: vault-cm
    volumeMounts:
    - name: vault01
      mountPath: "/mnt/vault" # Vault mount point.
      readOnly: true
  volumes:
  - name: vault01
    persistentVolumeClaim:
      claimName: pvc-vault
```

Podをデプロイします。

```
$ kubectl apply -f nginx-pod-vault.yaml
```

続いて、PodがVaultに格納された機密情報をマウントできているかを確認します。
Podにアタッチしたのち、マウントしたディレクトリ`/mnt/vault`を確認します。

```
$ kubectl exec -ti nginx-vault /bin/bash
root@nginx-vault:/# ls /mnt/vault/
foo
root@nginx-vault:/# cat /mnt/vault/foo 
hello
```

結果、Vaultの機密情報が格納されたパス`foo`の値が`/mnt/vault/foo`に格納されています。
このように、Vaultに格納された機密情報をPVC,PVを通じて参照することが可能となります。

# 感想
今回の動作検証では、機密情報を格納するためのリポジトリであるVaultに格納したデータを、Kubernetesで永続ストレージ(PVC,PV)としてマウントする方法を動作検証しました。これにより、容易にVaultに格納されたパスワードやアクセストークンをKubernetes上で読み出すことが可能となります。
ついつい利便性を優先し、アプリケーションのパスワードやアクセストークンなどの機密情報の管理が甘くなりがちな人もいるかもしれません。今回検証したVaultとKubernetsの連携のハードルは高くないので、ぜひ選択肢のひとつとしてご検討ください。

# おまけ(Tips)
環境変数として利用したい場合は以下のように設定できます。

```
$ export FOO=`cat /mnt/vault/foo`
```

# 参考情報
- [secrets-store-csi-driver](https://github.com/deislabs/secrets-store-csi-driver)
- [KubeCon2019 EU: "Secrets Store CSI Driver-Bring Your Own Enterprise Secrets Store to K8s"](https://kccnceu19.sched.com/event/MPdZ/secrets-store-csi-driver-bring-your-own-enterprise-secrets-store-to-k8s-rita-zhang-microsoft-anubhav-mishra-hashicorp)