Vault の Agent Sidecar Injector を使うと Vault に格納された Secret を Sidecar 経由で Pod に渡したりすることができます。
ここでは、Agent SIdecar Injector の概要を把握し、Vault の PKI Secret Engine で生成した証明書を nginx が動く Pod に動的に配置して、クライアント証明書認証を行うまで試してみます。
概要
ドキュメントは https://www.vaultproject.io/docs/platform/k8s/injector/index.html にあります。
また、https://qiita.com/ryysud/items/ec8de49aa39a2f9fbceb でも丁寧に解説されてありますので、参照すると良さそうです。
Vault Agent Injector は Pod の CREATE
と UPDATE
イベントをインターセプトして、 [vault.hashicorp.com/agent-inject:](http://vault.hashicorp.com/agent-inject:) true
という Annotation があれば Vault から Secret を取得するための Vault Agent コンテナを InitContainer として追加する Mutation Webhook Controller です。
これは Annotation で実現することもできますし、Vault Agent の設定ファイルを用いることでも可能です。
この図は sidecar として consul-template がありますが、こういうことが実現できます。
- initContainer である vault-agent-auth が Vault の Kubernetes Auth Method でログイン
- Vault のトークンを取得
- 共有ボリュームに Vault のトークンを書き込む
- consul-template はそのトークンを使って Vault の Secret を読み込んで、ミドルウェアの設定ファイルを生成したりする
大変便利なのですが、2020/02/28 現在、Vault の Secret を Volume のファイルに書き出すしかなく、環境変数で渡すということはできません。これは https://github.com/hashicorp/vault-k8s/issues/14 で議論されていて、issue に書かれている3つの課題をクリアできれば実装されるそうです。実装したいというコメントがあるので、 Watch しておきたいですね。
インストール
helm でインストールします。長いのでコマンド結果は省略します。
❯ git clone git@github.com:hashicorp/vault-helm.git
❯ cd vault-helm/
❯ git checkout v0.4.0
❯ cd ..
❯ helm inspect values ./vault-helm > values.yml
❯ helm install --name vault -f values.yml ./vault-helm
上記で Vault server も Kubernetes クラスタ上に展開されます。
既存の Vault server を指定したい場合は externalVaultAddr
を設定すると Vault server の展開はせず、向き先だけを変更できそうです。
ref : https://github.com/hashicorp/vault-helm/blob/master/values.yaml#L20
インストールすると、このようになります。
❯ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 Running 0 19m
pod/vault-agent-injector-686fbb6c54-6q6cx 1/1 Running 0 19m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault ClusterIP 10.0.1.95 <none> 8200/TCP,8201/TCP 19m
service/vault-agent-injector-svc ClusterIP 10.0.11.105 <none> 443/TCP 19m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/vault-agent-injector 1/1 1 1 19m
NAME DESIRED CURRENT READY AGE
replicaset.apps/vault-agent-injector-686fbb6c54 1 1 1 19m
NAME READY AGE
statefulset.apps/vault 0/1 19m
Port-forwarding して vault につなぎます。
❯ kubectl port-forward vault-0 8200:8200
初期化と Unseal も忘れないようにしておきます。
❯ kubectl exec -ti vault-0 -- vault operator init
❯ kubectl exec -ti vault-0 -- vault operator unseal
PKI Secret Engine で証明書を作る
今回は Vault の PKI Secret Engine で作成された証明書を nginx に動的に配置し、クライアント証明書認証を行うことを目的としますので、まずは PKI Secret Engine を有効にして証明書を作成します。
❯ vault login
❯ vault secrets enable -path=mrtc0 -description="mrtc0 Root CA" -max-lease-ttl=87600h pki
Success! Enabled the pki secrets engine at: mrtc0/
❯ vault write mrtc0/root/generate/internal common_name="mrtc0 Root CA" ttl=87600h key_bites=4096 exclude_cn_from_sans=true
Key Value
--- -----
certificate -----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUQSptIf7IkZijJrNwagiATUSLQLEwDQYJKoZIhvcNAQEL
[snip]
❯ vault write mrtc0/config/urls issuing_certificate="http://127.0.0.1:8200/v1/mrtc0/ca" crl_distribution_points="http://127.0.0.1:8200/v1/mrtc0/crl"
Success! Data written to: mrtc0/config/urls
❯ vault write mrtc0/roles/mrtc0ssrfin key_bites=2048 max_ttl=8760h allow_any_name=true
Success! Data written to: mrtc0/roles/mrtc0ssrfin
❯ vault write mrtc0/issue/mrtc0ssrfin common_name="mrtc0.ssrf.in" ip_sans="127.0.0.1" ttl=720h foramt=pem
Key Value
--- -----
certificate -----BEGIN CERTIFICATE-----
MIIDhjCCAm6gAwIBAgIUOk+RxGxcp6NJxB+5lXNTRVF5XqowDQYJKoZIhvcNAQEL
[snip]
続いてこの証明書を読み出したりするためのポリシーを作成します。このポリシーは後で ServiceAccount と紐づくことになります。
# policy.hcl
path "mrtc0/issue/*" {
capabilities = ["read", "list", "update"]
}
ポリシーを適用します。
❯ vault policy write pki-policy policy.hcl
Success! Uploaded policy: pki-policy
Kubernetes Auth Method
まずは先程の図の 1, 2 に該当する処理 Kubernetes Auth Method を試します。
これは ServiceAccount のトークンと証明書を使って Vault にログインする方法です。ServiceAccount と Vault のポリシーを紐付けることができるので、権限周りも意識できます。
ServiceAccount(以降SA) を使ってログインするので、そのための SA vault-auth
を作成します。
# vault-service-account.yml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth
namespace: vault
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
namespace: vault
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-auth
namespace: vault
上記 manifest を適用します。
❯ kubectl apply -f vault-service-account.yml
serviceaccount/vault-auth created
clusterrolebinding.rbac.authorization.k8s.io/role-tokenreview-binding created
続いて Kubernetes Auth Method の設定を行います。Kubernetes Auth Method 自体は vault auth enable kubernets
で有効になります。
❯ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
次に先程作成した vault-auth SA でログインできるようにするために、vault-auth SA のトークンと証明書が必要になります。また、Kubernetes API のエンドポイントも取得しておきます。
❯ export K8S_VAULT_SA_SECRET=(kubectl get serviceaccount vault-auth -o json | jq -r .secrets[0].name)
❯ kubectl get secret $K8S_VAULT_SA_SECRET -o json | jq -r '.data["token"]' | base64 -D > ca.token
❯ kubectl get secret $K8S_VAULT_SA_SECRET -o json | jq -r '.data["ca.crt"]' | base64 -D > ca.crt
/ $ echo $KUBERNETES_PORT_443_TCP_ADDR
10.0.0.1
上記でログインできるように auth/kubernetes/config
に設定を書き込みます。
❯ vault write auth/kubernetes/config \
token_reviewer_jwt=(cat ca.token) \
kubernetes_host=https://10.0.0.1:443 \
kubernetes_ca_cert=@ca.crt
Success! Data written to: auth/kubernetes/config
次に証明書を作成した過程で作ったポリシーをロールに紐付けます。
今回は app
というロールとしています。この設定で vault
という namespace の vault-auth
という SA でアクセス可能となります。namespace や SA は複数指定できます。
❯ vault write auth/kubernetes/role/app \
bound_service_account_names=vault-auth \
bound_service_account_namespaces=vault \
policies=pki-policy \
ttl=1h
Success! Data written to: auth/kubernetes/role/app
これで準備完了です。実際にログインできるか試してみましょう。
❯ kubectl run --image=ubuntu:latest --rm -it --serviceaccount=vault-auth test-pod -- bash
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
If you don't see a command prompt, try pressing enter.
root@test-pod-f7668766d-p4qcq:/# apt update ; apt install -y jq curl
[snip]
root@test-pod-f7668766d-p4qcq:/# export VAULT_ADDR=http://vault.vault.svc.cluster.local:8200
root@test-pod-f7668766d-p4qcq:/# export KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
root@test-pod-f7668766d-p4qcq:/# curl --request POST \
> --data '{"jwt": "'"$KUBE_TOKEN"'", "role": "app"}' \
> $VAULT_ADDR/v1/auth/kubernetes/login | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1534 100 660 100 874 18857 24971 --:--:-- --:--:-- --:--:-- 43828
{
"request_id": "32e2c297-2fd2-08fe-9fe8-770e03e1d8cc",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": null,
"warnings": null,
"auth": {
"client_token": "s.[snip]",
"accessor": "[snip]",
"policies": [
"default",
"pki-policy"
],
"token_policies": [
"default",
"pki-policy"
],
"metadata": {
"role": "app",
"service_account_name": "vault-auth",
"service_account_namespace": "vault",
"service_account_secret_name": "vault-auth-token-sp78t",
"service_account_uid": "4d9ad1b1-59dd-11ea-a767-42010a8c00fc"
},
"lease_duration": 3600,
"renewable": true,
"entity_id": "8c2a9a14-1315-e4ec-f8d7-9db9da9b8b8d",
"token_type": "service",
"orphan": true
}
}
無事ログインできました。
Annotation で Secret を取得する
さて、それでは Agent Sidecar Injector を使って Secret を取得してみます。
前述しましたが、これは Annotation で実現することもできますし、Vault Agent の設定ファイルを用いることでも可能です。まずは Annotation でやってみます。
次のような manifest を用意します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-agent-example
labels:
app: sample
spec:
replicas: 1
selector:
matchLabels:
app: sample
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
# agent-inject-secret-<filename> の <filename> の部分がファイル名となる
vault.hashicorp.com/agent-inject-secret-cert: "mrtc0/issue/mrtc0ssrfin"
# vault agent template を使って整形する
vault.hashicorp.com/agent-inject-template-cert: |
{{- with secret "mrtc0/issue/mrtc0ssrfin" "common_name=mrtc0.ssrf.in" }}
{{ .Data.certificate }}
{{ end }}
vault.hashicorp.com/role: "app"
labels:
app: sample
spec:
serviceAccountName: vault-auth
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 80
vault.hashicorp.com/agent-inject: "true"
にすることで Agent Sidecar Inject が有効になります。また、 vault.hashicorp.com/role: "app"
も忘れずに。
これを適用すると vault agent が Sidecar として Injection されます。
❯ kubectl apply -f vault-agent-example-deployment.yaml
deployment.apps/vault-agent-example created
❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
vault-0 1/1 Running 0 35m
vault-agent-example-54bb85f485-7mpl9 2/2 Running 0 27s
vault-agent-injector-686fbb6c54-mhkb5 1/1 Running 0 35m
❯ kubectl describe pod vault-agent-example-54bb85f485-7mpl9
[snip]
Containers:
nginx-container:
[snip]
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from vault-auth-token-sp78t (ro)
/vault/secrets from vault-secrets (rw)
vault-agent:
[snip]
Command:
/bin/sh
-ec
Args:
echo ${VAULT_CONFIG?} | base64 -d > /tmp/config.json && vault agent -config=/tmp/config.json
vault agent によって取得された Secret は /vault/secrets
配下に書き出されます。
❯ kubectl exec -it vault-agent-example-54bb85f485-7mpl9 sh
Defaulting container name to nginx-container.
Use 'kubectl describe pod/vault-agent-example-54bb85f485-7mpl9 -n vault' to see all of the containers in this pod.
# cat /vault/secrets/cert
-----BEGIN CERTIFICATE-----
MIIDgDCCAmigAwIBAgIUIiUrj50x0r3UXjBDuUsFMKz0bn0wDQYJKoZIhvcNAQEL
[snip]
便利 !!!
Vault Agent の設定と Consul Template の利用
先程は Annotaion を指定して Secret を取得しました。次に Vault Agent に設定を入れ、さらに Consul Template を Sidecar として動かして Secret の取得を行ってみます。
Vault Agent の設定として、次の内容を vault-agent-config.hcl
として保存します。
exit_after_auth = true
pid_file = "/home/vault/pidfile"
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "app"
}
}
sink "file" {
config = {
path = "/home/vault/.vault-token"
}
}
}
次に Consul Template の設定を consul-template-config.hcl
として保存します。
vault {
renew_token = false
vault_agent_token_file = "/home/vault/.vault-token"
retry {
backoff = "1s"
}
}
template {
destination = "/etc/secrets/mrtc0.ssrf.in.pem"
contents = "{{- with secret \"mrtc0/issue/mrtc0ssrfin\" \"common_name=mrtc0.ssrf.in\" }}{{ .Data.issuing_ca }}{{ end }}"
}
template {
destination = "/etc/secrets/mrtc0.ssrf.in.crt"
contents = "{{- with secret \"mrtc0/issue/mrtc0ssrfin\" \"common_name=mrtc0.ssrf.in\" }}{{ .Data.certificate }}{{ end }}"
}
template {
destination = "/etc/secrets/mrtc0.ssrf.in.key"
contents = "{{- with secret \"mrtc0/issue/mrtc0ssrfin\" \"common_name=mrtc0.ssrf.in\" }}{{ .Data.private_key }}{{ end }}"
}
上記設定を example-vault-agent-config
という名前の configMap で保存します。
❯ kubectl create configmap example-vault-agent-config --from-file=./configs-k8s/
configmap/example-vault-agent-config created
続いて次のような Deployment を定義します。
apiVersion: v1
kind: Pod
metadata:
name: vault-agent-example
spec:
serviceAccountName: vault-auth
volumes:
- name: vault-token
emptyDir:
medium: Memory
- name: config
configMap:
name: example-vault-agent-config
items:
- key: vault-agent-config.hcl
path: vault-agent-config.hcl
- key: consul-template-config.hcl
path: consul-template-config.hcl
- name: shared-data
emptyDir: {}
initContainers:
# Vault container
- name: vault-agent-auth
image: vault
volumeMounts:
- name: config
mountPath: /etc/vault
- name: vault-token
mountPath: /home/vault
env:
- name: VAULT_ADDR
value: http://vault.vault.svc.cluster.local:8200
# Run the Vault agent
args:
[
"agent",
"-config=/etc/vault/vault-agent-config.hcl",
"-log-level=debug",
]
containers:
# Consul Template container
- name: consul-template
image: hashicorp/consul-template:alpine
imagePullPolicy: Always
volumeMounts:
- name: vault-token
mountPath: /home/vault
- name: config
mountPath: /etc/consul-template
- name: shared-data
mountPath: /etc/secrets
env:
- name: HOME
value: /home/vault
- name: VAULT_ADDR
value: http://vault.vault.svc.cluster.local:8200
# Consul-Template looks in $HOME/.vault-token, $VAULT_TOKEN, or -vault-token (via CLI)
args:
[
"-config=/etc/consul-template/consul-template-config.hcl",
"-log-level=debug",
]
# Nginx container
- name: nginx-container
image: mrtc0/vault-cert-auth-test-nginx
command: ["nginx", "-g", "daemon off;"]
ports:
- containerPort: 443
volumeMounts:
- name: shared-data
mountPath: /etc/nginx/vault/
initContainer で vault agent を動かしトークンを /home/vault
に保存しています。それから Sidecar の consul-template でそのトークンを使って Secret を取得しています。
これを適用すると無事、Vault から証明書を取得して nginx を起動し、クライアント証明書認証ができます。
❯ kubectl apply -f vault-agent-example-nginx.yml
❯ kubectl port-forward vault-agent-example 4443:443
❯ curl -k https://mrtc0.ssrf.in:4443
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.15.5</center>
</body>
</html>
# vault weite mrtc0/issue/mrtc0ssrfin common_name=mrtc0.ssrf.in format=pem
❯ curl -k --cert client.crt --key client.key https://mrtc0.ssrf.in:4443
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
References
- https://www.vaultproject.io/docs/platform/k8s/helm/run/
- https://www.vaultproject.io/docs/platform/k8s/injector/index.html
- https://www.vaultproject.io/api/auth/kubernetes/index.html
- https://learn.hashicorp.com/vault/developer/vault-agent-k8s
- https://repl.info/archives/2759/
- https://qiita.com/ryysud/items/ec8de49aa39a2f9fbceb