kubernetes 上に HashiCorp vault を構築して機能を試す
はじめに
Vault は Terraform で有名な HashiCorp が提供する Secret Management 用のサービスであり、各サービスに使う認証情報や任意の key-value など種々の機密情報を一元管理することができます。
AWS の Secret Manager や GCP の Secret Manager など各クラウドプロバイダーで機密情報を管理するためのマネージドサービスがありますが、それと同じカテゴリのサービスという位置付けになっています。各種パッケージマネージャーや docker イメージ、helm など様々な方法で構築できるため(インストールページを参照)、今回は helm で k8s クラスタ上にインストールします。helm でインストールできる vault 関連のコンポーネントでは通常の vault server の他に k8s secret と連動可能な機能があるため、そちらについても実際に使ってみます。
セットアップ
- インストール手順: Run Vault on kubernetes
helm で vault をインストールする際はさまざまな項目をカスタマイズできますが、はじめはデフォルト設定でインストールします。設定可能な項目は Configuration を参照。
また、helm では vault の構成について以下の 4 つのモードを選択できます。
- Dev: 開発用のモード。データはメモリ上に保存され、永続化されない。
- StandAlone: 単一の pod で稼働。データは persistentVolume に保存、永続化される。
- HA: 複数の pod で稼働する High Availability 構成。
- External: 外部にある vault server を利用する構成。
今回はデフォルト設定の StandAlone モードで作成します。また pod を起動する際に persistedVolume が必要になるので事前に作成しておきます (今回は openEBS による dynamic provisioner で対応)。
helm でレポジトリを追加し、hashicorp/vault
をインストール。
$ helm repo add hashicorp https://helm.releases.hashicorp.com
$ helm install vault hashicorp/vault
インストールにより以下のような pod, service が作成されます。
$ kubectl get pod,svc
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 Running 0 58s
pod/vault-agent-injector-55748c487f-hjxrj 1/1 Running 0 59s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault ClusterIP 10.97.192.100 <none> 8200/TCP,8201/TCP 59s
service/vault-agent-injector-svc ClusterIP 10.110.234.104 <none> 443/TCP 59s
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 59s
service/vault-ui ClusterIP 10.111.79.217 <none> 8200/TCP 10d
Initialize と unseal
インストールが成功すると vault-0
pod が起動しますが、この時点ではまだ Ready にはなりません。上記の 4 つのモードのうち Dev 以外の構成では vault server は seal 状態で起動します。seal は「封をする」という意味ですが、文字通りこのままではほとんどの操作を行えないため unseal
という操作を実行する必要があります。
CLI initialize and unseal に従って 初期化処理と unseal を実行します。
まずは vault-0 pod 内で vault operator init
を実行します。
$ kubectl exec -ti vault-0 -- vault operator init
Unseal Key 1: C0eR4peV1QvcCVl5OCG9i6lkEr4/wlYIfIZs/YaGYe2F
Unseal Key 2: QZUUlu9Gdir9bC+qT2cBMZJqIvfhqmglv24/iKlsUKdv
Unseal Key 3: X1SN6V/DoPWmQjG9T4irbbmfWBX1JqOHXW8wAS92pm5w
Unseal Key 4: vXoXviXzA0MsBZKuFk0Cjg3uqzqUC4x8kharKMFcjE49
Unseal Key 5: 2gFG5OCZ4pdn2YDwKgIG5OvZ4I8omsa9TGMO6aHg3fQP
Initial Root Token: hvs.pmlUwjaoZKR3tQTUU0GnBnOV
Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.
Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!
It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.
ここで表示されるメッセージにも記載されているように、上記 5 つの Unseal Key
と Initial Root Token
は vault における重要な key と token となっています。詳しくは以下を参照。
これらの値は今後も使うのでどこかにメモしておきます。
次に上記の Unseal key を使って unseal を実行します。pod 内で vault operator unseal
を実行すると unseal key の入力を求められるので、出力された unseal key のいずれかの値を入力します。
$ kubectl exec -ti vault-0 -- vault operator unseal
Unseal Key (will be hidden): # Unseal key 1 を入力
key の値を重複しないように 3 回繰り返すと unseal が成功し、pod が Ready 状態となります。これで vault server を使うための準備が完了し、secret の読み書きが行えるようになります。
vault CLI のインストール
vault pod 内では vault CLI がインストール済みのため vault
コマンドが実行できますが、pod 外でもコマンドが実行できたほうが便利なので vault CLI をノード等にインストールしておきます。Install Vault から使用している OS, arch に合ったものをインストールします。例えば amd64 用のバイナリをインストールする場合は以下。
$ wget https://releases.hashicorp.com/vault/1.15.6/vault_1.15.6_linux_amd64.zip
$ unzip vault_1.15.6_linux_amd64.zip
$ sudo mv vault /usr/local/bin
$ rm vault_1.15.6_linux_amd64.zip
$ vault version
Vault v1.15.6 (615cf6f1dce9aa91bc2035ce33b9f689952218f0), built 2024-02-28T17:07:34Z
Completion は以下を実行した後シェルを再起動することで有効化できます。参考
vault -autocomplete-install
vault server に接続するには接続先をコマンド実行時のオプションか環境変数 VAULT_ADDR
に指定します。
vault pod に接続するための k8s service は ClusterIP として作成されるため、k8s クラスタ内から接続する場合はこの CLUSTER-IP を指定できます (クラスタ外部から接続する場合は ingress の設定が必要)。
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
vault ClusterIP 10.111.155.187 <none> 8200/TCP,8201/TCP 4d23h
また、各種コマンドの実行には vault の認証が必要になります。これに関しては様々な方法がありますが、手っ取り早く行うには initialize 時の root token を使うことができます。
環境変数を使う場合は VAULT_ADDR
にアドレス、VAULT_TOKEN
にトークンの値を指定します。
export VAULT_ADDR="http://10.111.155.187:8200"
export VAULT_TOKEN="hvs.pmlUwjaoZKR3tQTUU0GnBnOV"
これにより vault server に対して接続・認証が成功し、secret の読み書き等の各種コマンドが実行できるようになります。
vault k8s の機能を試す
上記で k8s クラスタ上に vault server を構築できました。
ingress を作成してクラスタ外部からもアクセスできるようにすれば、特に k8s 上で動作していることを意識せずに secret の書き込みや参照が実行できます。
vault 自体の使い方は検索するといろいろ記事が出てくるので、ここでは k8s vault 独自の機能をいくつか試してみます。
Vault CSI provider
Vault CSI provider は secrets-store-csi-driver を利用した vault の機能の 1 つであり、事前に設定は必要ですが pod 作成時に vault server から secret を動的に取得して pod 内で参照することができます。
hashicorp の以下のページに使い方が記載されているため、こちらを参考に実際に使ってみます。
- https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-secret-store-driver
- https://developer.hashicorp.com/vault/docs/auth/kubernetes
hashicorp-japan による日本語の説明も以下にあります。
事前準備
vault CSI provider を使うには secrets-store-csi-driver が必要になるので、helm を使ってインストールします。
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver --namespace kube-system
vault CSI provider は vault を helm でインストールする際に --set "csi.enabled=true"
をつけることで追加されます。
helm install vault hashicorp/vault --set "csi.enabled=true"
インストールが完了すると vault-csi-provider の pod (daemonset) が起動します。
vault-0 1/1 Running 0 20h
vault-csi-provider-k92cr 2/2 Running 0 20h
vault 側の準備
pod 内で vault 側の secret を参照する動作を確認するため、適当な secret を vault server に書き込んでおきます。参照する secret は何でも良いので、ここでは kv v2 タイプでdatabase/secret
というパスに username, password の key-value を書き込みます。
# kv v2 secret engine を有効化
$ vault secrets enable -path=secret kv-v2
# 書き込み
$ vault kv put -mount=secret database/secret username=myusername password=mypassword
# 参照
$ vault kv get -mount=secret database/secret
======= Secret Path =======
secret/data/database/secret
...
====== Data ======
Key Value
--- -----
password mypassword
username myusername
vault CSI provider では vault の kubernetes 認証 を使って認証を行うため、以下のコマンドで有効化します。
$ vault auth enable kubernetes
vault 側で k8s ホストを設定するため、vault pod 内で以下のコマンドを実行します
(pod 内でないと KUBERNETES_PORT_443_TCP_ADDR
の環境変数が設定されていないため)。
# pod でシェル起動
$ kubectl exec -it vault-0 -- sh
# vault server に接続するために環境変数を設定
/ $ export VAULT_ADDR="http://vault.vault:8200"
/ $ export VAULT_TOKEN="[root_token の値]"
# kubernetes 認証の k8s ホストを設定
/ $ vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
read して kubernetes_host
に ip address が設定されていれば ok
/$ vault read auth/kubernetes/config
Key Value
--- -----
disable_iss_validation true
disable_local_ca_jwt false
issuer n/a
kubernetes_ca_cert n/a
kubernetes_host https://10.96.0.1:443
pem_keys []
また、kubernetes 認証時に使用される vault 側の role と policy を作成します。
policy では上記で作成した kv v2 のパス secret/data/database/secret
を read できる権限を設定します。
$ vault policy write db-policy - <<EOF
path "secret/data/database/*" {
capabilities = ["read"]
}
EOF
role は k8s 側の serviceAccount (SA) にマッピングする形で作成します。SA 名はこの時点では何でもいいですが、ここで指定した値を後ほど k8s 側で作成して使用します (namespace も同様)。policies には上記で作成した db-policy
を指定し、database
という名前の role に結びつけます。
- vault 側 role 名: database
- k8s 側 ServiceAccount: db
- k8s 側 namespace : default
$ vault write auth/kubernetes/role/database \
bound_service_account_names=db \
bound_service_account_namespaces=default \
policies=db-policy \
ttl=20m
k8s 側の準備
k8s 側では vault から secret を取得するためのカスタムリソース SecretProviderClass
を作成します。
マニフェストでは spec.parameters
に以下の項目を指定します。
- vaultAddress: vault server のアドレス。vault server は namespace
vault
上にvault
という service 名で公開されているので、http://vault.vault:8200
を指定すればアクセスできる。 - roleName: vault server 側で作成した kubernetes の role 名。上記の例では
database
- objects: vault 側から取得する secret を list 形式で指定
- secretPath: vault server 上の対象 secret までのパス
- secretKey: secret 内の取得する key 名
- objectName: 取得した secret を格納する key 名。pod 内ではここで指定したファイルの中に value が書き込まれる。
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: db-secret-class
spec:
provider: vault
parameters:
vaultAddress: "http://vault.vault:8200"
roleName: database
objects: |
- objectName: "obj-username"
secretPath: "secret/data/database/secret"
secretKey: "username"
- objectName: "obj-password"
secretPath: "secret/data/database/secret"
secretKey: "password"
また、上記を実行するための serviceAccount を作成
kubectl create sa db
次に実際に secret を参照する pod を作成します。
上記で作成した secretProviderClass
オブジェクトを pod 内で参照するには spec.volumes
で定義します。secretProviderClass には上記で作成したリソース名 db-secret-class
を指定します。
volumes:
- name: vault-db-creds
csi:
driver: 'secrets-store.csi.k8s.io'
readOnly: true
volumeAttributes:
secretProviderClass: db-secret-class
これを POD 内の特定のコンテナにマウントするために spec.containers[].volumeMounts
を指定します。
name には上記で指定した volumes.name
と同じものを指定し、mountPath にはコンテナ内で mount するパスを指定します。
spec:
containers:
volumeMounts:
- name: vault-db-creds
mountPath: '/mnt/secrets-store'
readOnly: true
今回は検証のためイメージは適当に busybox を使用します。最終的なマニフェストは以下。
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
labels:
app: demo
spec:
selector:
matchLabels:
app: demo
replicas: 1
template:
metadata:
annotations:
labels:
app: demo
spec:
serviceAccountName: db
containers:
- name: app
image: busybox:1.29
command:
- "/bin/sleep"
- "10000"
volumeMounts:
- name: vault-db-creds
mountPath: '/mnt/secrets-store'
readOnly: true
volumes:
- name: vault-db-creds
csi:
driver: 'secrets-store.csi.k8s.io'
readOnly: true
volumeAttributes:
secretProviderClass: db-secret-class
動作確認
上記で準備した pod を作成すると pod 内コンテナが起動するタイミングで vault server への認証が行われ、secret の取得 ~ pod 内のマウントが行われます。
pod 内でシェルを起動して volumeMounts.mountPath
に指定したパスを確認すると、SecretProviderClass
において指定した secret 内の key がシンボリックリンクとしてマウントされていることが確認できます。ファイル名は objectName
に指定した名前にマッピングされており、各々のファイルの中身に key に対応する value が書き込まれています。
# Pod 内でシェル起動
$ kubectl exec -it app-75fbd57ff8-pz482 -- sh
/ # ls -l /mnt/secrets-store/
total 0
lrwxrwxrwx 1 root root 15 Mar 20 08:26 obj-password -> ..data/obj-password
lrwxrwxrwx 1 root root 15 Mar 20 08:26 obj-username -> ..data/obj-username
/ # cat /mnt/secrets-store/username
myusername
/ # cat /mnt/secrets-store/password
mypassword
このようにして vault CSI provider を使うことで vault 側の secret を pod 内で参照することができます。
ちなみに、 vault 側で secret の値を変更した際に pod 内の値も変更されるか見ておきます。
vault 側の value をそれぞれ myusername2
, mypassword2
に更新します。
$ vault kv put -mount=secret database/secret username=myusername2 password=mypassword2
pod 内を見ても値は変更されていません。
/ # cat /mnt/secrets-store/username
myusername
/ # cat /mnt/secrets-store/password
mypassword
前述の通り secret は pod 作成時に vault server から取得する仕様であるため、vault 側の secret を変更してもリアルタイムで更新が反映されるわけではなく pod の再作成が必要となります。
既存の pod を削除した後、再度値を確認すると更新した値になっていることが確認できます。
# Pod を削除.
$ kubectl delete pod app-75fbd57ff8-pz482
pod "app-75fbd57ff8-pz482" deleted
# deployment により別 pod が起動するのでシェル起動
$ kubectl exec -it app-75fbd57ff8-z9927 -- sh
/ #
# 確認
/ # cat /mnt/secrets-store/username
myusername2
Secret の使用状況
SecretProviderClass
で作成された secret がいずれかの pod で使用されているときは secretproviderclasspodstatuses
リソースが作成されます。これは SecretProviderClass
リソースがいずれかの pod で参照されているときのみ動的に作成されるリソースとなっており、どの pod からも参照されていない場合には自動的に削除されます。
例えば、上記の例では app-75fbd57ff8-vxclr
という pod で db-secret-class
という SecretProviderClass を参照しているため、以下のリソースが作成されています。
(リソース名は [pod-name]-[namespace]-[SecretProviderClass-name]
となるっぽい)。
$ kubectl get secretproviderclasspodstatuses.secrets-store.csi.x-k8s.io
NAME AGE
app-75fbd57ff8-vxclr-default-db-secret-class 89s
kubectl describe
で内容を見ると、実際に secret がどのような key 名で pod にマウントされているかが確認できます。
$ kubectl describe secretproviderclasspodstatuses.secrets-store.csi.x-k8s.io
...
Status:
Mounted: true
Objects:
Id: obj-password
Version: CyNGjiAyP1qYpoZcgGU-suc3FMYduH64LU-Pwy2X9MQ=
Id: obj-username
Version: uoQc8U20pD_ebH3RTbcZgmyWuDbSTQ_9MRXU3daavOM=
Pod Name: app-75fbd57ff8-vxclr
Secret Provider Class Name: db-secret-class
Target Path: /var/lib/kubelet/pods/b3193227-239d-437f-b73d-a40fc13d6a35/volumes/kubernetes.io~csi/vault-db-creds/mount
Events: <none>
SecretProviderClass を参照する pod がなくなると secretproviderclasspodstatuses も自動で削除されます。
$ kubectl delete deployments.apps app
deployment.apps "app" deleted
$ kubectl get secretproviderclasspodstatuses.secrets-store.csi.x-k8s.io 1 ↵
No resources found in default namespace.
ちなみに上記の Target Path
に表示されているパスは実際に secret がマウントされているノード上のパスを表しています。
POD が起動しているノード上の上記のパスを確認すると、pod 内で確認したのと同じ secret がマウントされていることが確認できます。
$ pwd
/var/lib/kubelet/pods/b3193227-239d-437f-b73d-a40fc13d6a35/volumes/kubernetes.io~csi/vault-db-creds/mount
$ ls -l
total 0
lrwxrwxrwx 1 root root 19 Mar 20 08:26 obj-password -> ..data/obj-password
lrwxrwxrwx 1 root root 19 Mar 20 08:26 obj-username -> ..data/obj-username
$ cat obj-username
myusername2
エラーの確認
マニフェストの設定などを間違えていて vault server から正常に secret が取得できない場合、 pod 作成時に status が ContainerCreating
のままになります。
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
app-75fbd57ff8-hm787 0/1 ContainerCreating 0 13s
describe pod でエラー内容を確認できます。
$ kubectl describe pod app-75fbd57ff8-hm787
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 99s default-scheduler Successfully assigned vault/app-75fbd57ff8-hm787 to k8s-w1
Warning FailedMount 36s (x8 over 100s) kubelet MountVolume.SetUp failed for volume "vault-db-creds" : rpc error: code = Unknown desc = failed to mount secrets store objects for pod vault/app-75fbd57ff8-hm787, err: rpc error: code = Unknown desc = error making mount request: couldn't read secret "username": failed to login: Error making API request.
URL: POST http://vault.vault:8200/v1/auth/kubernetes/login
Code: 400. Errors:
* invalid role name "db"
上記の例では * invalid role name "db"
となっています。これは SecretProviderClass
オブジェクト内では rolename が db と指定されていますが、vault server 内で指定した rolename は database
であることから、vault server へのログインに失敗したことがわかります。
なお、vault-csi-provider pod の vault-csi-provider
コンテナのログからも同様のエラーメッセージが確認できます。
$ kubectl logs vault-csi-provider-k92cr vault-csi-provider
2024-03-20T08:22:18.838Z [INFO] server: Finished unary gRPC call: grpc.method=/v1alpha1.CSIDriverProvider/Mount grpc.time=9.530387ms grpc.code=Unknown
err=
| error making mount request: couldn't read secret "username": failed to login: Error making API request.
|
| URL: POST http://vault.vault:8200/v1/auth/kubernetes/login
| Code: 400. Errors:
|
| * invalid role name "db"
そのため、うまく行かない場合はこれらのメッセージを確認すると良いでしょう。
Vault Agent Injector
Vault Agent Injector は secret を参照する pod に annotation をつけておくと、pod 作成時に Agent injector が検出して secret を含む sidecar コンテナを pod 内に inject するという仕組みです(よくある sidecar injection 型の仕組み)。
機能的には CSI Provider と同じなのでここでは割愛します。両者の比較については以下を参照。
Vault Secrets Operator
Vault Secrets Operator は k8s でよくある Operator を利用し、カスタムリソースを使って vault server に保存された各 secret を k8s の secret に同期する機能になっています。詳しい説明は以下を参照。
こちらも上記ドキュメントに使用手順が記載されているので試してみます。
セットアップ
Vault Secrets Operator も helm でインストール可能ですが、vault server とは別の chart hashicorp/vault-secrets-operator
を使います。
helm install --version 0.5.2 --create-namespace --namespace vault-secrets-operator vault-secrets-operator hashicorp/vault-secrets-operator
インストールが完了すると operator pod が起動します。
$ kubectl get pod -n vault-secrets-operator
NAME READY STATUS RESTARTS AGE
vault-secrets-operator-controller-manager-7d48875c77-fmftl 2/2 Running 0 30h
Vault Secrets Operator では vault server への認証や取得する secret はカスタムリソースで定義していきます。
まずはじめに接続先の vault server を記述する VaultConnection
カスタムリソースを作成します。ここでの接続先は同じクラスタ内の vault service になるので、vault svc を spec.address
に指定すれば ok です。
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: vault-connection
spec:
address: http://vault.vault.svc.cluster.local:8200
次に vault server に認証を行うためのカスタムリソース VaultAuth
を作成します。Supported Vault authentication methods にあるように kubernetes 認証の他に以下のような認証が使用できます。
- kubernetes
- JWT
- AppRole
ドキュメントでは kubernetes 認証の設定が記載されていますが、せっかくなのでここでは appRole を使って認証するように設定します。
認証に使う appRole を k8s-vault-op
として事前に vautl server 上に作成しておきます。secret は secret/data/k8s/testdata/secret
に kv v2 として作成し、これを読み取れるように policy を設定します。
$ cat policy.yml
path "secret/data/k8s/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# appRole 用の policy 作成
$ vault policy write k8s-vault-op policy.yml
# appRole 作成
$ vault write auth/approle/role/k8s-vault-op policies="k8s-vault-op"
# Role id の取得
$ vault read auth/approle/role/k8s-vault-op/role-id
# secret id を取得
$ vault write -f auth/approle/role/k8s-vault-op/secret-id
次に上記の appRole を使って認証を行う VaultAuth カスタムリソースを作成します。kubernetes 認証を使う際の例が VaultAuth custom resource に記載されているので、これを元に appRole 用の VaultAuth に設定するプロパティを見ていきます。
API Reference の VaultAuthSpec を参照すると appRole の場合は appRole
フィールドに VaultAuthConfigAppRole を指定し、この中に roleId の値と secretId を格納した secretRef を指定すれば良いことがわかります。そのため、上記で取得した secret id を記載した k8s secret k8s-vault-op
を作成します。
apiVersion: v1
kind: Secret
metadata:
name: k8s-vault-op
type: Opaque
stringData:
id: 3a145c1e-54ed-b841-2d59-f2eb1839e539 # secret ID
$ kubectl apply -f secret-id.yml
VaultAuth のマニフェストには appRole
以下に roleId を指定し、secretRef には上記の k8s-vault-op
を指定。
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
spec:
vaultConnectionRef: vault-connection
method: appRole
mount: approle
appRole:
roleId: e7a69056-729a-5448-ca6b-5c2b39035ee9
secretRef: k8s-vault-op
リソース作成後、vaultauths
を describe することで認証に成功したか確認できます。
$ kubectl describe vaultauths.secrets.hashicorp.com vault-auth
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Accepted 3s VaultAuth Successfully handled VaultAuth resource request
これで一連の準備ができたので、最後に vault server 側で以下の検証に使用する kv v2 の値を書き込んでおきます。
$ vault kv put -mount=secret k8s/testdata/secret key1=value1
========= Secret Path =========
secret/data/k8s/testdata/secret
StaticSecret の作成
vault 側の secret を k8s 側に同期するには VaultStaticSecret カスタムリソース を作成します。spec に指定する内容は以下。
- vaultAuthRef: vault server への認証に使用する vaultAuth カスタムリソース名を指定。
- mount: vault 側の mount を指定。secret 作成時に
-mount=secret
を指定したためここでも secret を指定。 - type: vault 側の type を指定。secret 作成時に kv v2 で書き込んだため
kv-v2
を指定。 - path: vault 側の secret までの path を指定。
- refreshAfter: secret が同期される周期?(ドキュメントの定義を見てもよくわからなかった)
- destination: vault 側から取得した値を保存する k8s 側の secret に関する情報
- create: true にした場合、secret がない場合に新規作成する。
- name: k8s secret 名
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: vault-static-secret-v2
spec:
vaultAuthRef: vault-auth
mount: secret
type: kv-v2
path: k8s/testdata/secret
refreshAfter: 60s
destination:
create: true
name: kv-secret
リソース作成後 vault 側から正常に secret が取得できると、vaultstaticsecrets
リソースの Event に Secret synced
が記録されます。
$ kubectl describe vaultstaticsecrets.secrets.hashicorp.com vault-static-secret-v2
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SecretSynced 10s VaultStaticSecret Secret synced
Normal SecretRotated 10s VaultStaticSecret Secret synced
これによりマニフェスト内で指定した名前の secret が作成されます。
$ kubectl get secret kv-secret
NAME TYPE DATA AGE
kv-secret Opaque 2 35s
vault 側では key1: value1
という key-value を書き込みましたが、secret 内では data 以下に key が格納され、value は base64 encode されて書き込まれます。
$ kubectl get secret kv-secret -o yaml
apiVersion: v1
data:
_raw: eyJkYXRhIjp7ImtleTEiOiJ2YWx1ZTEifSwibWV0YWRhdGEiOnsiY3JlYXRlZF90aW1lIjoiMjAyNC0wMy0yMVQwOTo1NTo0NC4zMjgxOTIyMTFaIiwiY3VzdG9tX21ldGFkYXRhIjpudWxsLCJkZWxldGlvbl90aW1lIjoiIiwiZGVzdHJveWVkIjpmYWxzZSwidmVyc2lvbiI6MX19
key1: dmFsdWUx
kind: Secret
...
$ kubectl get secret kv-secret -o yaml | yq -r ".data.key1" | base64 -d
value1
上記のように作成された secret は通常の secret と同じように扱えるため、pod 側でマウント等を行うことで参照できます。
また、 k8s 側で secret の値を value2
に書き換えてみます。
$ echo -n "value2" | base64
dmFsdWUy
# base64 encode した値を secret に書き込み
$ kubectl edit secret kv-secret
secret/kv-secret edited
# 値の取得
$ kubectl get secret kv-secret -o yaml | yq -r ".data.key1" | base64 -d
value2%
変更した直後は value2
となっていますが、少し待ってから再度 key1 の値を取得すると value1
となっています。
$ kubectl get secret kv-secret -o yaml | yq -r ".data.key1" | base64 -d
value1%
k8s 側の secret は vault server 側と定期的に同期されているため、k8s secret 側の値が変更されていても同期のタイミングで vault server 側の値に戻されます。同期が完了するタイミングで VaultStaticSecret
のイベントで SecretRotated
が出力されます。
Events
Normal SecretRotated 15s (x4 over 7m51s) VaultStaticSecret Secret synced
ただあくまで vault → k8s で定期的に同期されているだけなので、k8s 側の値を変えたからといって vault 側の値が変更されることはありません。そのため同期というよりは上記の通り定期的に secret が rotate されるといった表現の方が適切かもしれません。
もちろん k8s secret 側の値を変更した直後でも vault server 側の値が変わってないことが確認できます。
# base64 encode した値を secret に書き込み
$ kubectl edit secret kv-secret
secret/kv-secret edited
# 直後に vault server 側の secret を確認しても value 1 のまま。
$ vault kv get -mount=secret k8s/testdata/secret
========= Secret Path =========
secret/data/k8s/testdata/secret
==== Data ====
Key Value
--- -----
key1 value1
StaticSecret のカスタムリソースで作成された secret は定期的に rotate されるため、k8s 側で不意の操作により secret の値が書き換わった結果、想定しない動作が発生するといった事故を防ぐことができます。
DynamicSecret の作成
StaticSecret の他に DynamicSecret に対応するカスタムリソースを作成することもできます。 StaticSecret は上記で見たように事前に Vault server に書き込んである値を参照する際に使用しますが、DynamicSecret はリクエストに基づいて動的に作成され、有効期限を持つような資格情報などを参照する際に使用します。ドキュメントの例では AWS Secret を使う例が記載されているのでこちらを試してみます。
この例ではカスタムリソース作成時に AWS の IAM ユーザー を動的に作成する動作となっています。また、作成される IAM ユーザーは vault 側で指定した有効期限でローテートされるため、IAM ユーザーの認証情報が漏洩しても一定時間後に有効期限が切れるため悪用されにくいというメリットがあるようです。
AWS Secret を使う場合は AWS secrets engine の Setup を事前に済ませておく必要があります。
また、この時に DynamicSecret で作成される IAM ユーザーの詳細やポリシーなどを設定します。
# AWS secret engine の有効化
$ vault secrets enable aws
# AWS にアクセスする iam credential の設定
# access_key, secret_key は事前に作成しておく
$ vault write aws/config/root \
access_key=... \
secret_key=... \
region=ap-northeast-1
# 作成する IAM user の設定
$ vault write aws/roles/my-role \
credential_type=iam_user \
policy_document=-<<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ec2:*",
"Resource": "*"
}
]
}
EOF
上記を設定した後、通常の Vault コマンドを使用する方法では vault read aws/creds/my-role
を実行することで AWS 側に動的に IAM ユーザーを作成できます。
Secret Operator ではコマンドを実行する代わりに VaultDynamicSecret
カスタムリソースを作成することにより、リソース作成のタイミングで IAM ユーザーを作成します。
まず先ほど利用した appRole で aws/creds/my-role
にアクセスできるようポリシーを追加しておきます。
$ cat role-policy.hcl
path "aws/creds/*" {
capabilities = ["read"]
}
$ vault policy write iam-role role-policy.hcl
$ vault write auth/approle/role/k8s-vault-op policies=k8s-vault-op,iam-role
次にドキュメントの例に沿って VaultDynamicSecret
のマニフェストを作成します。
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: vault-dynamic-secret-aws-iam
spec:
vaultAuthRef: vault-auth
mount: aws
path: creds/my-role
destination:
create: true
name: dynamic-aws-iam
このをリソースを作成すると AWS 側に IAM ユーザーが作成されます。
同時に k8s 側に secret dynamic-aws-iam
も作成され、この中に作成した IAM ユーザーに関する 認証情報 (access_key と secret_key) が記載されます。
$ kubectl get secret dynamic-aws-iam -o yaml | yq -r ".data._raw" | base64 -d | jq
{
"access_key": "xxx",
"secret_key": "yyy",
"security_token": null
}
AWS 側も確認すると、vault-approle-my-role-1711468574-iTMb25HW7bU0mfk1v4yV
という IAM ユーザーが作成されているのが確認できます。
$ aws iam list-users
Users:
- Arn: ....
CreateDate: ...
Path: /
UserId: AIDARTHR5BBQBSFCH6X6R
UserName: vault-approle-my-role-1711468574-iTMb25HW7bU0mfk1v4yV
DynamicSecret は一時的な認証情報であるため TTL による有効期限を設定するのが一般的です。TTL が切れた際の動作についても確認するため、ここでは以下のコマンドにより上記の IAM ユーザーの有効期限 (TTL) を 1 分に設定します。
$ vault write sys/mounts/aws/tune default_lease_ttl=1m max_lease_ttl=1m
これで上記の VaultDynamicSecret
リソースを再度作成して動作を見てみると、どうやら 1 分間に IAM ユーザーの削除 → 新しいIAM ユーザーの作成という動作が実行されているようです。こちらは aws iam list-users
を 1 分間毎に実行してユーザーの CreateDate を見ることで確認できます。
また、この様子は VaultDynamicSecret を describe した際のイベントからも確認できます。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SecretSynced 2m30s VaultDynamicSecret Secret synced, lease_id="aws/creds/my-role/QQq6eSDpqOmUl2mfbV3FniBS", horizon=44.279659737s
Normal SecretRotated 2m28s VaultDynamicSecret Secret synced, lease_id="aws/creds/my-role/csc72JohsmDQVtbzIKYJsN95", horizon=43.631735595s
Normal SecretLeaseRenewal 106s VaultDynamicSecret Lease renewal duration was truncated from 60s to 18s, requesting new credentials
Normal SecretRotated 104s VaultDynamicSecret Secret synced, lease_id="aws/creds/my-role/yVwNVa8PQ105wpQrurYyFWbu", horizon=41.194170039s
Normal SecretLeaseRenewal 63s VaultDynamicSecret Lease renewal duration was truncated from 60s to 19s, requesting new credentials
Normal SecretRotated 60s VaultDynamicSecret Secret synced, lease_id="aws/creds/my-role/AvZyDu9DecRCG5RodD8P22Pt", horizon=43.546827077s
Normal SecretLeaseRenewal 17s VaultDynamicSecret Lease renewal duration was truncated from 60s to 17s, requesting new credentials
Normal SecretRotated 14s VaultDynamicSecret Secret synced, lease_id="aws/creds/my-role/bDKo5OTSlzlwr7j0SVVfx6s1", horizon=41.868678212s
新しい IAM ユーザーが作成されてから 1 分後には TTL が切れて削除されますが、VaultDynamicSecret リソースが存在している間は SecretRotated
により都度新しいユーザーが作成されます。これにより有効期限が切れても VaultDynamicSecret 側でよしなに対応してくれることがわかりました。TTL 毎に認証情報が rotate されるため、この認証情報を使う pod 等では特に TTL の有効期限切れ等を意識せずに使うことができます(rotate される度に IAM ユーザー名や access_key の値自体は変更されるので場合によっては意識する必要があるかも知れませんが)。
VaultDynamicSecret リソースを削除すると rotate も行われなくなり、この中で作成された IAM ユーザーは TTL が切れると vault により削除されるので、最終的には VaultDynamicSecret リソース作成前の状態に戻ります。
Auto unseal
Dev 以外のモードで起動した vault server pod は Initialize と unseal で見たように seal 状態で起動するため、pod が再作成される度に unseal の操作が必要になります。ただ pod が再作成される度に手動で unseal を実行するのは運用上かなり不便なので Auto unseal という機能が備わっています。これは vault 外部のクラウド等に保存した key を使って unseal を自動で実行する機能になっています。
key の保管場所としては Auto unseal に以下の選択肢が記載されています。
- AWS KMS
- GCP Cloud KMS
- Azure Key Vault
- HSM (Hardware Security Module)
- Transit Secret Engine
このうち上 3 つはクラウドプロバイダーの Key Management サービスを利用する方法であり、HSM は Vault enterprise 専用の機能となっています。最後の transit secret engine は vault の engine を利用した機能となっており、対象の vault server とは別に構築した vault server 上に key を保存する仕組みになっています。こちらは手元でも環境を用意できるので試してみます。
セットアップ
基本的にはドキュメントのAuto-unseal using Transit secrets engine の手順に沿って実現します。
Transit Secret Engine の方法では k8s クラスタ上に構築した vault server とは別に key を保存するための vault server が必要になります。もうひとつ k8s クラスタを用意して helm で vault を構築してもいいのですが、ここでは手軽さを重視して別のサーバ上に docker で構築します。
構築のための docker-compose.yml
は以下。
services:
vault:
container_name: vault
image: hashicorp/vault
ports:
- 8200:8200
cap_add:
- IPC_LOCK
command: server -dev -dev-root-token-id="00000000-0000-0000-0000-000000000000"
environment:
VAULT_DEV_ROOT_TOKEN_ID: '00000000-0000-0000-0000-000000000000'
VAULT_TOKEN: '00000000-0000-0000-0000-000000000000'
また、ドキュメントでは Auto unseal を有効化する vault server を vault1, そのための key を保存する vault server を vault 2 としていますが、ここではそれぞれ以下のように読み変えます。
- vault1: vault 2 の key を保存する vault server. docker で構築する。
- vault2: k8s クラスタ上の vault server. 今まで使用してきたものを使う。
https://developer.hashicorp.com/vault/tutorials/auto-unseal/autounseal-transit#scenario-introduction より引用
docker で vault server を起動したらコンテナの vault server に接続するための環境変数を設定、transit engine の有効化、autounseal
という名前の key を作成します。このあたりはドキュメントの手順と同じです。
# vault1 が動作するサーバ上で実行
$ export VAULT_ADDR="http://0.0.0.0:8200"
$ export VAULT_TOKEN="00000000-0000-0000-0000-000000000000"
# Audit log の有効化
# ドキュメントでは audit.log に出力するように設定しているが
# docker の場合は stdout に出すように設定
$ vault audit enable file file_path=stdout
$ vault secrets enable transit
$ vault write -f transit/keys/autounseal
$ vault policy write autounseal -<<EOF
path "transit/encrypt/autounseal" {
capabilities = [ "update" ]
}
path "transit/decrypt/autounseal" {
capabilities = [ "update" ]
}
EOF
# Token を作成
$ vault token create -orphan -policy="autounseal" \
-wrap-ttl=120 -period=24h \
-field=wrapping_token > wrapping-token.txt
# Unwrap
$ vault unwrap -field=token $(cat wrapping-token.txt)
動作確認
auto Unseal を有効化するためには、vault server の configuration ファイル内に seal block
を追加します。k8s クラスタ上の vault server では configmap の vault-config
に hcl 構文で設定が記載されているため、こちらを編集します。
- address: docker で稼働している vault server のアドレスを指定
- token: 上記の工程で unwrap した token の値を設定
- disable_renewal: false に設定
- key_name: docker vault で設定した keyname を指定
- mount_path: docker vault で設定した mount path を指定
- tls_skip_verify: http 通信なので true に設定
seal "transit" {
address = "http://192.168.3.181:8200"
token = "hvs.CAESIFCKWyUjvdWquPuRdiLSkbxgiog0Ke6zODf5ApbdP8awGh4KHGh2cy5WWUlZQ0c5YWdsNGhxOGc1N1dHSERCeG0"
disable_renewal = "false"
key_name = "autounseal"
mount_path = "transit/"
tls_skip_verify = "true"
}
configmap を編集して上記の値を設定。
$ kubectl edit configmaps vault-config
編集したら vault pod を削除して configmap の変更を反映させます。
pod が再作成されると seal 状態で起動しますが、この段階ではまだ auto unseal が適用されていません。
というのも、Initialize と unseal において一度 unseal token を使って unseal を実行しているので、現在の seal の設定は Shamir seal になっています。なので Seal migration の手順に沿って Shamir seal から auto unseal に移行する必要があります。手順では vault のバージョンや HA 構成によって実行方法が異なりますが、今回のように StandAlone 構成の vault では以下の手順に従えば良いです。
Now, bring the standby node back up and run the unseal command on each key, by supplying the -migrate flag.
よって、k8s クラスタ上の vault server に対して kubectl exec -ti vault-0 -- vault operator unseal --migrate
を実行し、Initialize と unseal の際と同様に異なる unseal key の入力を 3 回繰り返します。
入力に成功すると特にメッセージ等は表示されませんが auto unseal が有効になります。これを確認するためにもう一度 vault pod を削除して少し待つと、unseal 操作を実行しなくても pod が ready となります。
docker 側の vault server のログを docker log vault
で確認すると、audit log によって以下のようなリクエストが記録されています。ドキュメントの記載の通り path: transit/decrypt/autounseal
に対する operation: update
が実行されているので、このリクエストにより auto unseal が実行されていることが確認できます。
"request": {
"id": "e353d0e1-57e3-b714-db1a-0bd4b12fad18",
"client_id": "CazzHB1T6UPO5F7ytGq17GnkZg/t3aiSR1ZYkz28Cxk=",
"operation": "update",
"mount_point": "transit/",
"mount_type": "transit",
"mount_accessor": "transit_292d86f0",
"mount_running_version": "v1.15.6+builtin.vault",
"mount_class": "secret",
...
"path": "transit/encrypt/autounseal",
...
"remote_address": "192.168.3.125",
以上より、auto-unseal のための key を別の vault server に保存し、Transit Secret Engine を使って実行する動作が確認できました。
その他
OpenBao について
2023 年に hashicorp terraform のライセンス関係でいろいろあり、terraform から fork されたオープンソースプロジェクトの OpenTofu が開始されました。
Vault に関しても同様に vault から fork された OpenBao というプロジェクトが開始されています。
現時点ではプロジェクトのホームページ等はない状態ですが、OpenTofu が(少なくとも普通に使う分には) terraform と同様の感覚で使えることから、openbao プロジェクトに関してもが上記のような機能が充実していくことが期待されます。
おわりに
HashiCorp vault を k8s クラスタ上に構築していろいろな機能を試しました。vault 自体が secret management として機密情報を管理するサービスであり、kubernetes の secret とシームレスに統合する機能がけっこう充実していることが確認できました。実際の運用において機密情報 (secret) をどう取り扱っていくかというのはよくある問題だと思いますが、vault 側で一元管理し、kubernetes の secret に対しては今回紹介した Vault Secrets Operator を使って同期させるなどいろいろな工夫が考えられそうです。
Discussion