Vaultのデータ(kv)をExternalSecretを使ってEKSでSecretとして読み込む

## External Secretとは
[External Secret](https://external-secrets.io/main/)とはExternal Secret Operatorによって提供される機能で、Kubernetes外にあるSecret(例：AWS Secret Manager, Vault等)を読み取ってKubernetesのSecretとしてクラスタにSecretを挿入してくれる。
提供されるカスタムリソースとしては、ExternalSecretとSecretStoreがあり、ExternalSecretは外部Secretとクラスタ内Secretの関係性を定義し、SecretStoreは外部Secretからどのように取得するかを指定する。
![1654482450879.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/ba106974-2912-537b-5b2d-8030f2ecc5d1.png)
※画像は[こちら](https://external-secrets.io/v0.6.0-rc1/overview/)から引用

通常EKSでExternalSecretを使う場合はSecretManagerから値を引っ張ることが多いが、今回はVaultから引っ張った際の検証メモ。

## External Secret Operatorのインストール
[オフィシャルサイトのGetting Started](https://github.com/external-secrets/external-secrets/blob/main/docs/guides/getting-started.md)に従い、External Secret Operatorをインストールする。


```
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
   external-secrets/external-secrets \
    -n external-secrets \
    --create-namespace \
    --set installCRDs=true
```

## Vaultのインストール
Vaultもインストールする。
詳細は[オフィシャル手順](https://www.vaultproject.io/docs/platform/k8s/helm)にお任せして、ここではやった手順をメモ程度に残しておく。
```
helm repo add hashicorp https://helm.releases.hashicorp.com
helm show values hashicorp/vault > vault-values.yaml
helm upgrade -i -f vault-values.yaml vault -n vault --create-namespace hashicorp/vault
```
vault-values.yamlはIngressを使うよう書き換えている。
Vault起動後にunseal作業を行う。unseal作業を実施しないと、Podが以下のエラーを吐き続けて起動が完了しない。
```
2022-08-04T03:28:40.124Z [INFO]  core: seal configuration missing, not initialized
2022-08-04T03:28:45.090Z [INFO]  core: security barrier not initialized
```
unsealのためのkeyやTokenを`vault operator init`で取得する。
```
kubectl exec -ti vault-0 -n vault -- vault operator init
```
この時のInitial Root Tokenは色々使うので残しておくこと。
unseal keyを使ってunsealを行う。
```
kubectl exec -ti vault-0 -n vault -- vault operator unseal
```
3回unseal作業を行うと、以下のような出力が得られてVaultが使えるようになる。
```
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.10.3
Storage Type    file
Cluster Name    vault-cluster-44f5b753
Cluster ID      d4b134d3-6888-37a1-dba4-3b7063a61a80
HA Enabled      false
```

## Vaultの設定
ここではVault内にサンプルデータを投入し、またアクセスのためのポリシーを作成する。\
ここの記述は基本的には以下の情報を参考にした。
- https://www.vaultproject.io/docs/auth/kubernetes
- https://learn.hashicorp.com/tutorials/vault/agent-kubernetes

ここからはHelmのVaultのIngressに対してローカルPCからvaultコマンドでアクセスする。
```sh
export VAULT_ADDR=https://vault.mydomain.info
vault login
```
デフォルトではKeyValueエンジンが有効化されていなかったので、有効化する。
```
vault secrets enable kv
```
KeyとValueのサンプルデータを投入する。
```
vault kv put kv/mysecret password=himitsu
```
確認する。
```
$ vault kv get kv/mysecret
====== Data ======
Key         Value
---         -----
password    himitsu
```
次にポリシーを作成する。
```
cat <<EOF > /tmp/vault_sample_policy.hcl
path "*" {
    capabilities = ["read", "list"]
}
EOF
```
ポリシーを割り当てる。ポリシーの名前はTutorialに従って`myapp-kv-ro`とした。
```
vault policy write myapp-kv-ro /tmp/vault_sample_policy.hcl
```

Kubernetes内のリソースを触るServiceAccountを作成する。今回はTutorialに従って`default`のNamespaceに作成した。権限周りでトラシュすることがあったのでcluster-adminにしているが、もっと絞れるとは思う。
```yaml
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: vault-auth
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: role-tokenreview-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: vault-auth
  namespace: default
EOF
```

Kubernetesの認証を有効化し、認証情報を追加する。

```sh
vault auth enable kubernetes
export SA_SECRET_NAME=$(kubectl get secrets --output=json \
    | jq -r '.items[].metadata | select(.name|startswith("vault-auth-")).name')
export SA_JWT_TOKEN=$(kubectl get secret $SA_SECRET_NAME \
    --output 'go-template={{ .data.token }}' | base64 --decode)
export SA_CA_CRT=$(kubectl config view --raw --minify --flatten \
    --output 'jsonpath={.clusters[].cluster.certificate-authority-data}' | base64 --decode)
export K8S_HOST=$(kubectl config view --raw --minify --flatten \
    --output 'jsonpath={.clusters[].cluster.server}')
vault write auth/kubernetes/config \
     token_reviewer_jwt="$SA_JWT_TOKEN" \
     kubernetes_host="$K8S_HOST" \
     kubernetes_ca_cert="$SA_CA_CRT" \
     issuer="https://kubernetes.default.svc.cluster.local"
```
問題なければ、`vault write auth/kubernetes/config`の結果として以下のメッセージが得られるはずだ。
```
Success! Data written to: auth/kubernetes/config
```
次にロールを作成する。ロール名は`hoge`とした。
```
vault write auth/kubernetes/role/hoge \
     bound_service_account_names=vault-auth \
     bound_service_account_namespaces=default \
     policies=myapp-kv-ro \
     ttl=24h
```
なお、ここで出てくるServiceAccountの名前やNamespace、ポリシー名を変えている場合は適宜変更して実行すること。
```
vault write auth/kubernetes/login role=hoge jwt=$SA_JWT_TOKEN iss=https://kubernetes.default.svc.cluster.local
```

## VaultのSecretをEKSに展開する

Vaultとの接続には[SecretStoreリソース](https://external-secrets.io/v0.6.0-rc1/api/secretstore/)を利用する。
```yaml
cat << EOF | kubectl apply -f -
 apiVersion: external-secrets.io/v1beta1
 kind: SecretStore
 metadata:
   name: vault-backend
 spec:
   provider:
     vault:
       server: "https://vault.mydomain.info"
       path: "kv"
       version: "v1"
       namespace: "default"
       auth:
         kubernetes:
           mountPath: "kubernetes"
           role: "hoge"
           serviceAccountRef:
             name: "vault-auth"
EOF
```
なお、versionを"v2"にすると、ExternalSecretを作ると以下のようなエラーが出てSecretが作成されないことがある。自分も遭遇して少しハマった。
```
"cannot read secret data from Vault: Error making API request.\n\nNamespace: default\nURL: GET https://vault.mydomain.info/v1/kv/data/mysecret\nCode: 404.
```
そのため、[こちら](https://www.goglides.dev/bkpandey/how-to-manage-secrets-in-openshiftkubernetes-using-vault-and-external-secrets-1n6k)の記事を参考に、v1に変更して回避している。

※追記：\
**KVをGUIから作成するとv2となって、CLIから作成するとv2となる模様。GUIで作成した人はv2もしくは指定なしにする必要あり。**

状態を確認する。
```
$ kubectl get secretstore
NAME            AGE   STATUS
vault-backend   8s    Valid
```
なお、設定ミス等があると、以下のようになる。
```
$ kubectl get secretstore.external-secrets.io/vault-backend
NAME            AGE     STATUS
vault-backend   9m50s   InvalidProviderConfig
```
この場合、例えばServiceAccountを設定し忘れた場合はexternal-secretsのPod内に`Code: 500. Errors:\n\n* service account name not authorized`
のようなエラーが出ているため、エラーを見て何が起きているか確認すること。

次にExternalSecretで実際にKubernetesのSecretリソースとして取り込む。
```yaml
cat <<EOF | kubectl apply -f -
 apiVersion: external-secrets.io/v1beta1
 kind: ExternalSecret
 metadata:
   name: vault-example
 spec:
   secretStoreRef:
     name: vault-backend
     kind: SecretStore
   target:
     name: vault-example-secret
   data:
   - secretKey: fuga
     remoteRef:
       key: mysecret
       property: password
EOF
```
`spec.target.name`は作成されるSecret名、`spec.data.secretKey`は作成されるSecret内のKeyで`spec.data.remoteRef`でVault側のパス(key部分、SecretStoreで指定したpathを取り除いたもの)とKey-Valueのkey(property)を設定する。

上記リソース作成後、statusが`SecretSynced`になっていればOK.
```
$ kubectl get externalsecret
NAME            STORE           REFRESH INTERVAL   STATUS
vault-example   vault-backend   1h                 SecretSynced
```

Secretが作成されて、Vaultから値が引っ張ってこれていることが分かる。
```
$ kubectl get secret vault-example-secret -o jsonpath={.data.fuga} | base64 -d
himitsu
```