vault-k8s を使って Vault に格納されたシークレットを自動で Pod に渡す

この記事は [Z Lab Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zlab) の22日目の記事となります。

# はじめに

2019年12月20日 に Hashicorp社 から [hashicorp/vault-k8s](https://github.com/hashicorp/vault-k8s) というツールが公開されました。これを使うことで Vault に格納されたシークレットをサイドカーを介して自動で Pod に渡すことが可能になるようです。非常に良さそうなので早速試してみました。
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Excited to announce a Vault + K8S integration to automatically inject secrets (+ rotation!) into Kubernetes pods. This is seriously the easiest way to get going with Vault now, and its total magic. 🧙‍♂️ <br><br>📝 Blog: <a href="https://t.co/wN7ogc640p">https://t.co/wN7ogc640p</a> <br><br>📺 Video: <a href="https://t.co/RHZHa1RDAa">https://t.co/RHZHa1RDAa</a> <a href="https://t.co/DklXeHAAZ0">pic.twitter.com/DklXeHAAZ0</a></p>&mdash; Mitchell Hashimoto (@mitchellh) <a href="https://twitter.com/mitchellh/status/1207770541358075904?ref_src=twsrc%5Etfw">December 19, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# vault-k8s について

まずはドキュメントに目を通して vault-k8s についての理解を深めていきます。

## 概要

vault-k8s は Vault と Kubernetes を統合するためのツールで、それの1つの機能である [Agent Sidecar Injector](https://www.vaultproject.io/docs/platform/k8s/injector/index.html) を使うことで Vault に格納されたシークレットをサイドカーを介して自動で Pod に渡すことが可能になるようです。

ちなみに vault-k8s の初回リリース [v0.1.0](https://github.com/hashicorp/vault-k8s/releases/tag/v0.1.0) では機能として Agent Sidecar Injector のみがサポートされている状況で、今後 Vault と Kubernetes の統合に向けて機能が追加されていく予定のようです。というわけでここからは Agent Sidecar Injector について掘り下げていきたいと思います。

## Agent Sidecar Injector 

Agent Sidecar Injector の実体は Mutating Admission Webhook Controller[^1] で、Pod マニフェストに `vault.hashicorp.com/*` で始まる [特定のアノテーション](https://www.vaultproject.io/docs/platform/k8s/injector/index.html#annotations) を付与すると、その値を元に Agent Sidecar Injector が Pod マニフェストを書き換えて Vault からシークレットを取得するための Vault Agent コンテナを Init もしくは Init + Sidecar として追加する仕組みとなっています。[^2] なお、Vault Agent コンテナからアプリケーションコンテナへのデータの受け渡しは共有メモリボリュームを介して行われる仕様となっています。

![1576690939-request-flow-final.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49924/4523bdfb-a033-6a37-a88d-07e7e98408ed.png)

これにより Vault クライアントの機能をアプリケーションに組み込むことなく、Pod マニフェストにアノテーションを付与してデプロイするだけで Vault から取得したシークレットを Pod 内のアプリケーションコンテナに渡すことが可能になります。

## Vault への認証方法

認証方法としては [Kubernetes Auth Method](https://www.vaultproject.io/docs/auth/kubernetes.html) が採用されているため Pod に割り振られた Service Account 単位での認証となります。Service Account と Vault ポリシーを関連付けることで、特定のアプリケーションに必要なシークレットはそのアプリケーションからしか取得させないといった制御が可能になります。

## インストール方法

vault-k8s がサポートされている [Vault Helm Chart](https://github.com/hashicorp/vault-helm) を使用してインストールする方法が推奨されています。vault-k8s は Vault Helm Chart の [v0.3.0 からサポートされている](https://github.com/hashicorp/vault-helm/blob/v0.3.0/CHANGELOG.md#030-december-19th-2019) ので、それ以降のバージョンをインストールすると良さそうです。また、[Dockerイメージ](https://hub.docker.com/r/hashicorp/vault-k8s) も公開されているので、Helm を使わずに自らマニフェストを作成してインストールするアプローチを取ることもできます。

# 実際に動かしてみる

minikube で Kubernetes クラスタを作成します。

```bash
minikube start --kubernetes-version v1.17.0
```

今回は推奨方式である Vault Helm Chart を使用してインストールするので、まずは Kubernetes クラスタに helm をインストール[^3] します。使用する helm のバージョンは `v2.16.1` となります。

```bash
cat << EOM | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: cluster-admin
rules:
- apiGroups:
  - '*'
  resources:
  - '*'
  verbs:
  - '*'
- nonResourceURLs:
  - '*'
  verbs:
  - '*'
EOM

kubectl create serviceaccount -n kube-system tiller
kubectl create clusterrolebinding tiller-cluster-rule \
  --clusterrole=cluster-admin --serviceaccount=kube-system:tiller

# これはワークアラウンドなので詳細が気になる方は注釈を参照ください
helm init --service-account tiller \
  --override spec.selector.matchLabels.'name'='tiller',spec.selector.matchLabels.'app'='helm' \
  --output yaml \
  | sed 's@apiVersion: extensions/v1beta1@apiVersion: apps/v1@' \
  | kubectl apply -f -
```

tiller が起動していることが確認できたら helm のインストールは完了です。

```bash
$ kubectl get -n kube-system pod tiller-deploy-969865475-9zkxx
NAME                            READY   STATUS    RESTARTS   AGE
tiller-deploy-969865475-9zkxx   1/1     Running   0          3m40s
```

vault-helm の v0.3.0 をクローンします。

```bash
git clone -b v0.3.0  https://github.com/hashicorp/vault-helm.git
```

vault-helm をインストールします。

```bash
helm install --name=vault --set='server.dev.enabled=true' ./vault-helm
```

上記のコマンドを実行すると `vault-agent-injector` に加えて Vault がデプロイされます。Vault Helm Chart を使うと Vault もインストールされるのですぐに試したい人には親切ですが、Vault を Kubernetes クラスタの外に構築しているケースでは Helm Chart を参考に自前でマニフェストを作る必要がありそうです。

```bash
$ kubectl get all
NAME                                        READY   STATUS    RESTARTS   AGE
pod/vault-0                                 1/1     Running   0          3m4s
pod/vault-agent-injector-5945fb98b5-l9vr5   1/1     Running   0          3m6s

NAME                               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/kubernetes                 ClusterIP   10.96.0.1       <none>        443/TCP             33m
service/vault                      ClusterIP   10.96.27.216    <none>        8200/TCP,8201/TCP   3m6s
service/vault-agent-injector-svc   ClusterIP   10.96.173.134   <none>        443/TCP             3m6s

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/vault-agent-injector   1/1     1            1           3m6s

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/vault-agent-injector-5945fb98b5   1         1         1       3m6s

NAME                     READY   AGE
statefulset.apps/vault   1/1     3m6s
```

同時に Mutating Admission Webhook の設定が適用されており、マニフェストを確認してみると `vault-agent-injector-svc` サービスへリクエストする設定となっており、`vault-agent-injector` が Mutating Admission Webhook Controller として機能することがわかります。

```bash
$ kubectl get mutatingwebhookconfigurations
NAME                       CREATED AT
vault-agent-injector-cfg   2019-12-20T16:40:45Z

# 一部抜粋
$ kubectl get mutatingwebhookconfigurations vault-agent-injector-cfg -o yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  ...
webhooks:
- admissionReviewVersions:
  - v1beta1
  clientConfig:
    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTRENDQWU2Z0F3SUJBZ0lVSk9RQ3NmWU8vNlJhVVExL3RVazZGNWV5VWpVd0NnWUlLb1pJemowRUF3SXcKR2pFWU1CWUdBMVVFQXhNUFFXZGxiblFnU1c1cVpXTjBJRU5CTUI0WERURTVNVEl5TURFMk5ERXpORm9YRFRJNQpNVEl4TnpFMk5ESXpORm93R2pFWU1CWUdBMVVFQXhNUFFXZGxiblFnU1c1cVpXTjBJRU5CTUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRThSTkNxRkZMQ0sxR3Z3aTlxNGlDWHhUZW4yN3Nob2Z3bG5OWjJSanQKRGNTQ0t3SHFndXpleVd2eXE0VVVNbHNrZUQzZVRhYmtHN25qN2ZoSmd5Y3lGcU9DQVJBd2dnRU1NQTRHQTFVZApEd0VCL3dRRUF3SUNoREFUQmdOVkhTVUVEREFLQmdnckJnRUZCUWNEQVRBUEJnTlZIUk1CQWY4RUJUQURBUUgvCk1HZ0dBMVVkRGdSaEJGOWpOenBsWkRvMk9UcGhNVHBsWVRvMFlqb3dOem8yTnpveVlqb3hZVHBoWWpveFl6b3oKTlRvNVlUb3dZenBoWWpwbFl6b3pZem94TURvNFlqb3lOenBoTkRveU5qcG1aam8xTmpwall6b3lORG8zT0RwbQpOem8xTlRwbU1qcGpaakJxQmdOVkhTTUVZekJoZ0Y5ak56cGxaRG8yT1RwaE1UcGxZVG8wWWpvd056bzJOem95Cllqb3hZVHBoWWpveFl6b3pOVG81WVRvd1l6cGhZanBsWXpvell6b3hNRG80WWpveU56cGhORG95TmpwbVpqbzEKTmpwall6b3lORG8zT0RwbU56bzFOVHBtTWpwalpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpQUE0ZE05REJZbgp0dVFXZFVDWDlIaUgyM0VIUGJBMmVMNEs3aDVxdEJXR3ZnSWhBTVM0NXRzOWtranptSS9XdDNkR1FIdXdxVEJTCk9McVRUZjR6b3RwdnFDMzUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
    service:
      name: vault-agent-injector-svc
      namespace: default
      path: /mutate
      port: 443
  ...
  name: vault.hashicorp.com
  ...
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
    scope: '*'
  ...
```

Vault のセットアップをするために Pod に exec します。

```bash
kubectl exec -it vault-0 /bin/sh
```

サンプルのアプリケーション用にポリシー `all-secret-reader` を作成します。お試しなので強めな権限を持ったポリシーを作成していますが、本番導入時にはアプリケーション毎に適切な権限を与えたものを作成するのが良いと思います。

```bash
cat << EOF | vault policy write all-secret-reader -
path "secret*" {
  capabilities = ["read"]
}
EOF
```

Vault で [Kubernetes Auth Method](https://www.vaultproject.io/docs/auth/kubernetes.html) を有効化します。

```bash
vault auth enable kubernetes

vault write auth/kubernetes/config \
    token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
    kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \
    kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
```

前のステップで作成したポリシーを付与したロール `sample-app` を作成します。これは default ネームスペースで sample-app という Service Account を持った Pod がアクセス可能なロールとなっています。

```bash
vault write auth/kubernetes/role/sample-app \
   bound_service_account_names=sample-app \
   bound_service_account_namespaces=default \
   policies=all-secret-reader \
   ttl=1h
```

Vault にシークレットを追加します。

```bash
vault kv put secret/sample-credentials username=xxx password=yyy
```

以上で Vault のセットアップは完了ですので Pod から exit します。

```bash
exit
```

シークレットを取得するアプリケーションマニフェストを作成します。`vault.hashicorp.com/*` で始まるアノテーション[^4] で "どのシークレット" を "どのロール" を使用して取得するかを指定します。Namespace と Service Account は事前に作成したロールに対応したものを設定する必要がありますのでご注意ください。

```yaml:sample-app.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sample-app
  labels:
    app: agent-sidecar-injector-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  labels:
    app: agent-sidecar-injector-demo
spec:
  selector:
    matchLabels:
      app: agent-sidecar-injector-demo
  replicas: 1
  template:
    metadata:
      annotations:
        #  Agent Sidecar Injector 有効化
        vault.hashicorp.com/agent-inject: "true"
        # Key でファイル名を Value で取得するシークレットを指定
        # vault.hashicorp.com/agent-inject-secret-<unique-name> の <unique-name> の部分がファイル名となる
        # 例: vault.hashicorp.com/agent-inject-secret-foo.txt とすれば foo.txt というファイルに出力される
        vault.hashicorp.com/agent-inject-secret-sample-credentials: "secret/sample-credentials"
        # Vault の認証に使用するロール
        vault.hashicorp.com/role: "sample-app"
      labels:
        app: agent-sidecar-injector-demo
    spec:
      serviceAccountName: sample-app
      containers:
      - name: sample-app
        image: jweissig/app:0.0.1
```

アプリケーションをデプロイします。Pod の 作成/更新 を検知して Vault Agent コンテナが追加されて Vault からシークレットが取得されます。

```bash
kubectl apply -f sample-app.yaml
```

Vault Agent コンテナによって取得されたシークレットは、アプリケーションコンテナ内の `/vault/secrets` に出力される（少し見た感じだと v0.1.0 ではこちらのパスはアノテーションでの変更には対応しておらずハードコードされていそうです）ので確認してみます。

```bash
$ kubectl get pods sample-app-565fbfbbcd-mccrt
NAME                          READY   STATUS    RESTARTS   AGE
sample-app-565fbfbbcd-mccrt   2/2     Running   0          2m28s

$ kubectl exec sample-app-565fbfbbcd-mccrt -c sample-app -- ls -l /vault/secrets
total 4
-rw-r--r--    1 100      1000           137 Dec 20 16:56 sample-credentials

$ kubectl exec sample-app-565fbfbbcd-mccrt -c sample-app -- cat /vault/secrets/sample-credentials
data: map[password:yyy username:xxx]
metadata: map[created_time:2019-12-20T16:52:30.737177761Z deletion_time: destroyed:false version:1]
```

Vault からシークレットが取得されてファイル `/vault/secrets/sample-credentials` に出力されることが確認できましたが、そのまま使おうとするとGoの構造体として出力されるため [Vault Agent Templates](https://www.vaultproject.io/docs/agent/template/index.html) を使ってテンプレートを定義することが推奨されているようです。

というわけでアプリケーションのマニフェストにテンプレート用のアノテーションを追加します。

```diff:sample-app.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sample-app
  labels:
    app: agent-sidecar-injector-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  labels:
    app: agent-sidecar-injector-demo
spec:
  selector:
    matchLabels:
      app: agent-sidecar-injector-demo
  replicas: 1
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-inject-secret-sample-credentials: "secret/sample-credentials"
+       vault.hashicorp.com/agent-inject-template-sample-credentials: |
+          {{- with secret "secret/sample-credentials" -}}
+          USERNAME={{ .Data.data.username }}
+          PASSWORD={{ .Data.data.password }}
+          {{- end }}
        vault.hashicorp.com/role: "sample-app"
      labels:
        app: agent-sidecar-injector-demo
    spec:
      serviceAccountName: sample-app
      containers:
      - name: sample-app
        image: jweissig/app:0.0.1
```

新しいマニフェストを適用します。

```bash
kubectl apply -f sample-app.yaml
```

再作成されたアプリケーションでテンプレートが適用されたファイルが出力されているか確認します。

```bash
$ kubectl get pods sample-app-5995b4df85-p5z9k
NAME                          READY   STATUS    RESTARTS   AGE
sample-app-5995b4df85-p5z9k   2/2     Running   0          20s

$ kubectl exec sample-app-5995b4df85-p5z9k -c sample-app -- cat /vault/secrets/sample-credentials
USERNAME=xxx
PASSWORD=yyy
```

想定通りのファイルが出力されました。このように Vault Agent Templates を使うことで各種ソフトウェアに応じた設定ファイルに Vault から取得したシークレットを柔軟に埋め込むことが可能になります。

今回は単純なテンプレートの例を紹介しましたが Vault Agent Templates では [Consul Template](https://github.com/hashicorp/consul-template) の文法がサポートされているので、導入を行う際にはそちらの文法を参照してテンプレートを作成するのが良さそうです。また、アノテーションの指定方法については [Vault Agent Injector Examples](https://www.vaultproject.io/docs/platform/k8s/injector/examples.html) が非常に参考になるかと思います。

以上が、動作検証となります。

# 最後に

今回は vault-k8s の機能である Agent Sidecar Injector を使って Vault に格納されたシークレットをサイドカーを介して自動で Pod に渡す方法を紹介しました。Hashicorp社 では今後も継続して Vault の Kubernetes サポートを充実させていくそうなので、引き続き動向を追いかけていきたいと思います。

また、現在の業務に関連するところだと、SPIRE から Workload に払い出された Workload Identity となる SVID（TLS証明書）を使って、Vault などのシークレットストアからアプリケーションが自身のシークレットを自ら取得する Secure Introduction[^6] のより良いアーキテクチャを探っているところなので、今回の Vault Agent Injector なども参考にしながら最適解が見つけられたらなと思っています。

余談ですが Vault Agent Injector と似たようなもので、[Secrets Store CSI driver](https://github.com/deislabs/secrets-store-csi-driver)（シークレットストアのプロバイダーとして [Azure Key Vault provider](https://github.com/Azure/secrets-store-csi-driver-provider-azure) と [HashiCorp Vault Provider](https://github.com/hashicorp/secrets-store-csi-driver-provider-vault) が提供されている） という Vault のシークレットを CSI を介して Pod にマウントするためのプロダクトも公開されており、どちらが利用に最適なのかは自分では今のところ判断がつかない状況です...。[^5]

おしまい。

# 参考資料

- https://www.hashicorp.com/blog/injecting-vault-secrets-into-kubernetes-pods-via-a-sidecar/
- https://www.vaultproject.io/docs/platform/k8s/injector/index.html
- https://www.vaultproject.io/docs/platform/k8s/injector/installation.html
- https://www.vaultproject.io/docs/platform/k8s/injector/examples.html

[^1]: Mutating Admission Webhook については [こちら](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) を参照ください
[^2]: 図は https://www.hashicorp.com/blog/injecting-vault-secrets-into-kubernetes-pods-via-a-sidecar/ より引用
[^3]: https://qiita.com/reoring/items/e50877f543bed72d93ee と https://github.com/helm/helm/issues/6374#issuecomment-533427268 を参考に helm をインストールします
[^4]: サポートされているアノテーションについては [Annnotations - Agent Sidecar Injector](https://www.vaultproject.io/docs/platform/k8s/injector/index.html#annotations) を参照ください
[^5]: Secrets Store CSI driver も気になるぞという方は [[Kubernetes] Vaultに格納された機密情報を永続ストレージとしてマウント](https://qiita.com/ysakashita/items/ea168066ce22c8467baf) が参考になるかと思います
[^6]: 弊社での取り組みは [Challenging Secure Introduction with SPIFFE](https://speakerdeck.com/hiyosi/challenging-secure-introduction-with-spiffe) を参照ください