🗝️

Amazon EKS Kubernetes の シークレットを HashiCorp Vault で管理する

Amazon EKS でのシークレット管理を External Secret Operator と Vault の連携により行うための設定に関する備忘録です。

前置き

Awarefy では、バックエンドアプリを Amazon EKS Kubernetes(以下 k8s)上に展開してサービスを運用しています。

k8s のシークレット管理のプラクティスとしては諸説があるのですが、当社では External Secret Operator(以下、ESO) の Provider を AWS Secrets Manager に指定し運用していました。

https://external-secrets.io/v0.8.3/

また、当社ではかねてより Infrastructure as Code の取り組みとして AWS CDK(TypeScript)を利用しており、k8s の構築も CDK で行っています。

https://github.com/aws/aws-cdk

ESO と Secret Manager の連携は EKS を利用する場合のプラクティスの1つと言えますが、Secret Manager の取り回しの悪さもあり、シークレットの更新や追加の作業が開発者にとって負担となっていました。

そこで、ESO の利用を継続する前提で、ESO が対応している Provider のうち、Secret Manager 以外のものを探すことになり、紆余曲折を経て HashiCorp Vault を選定しました。

https://www.vaultproject.io/

Vault は高機能かつセキュアなシークレット管理用 Key-Value Store を提供するツールおよびサービスです。ESO は Vault をサポートします。

https://external-secrets.io/v0.8.3/provider/hashicorp-vault/

設定方法

以下に、設定手順を記します。今回 HashiCorp の提供する クラウドサービスとしての Vault を利用しています。Vault クラスターの構築手順は割愛します。

各種設定はセキュリティにも関わることから個別の内容は省略・または架空のものを設定しておりますので、そのまま実行しても環境構築が成功しない場合があることをご了承ください。

ServiceAccout と ClusterBinding の作成

Secret、ServiceAccount、ClusterRoleBinding の定義を用意します。

vault-service-account.yaml
apiVersion: v1
kind: Secret
metadata:
  name: vault-auth
  annotations:
    kubernetes.io/service-account.name: vault-auth
type: kubernetes.io/service-account-token
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: vault-auth
secrets:
  - name: vault-auth
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: k8s-auth-client-for-vault
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
  - kind: ServiceAccount
    name: vault-auth

これらは Vault の設定の前提となるため、先行して apply しておきます。

kubectl apply -f vault-service-account.yaml

Vault

ここから vault CLI を利用します。

はじめに環境変数に各種パラメータを設定。

export EKS_API_ENDPOINT=https://xxxxx.sk1.us-west-2.eks.amazonaws.com
export EKS_NAMESPACE=my-namespace

export VAULT_ADDR="https://xxxxxxx.hashicorp.cloud:8200"
export VAULT_NAMESPACE="admin"
export VAULT_SECRET_NAME="my-secret"
export VAULT_AUTH_NAME="my-auth"
export VAULT_POLICY_NAME="my-policy"
export VAULT_KUBERNETES_AUTH_NAME=kubernetes-${VAULT_AUTH_NAME}

k8s から必要な情報を取得し変数に入れておきます。

TOKEN_REVIEWER_JWT=$(kubectl get secret -o jsonpath='{.data.token}' vault-auth | base64 -d)
KUBERNETES_CA_CERT=$(kubectl config view -o jsonpath='{.clusters[].cluster.certificate-authority-data}' --minify=true --raw | base64 -d)

Vault Policy を作成します。

vault-policy.hcl
path "secret/data/my-secret" {
  capabilities = ["read", "list"]
}
vault policy write ${VAULT_POLICY_NAME} vault-policy.hcl

Vault Secret を作成(仮)しておきます。動作確認のために仮でもいいので入れておきましょう。

vault kv put secret/${VAULT_SECRET_NAME} dummy_key1=dummy_value1

Kubernetes Auth を設定します。

vault auth enable -path=${VAULT_KUBERNETES_AUTH_NAME} kubernetes

