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 の CREATEUPDATE イベントをインターセプトして、 [vault.hashicorp.com/agent-inject:](http://vault.hashicorp.com/agent-inject:) true という Annotation があれば Vault から Secret を取得するための Vault Agent コンテナを InitContainer として追加する Mutation Webhook Controller です。

これは Annotation で実現することもできますし、Vault Agent の設定ファイルを用いることでも可能です。

https://d33wubrfki0l68.cloudfront.net/973cb1e1642fae194c65714f8c15998060ed8881/7e51a/img/vault-agent-k8s-4.png

ref : https://d33wubrfki0l68.cloudfront.net/973cb1e1642fae194c65714f8c15998060ed8881/7e51a/img/vault-agent-k8s-4.png

この図は sidecar として consul-template がありますが、こういうことが実現できます。

  1. initContainer である vault-agent-auth が Vault の Kubernetes Auth Method でログイン
  2. Vault のトークンを取得
  3. 共有ボリュームに Vault のトークンを書き込む
  4. 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