この記事は Z Lab Advent Calendar 2019 の22日目の記事となります。
はじめに
2019年12月20日 に Hashicorp社 から hashicorp/vault-k8s というツールが公開されました。これを使うことで Vault に格納されたシークレットをサイドカーを介して自動で Pod に渡すことが可能になるようです。非常に良さそうなので早速試してみました。
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. 🧙♂️
— Mitchell Hashimoto (@mitchellh) December 19, 2019
📝 Blog: https://t.co/wN7ogc640p
📺 Video: https://t.co/RHZHa1RDAa pic.twitter.com/DklXeHAAZ0
vault-k8s について
まずはドキュメントに目を通して vault-k8s についての理解を深めていきます。
概要
vault-k8s は Vault と Kubernetes を統合するためのツールで、それの1つの機能である Agent Sidecar Injector を使うことで Vault に格納されたシークレットをサイドカーを介して自動で Pod に渡すことが可能になるようです。
ちなみに vault-k8s の初回リリース v0.1.0 では機能として Agent Sidecar Injector のみがサポートされている状況で、今後 Vault と Kubernetes の統合に向けて機能が追加されていく予定のようです。というわけでここからは Agent Sidecar Injector について掘り下げていきたいと思います。
Agent Sidecar Injector
Agent Sidecar Injector の実体は Mutating Admission Webhook Controller1 で、Pod マニフェストに vault.hashicorp.com/*
で始まる 特定のアノテーション を付与すると、その値を元に Agent Sidecar Injector が Pod マニフェストを書き換えて Vault からシークレットを取得するための Vault Agent コンテナを Init もしくは Init + Sidecar として追加する仕組みとなっています。2 なお、Vault Agent コンテナからアプリケーションコンテナへのデータの受け渡しは共有メモリボリュームを介して行われる仕様となっています。
これにより Vault クライアントの機能をアプリケーションに組み込むことなく、Pod マニフェストにアノテーションを付与してデプロイするだけで Vault から取得したシークレットを Pod 内のアプリケーションコンテナに渡すことが可能になります。
Vault への認証方法
認証方法としては Kubernetes Auth Method が採用されているため Pod に割り振られた Service Account 単位での認証となります。Service Account と Vault ポリシーを関連付けることで、特定のアプリケーションに必要なシークレットはそのアプリケーションからしか取得させないといった制御が可能になります。
インストール方法
vault-k8s がサポートされている Vault Helm Chart を使用してインストールする方法が推奨されています。vault-k8s は Vault Helm Chart の v0.3.0 からサポートされている ので、それ以降のバージョンをインストールすると良さそうです。また、Dockerイメージ も公開されているので、Helm を使わずに自らマニフェストを作成してインストールするアプローチを取ることもできます。
実際に動かしてみる
minikube で Kubernetes クラスタを作成します。
minikube start --kubernetes-version v1.17.0
今回は推奨方式である Vault Helm Chart を使用してインストールするので、まずは Kubernetes クラスタに helm をインストール3 します。使用する helm のバージョンは v2.16.1
となります。
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 のインストールは完了です。
$ 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 をクローンします。
git clone -b v0.3.0 https://github.com/hashicorp/vault-helm.git
vault-helm をインストールします。
helm install --name=vault --set='server.dev.enabled=true' ./vault-helm
上記のコマンドを実行すると vault-agent-injector
に加えて Vault がデプロイされます。Vault Helm Chart を使うと Vault もインストールされるのですぐに試したい人には親切ですが、Vault を Kubernetes クラスタの外に構築しているケースでは Helm Chart を参考に自前でマニフェストを作る必要がありそうです。
$ 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 として機能することがわかります。
$ 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 します。
kubectl exec -it vault-0 /bin/sh
サンプルのアプリケーション用にポリシー all-secret-reader
を作成します。お試しなので強めな権限を持ったポリシーを作成していますが、本番導入時にはアプリケーション毎に適切な権限を与えたものを作成するのが良いと思います。
cat << EOF | vault policy write all-secret-reader -
path "secret*" {
capabilities = ["read"]
}
EOF
Vault で Kubernetes Auth Method を有効化します。
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 がアクセス可能なロールとなっています。
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 にシークレットを追加します。
vault kv put secret/sample-credentials username=xxx password=yyy
以上で Vault のセットアップは完了ですので Pod から exit します。
exit
シークレットを取得するアプリケーションマニフェストを作成します。vault.hashicorp.com/*
で始まるアノテーション4 で "どのシークレット" を "どのロール" を使用して取得するかを指定します。Namespace と Service Account は事前に作成したロールに対応したものを設定する必要がありますのでご注意ください。
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 からシークレットが取得されます。
kubectl apply -f sample-app.yaml
Vault Agent コンテナによって取得されたシークレットは、アプリケーションコンテナ内の /vault/secrets
に出力される(少し見た感じだと v0.1.0 ではこちらのパスはアノテーションでの変更には対応しておらずハードコードされていそうです)ので確認してみます。
$ 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 を使ってテンプレートを定義することが推奨されているようです。
というわけでアプリケーションのマニフェストにテンプレート用のアノテーションを追加します。
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
新しいマニフェストを適用します。
kubectl apply -f sample-app.yaml
再作成されたアプリケーションでテンプレートが適用されたファイルが出力されているか確認します。
$ 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 の文法がサポートされているので、導入を行う際にはそちらの文法を参照してテンプレートを作成するのが良さそうです。また、アノテーションの指定方法については Vault Agent Injector Examples が非常に参考になるかと思います。
以上が、動作検証となります。
最後に
今回は vault-k8s の機能である Agent Sidecar Injector を使って Vault に格納されたシークレットをサイドカーを介して自動で Pod に渡す方法を紹介しました。Hashicorp社 では今後も継続して Vault の Kubernetes サポートを充実させていくそうなので、引き続き動向を追いかけていきたいと思います。
また、現在の業務に関連するところだと、SPIRE から Workload に払い出された Workload Identity となる SVID(TLS証明書)を使って、Vault などのシークレットストアからアプリケーションが自身のシークレットを自ら取得する Secure Introduction5 のより良いアーキテクチャを探っているところなので、今回の Vault Agent Injector なども参考にしながら最適解が見つけられたらなと思っています。
余談ですが Vault Agent Injector と似たようなもので、Secrets Store CSI driver(シークレットストアのプロバイダーとして Azure Key Vault provider と HashiCorp Vault Provider が提供されている) という Vault のシークレットを CSI を介して Pod にマウントするためのプロダクトも公開されており、どちらが利用に最適なのかは自分では今のところ判断がつかない状況です...。6
おしまい。
参考資料
- 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
-
図は https://www.hashicorp.com/blog/injecting-vault-secrets-into-kubernetes-pods-via-a-sidecar/ より引用 ↩
-
https://qiita.com/reoring/items/e50877f543bed72d93ee と https://github.com/helm/helm/issues/6374#issuecomment-533427268 を参考に helm をインストールします ↩
-
サポートされているアノテーションについては Annnotations - Agent Sidecar Injector を参照ください ↩
-
弊社での取り組みは Challenging Secure Introduction with SPIFFE を参照ください ↩
-
Secrets Store CSI driver も気になるぞという方は [Kubernetes] Vaultに格納された機密情報を永続ストレージとしてマウント が参考になるかと思います ↩