vault write auth/${VAULT_KUBERNETES_AUTH_NAME}/config \
token_reviewer_jwt=${TOKEN_REVIEWER_JWT} \
kubernetes_host=${EKS_API_ENDPOINT} \
kubernetes_ca_cert=${KUBERNETES_CA_CERT}

vault write auth/${VAULT_KUBERNETES_AUTH_NAME}/role/eso-vault \
bound_service_account_names=vault-auth \
bound_service_account_namespaces=${EKS_NAMESPACE} \
policies=${VAULT_POLICY_NAME}

External Secret 設定を設定します。

vault-external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: https:xxxxxx.hashicorp.cloud:8200
      namespace: admin
      path: secret
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes-my-auth
          role: eso-vault
          serviceAccountRef:
            name: vault-auth
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: vault-external-secret
spec:
  refreshInterval: "0"
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: vault-secret
  dataFrom:
    - extract:
        key: my-secret
kubectl apply -f vault-external-secret.yaml

動作確認

kubectl get describe secrets vault-secret

dummy_key1 の存在が確認できれば成功です。

失敗している場合、External Secret が同期エラーになっている可能性が高いです。

# 一覧 でステータスを確認
kubectl -n awarefy get ExternalSecret

# エラーを確認
kubectl describe ExternalSecret vault-external-secret

エラーといっても 403 エラーになった場合解決の手がかりはほとんど示されません(悲)。根気強くひとつずつ手順を確認しましょう。

はまりどころ

今見ても全体的に初見○し感のあるフローですが、Vault をセルフホスティングで実行しているか、HashiCorp のクラウドサービスを利用しているかで前提や仔細の設定がことなるため、記事を参照する場合、セルフホスト or クラウドサービスのどちらを対象にした情報なのかを見極める必要があります(そしてだいたいセルフホストだった)。

また、認証が上手くいかないとあらゆるリクエストで 403 が戻されるため(なお詳細は示されない模様)、トークンやパスなどの初歩的なミスの問題なのか、根本的に設定が間違っているのかの切り分けに窮することが多々ありました。

たとえば、external-secrets.io/v1beta1 の設定で Provider を指定する項目がありますが、namespace: admin のように namespace を明示しないと 403 となります。あやうく検証を挫折するところでした。

ちなみに Vault は非常に高機能で、今回の手順では 10% 程度しか機能を使っていないのではないだろうかと思うほどです。シークレットの更新方法やバージョニングについてなどはチームでよく共有のうえ、運用に載せるとよいでしょう。

付録

Q & A

  • Kubernetes Auth 以外の選択はないのでしょうか?
    • → あります。ESO の公式ドキュメントには、トークンを指定した方式が掲載されています。トークンの場合、トークン情報をコミットできないことから k8s Manifest ファイルが git 管理外となってしまうことを忌避して採用しませんでした。
  • なぜわざわざ Secret Manager から乗り換えたのですか?
    • → 前置きに書いたとおりですが、チームで運用するうえで細々とした不便が積み重なり、別解を探すに致しました。
  • Vault のセルフホスティング運用はありでしょうか?
    • → ありだと思います。k8s を利用しているのであれば helm で簡単に構築できそうです。

トークンの場合

Vault トークン認証の場合はこんな感じで、Vault から払い出したトークンを base64 エンコードして Secret にセットします。

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: https://xxxxxx.hashicorp.cloud:8200
      namespace: admin
      path: secret
      version: v2
      auth:
        tokenSecretRef:
          name: vault-token
          key: token
---
apiVersion: v1
kind: Secret
metadata:
  name: vault-token
data:
  token: xxxxxxxx=

参考文献

https://qiita.com/hsmto25519/items/2b9dab68a85052816e2e

参考というかこの記事がなければ完遂できませんでした。本当にありがとうございました。

ほか公式ドキュメント。

https://external-secrets.io/v0.8.3/provider/hashicorp-vault/

https://developer.hashicorp.com/vault/tutorials/getting-started

Awarefy 技術ブログ

Discussion