zaki work log

作業ログやら生活ログやらなんやら

Azure Key Vaultを使ったHA構成HashiCorp Vaultの自動unseal / join / remove

ここのブログでHashicorp Vaultを扱うのはそういえば初めてだったけど、Vault使う上で面倒なunsealなどを自動処理する方法を調べる機会があったのでまとめました。
なお、unseal keyやroot tokenを記事用に全部書き出してますが、機密情報なので実際は厳重に管理してください。

環境は以下の通り、HelmでK8sクラスタへデプロイしたHashicorp Vaultです。

  • HashiCorp Vault 1.15.2
    • chart version 0.27.0
  • Kubernetes: 1.29.1
    • K3s(docker compose版)
    • ホストOS: Fedora 39
    • オンプレのProxmox VEのVMに構築

あと、Azureの権限周りはあまり詳しくなくてだいぶ適当なので、不要な権限を与えてるかもしれない。詳しい人教えてください。

なお、この記事はHashicorp Vaultのデプロイ方法について書いてるだけで、アプリケーションとしてのVaultの使用方法は全く書いていません。

Helmチャートの設定

helm repo add hashicorp https://helm.releases.hashicorp.com
helm search repo hashicorp/vault
$ helm search repo hashicorp
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION                                       
hashicorp/consul                        1.3.2           1.17.2          Official HashiCorp Consul Chart                   
hashicorp/terraform                     1.1.2                           Install and configure Terraform Cloud Operator ...
hashicorp/terraform-cloud-operator      2.2.0           2.2.0           Official Helm chart for HashiCorp Terraform Clo...
hashicorp/terraform-enterprise          1.1.1           1.16.0          Official HashiCorp Terraform-Enterprise Chart     
hashicorp/vault                         0.27.0          1.15.2          Official HashiCorp Vault Chart                    
hashicorp/vault-secrets-operator        0.4.3           0.4.3           Official Vault Secrets Operator Chart             
hashicorp/waypoint                      0.1.21          0.11.3          Official Helm Chart for HashiCorp Waypoint       

スタンドアロンモードのインストール(前置き)

読み飛ばし可

デプロイ

デフォルト値でVaultをインストール。(values.yamlは不要)

$ helm upgrade --install my-vault hashicorp/vault -n vault --create-namespace
Release "my-vault" does not exist. Installing it now.
NAME: my-vault
LAST DEPLOYED: Sat Feb 10 16:01:21 2024
NAMESPACE: vault
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Vault!

Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:

https://developer.hashicorp.com/vault/docs


Your release is named my-vault. To learn more about the release, try:

  $ helm status my-vault
  $ helm get manifest my-vault

StatefulSetでVaultのpodがデプロイされますがnot readyになります。

$ kubectl get pod,sts -n vault 
NAME                                           READY   STATUS    RESTARTS   AGE
pod/my-vault-agent-injector-6b66d76649-xmvbc   1/1     Running   0          25s
pod/my-vault-0                                 0/1     Running   0          25s

NAME                        READY   AGE
statefulset.apps/my-vault   0/1     25s

ログを見ると以下の通り。

2024-02-10T07:01:56.144Z [INFO]  core: security barrier not initialized
2024-02-10T07:01:56.144Z [INFO]  core: seal configuration missing, not initialized

Vaultのステータスをvault statusコマンドを実行して確認すると、未初期化(Initializedがtrue)であることが確認できます。

$ kubectl exec -n vault my-vault-0 -- vault status
Key                Value
---                -----
Seal Type          shamir
Initialized        false
Sealed             true
Total Shares       0
Threshold          0
Unseal Progress    0/0
Unseal Nonce       n/a
Version            1.15.2
Build Date         2023-11-06T11:33:28Z
Storage Type       file
HA Enabled         false

初期化とunseal

Vaultを使える状態にするにはunsealが必要で、そのためにはまず初期化します。
初期化はVaultのコマンドvault operator initを実行します。

$ kubectl exec -n vault my-vault-0 -- vault operator init
Unseal Key 1: PZuSm+GpsG6xoLz/1hm+kdke/q/UWm+kx2ewtyp1yJ3x
Unseal Key 2: 2eW+IlscQ1cKEu/pATh3ZJvJ5EZ9D5jopRbrUgACiC8h
Unseal Key 3: DbVJzW0vcT4a4bhS3lNIihwTizYridY6Rzt282Prl34m
Unseal Key 4: Ba4+tWfLXtyQ0hXvdHQbXlOOfHqCPvTgDDC7wtN6Ok86
Unseal Key 5: avg1gIAbhDDk4SRpSqby6gDENhOJPJzqk93CE8yCar+c

Initial Root Token: hvs.jnkpJnAEN4PDyPZZF3RJoGJV

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.

これでrootトークンと5つのunsealキーを取得できました。
ステータスを確認すると、Initializedがtrueになります。
※ これは記事用に全部書き出してますが、機密情報なので実際は厳重に管理してください。

$ kubectl exec -n vault my-vault-0 -- vault status
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    0/3
Unseal Nonce       n/a
Version            1.15.2
Build Date         2023-11-06T11:33:28Z
Storage Type       file
HA Enabled         false

初期化が済んだらunsealを行います。
unsealはvault operator unsealを実行します。
ここでは記事用に引数に指定してますが、機密情報をローカルエコーするのはよくないので、引数無しで実行して対話的に入力した方がよいです。(が、自動化するならこの方法かな?)

$ kubectl exec -n vault my-vault-0 -- vault operator unseal PZuSm+GpsG6xoLz/1hm+kdke/q/UWm+kx2ewtyp1yJ3x
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
Unseal Nonce       b8ff2c62-5fdd-18b0-6e20-ddff956f7b2f
Version            1.15.2
Build Date         2023-11-06T11:33:28Z
Storage Type       file
HA Enabled         false

Unseal Progressが1/3になりました。あと2回実行する必要があるので、5つあるキーのうちあと2個使って同じコマンドを実行します。

$ kubectl exec -n vault my-vault-0 -- vault operator unseal 2eW+IlscQ1cKEu/pATh3ZJvJ5EZ9D5jopRbrUgACiC8h
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    2/3
Unseal Nonce       b8ff2c62-5fdd-18b0-6e20-ddff956f7b2f
Version            1.15.2
Build Date         2023-11-06T11:33:28Z
Storage Type       file
HA Enabled         false
$ kubectl exec -n vault my-vault-0 -- vault operator unseal DbVJzW0vcT4a4bhS3lNIihwTizYridY6Rzt282Prl34m
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.15.2
Build Date      2023-11-06T11:33:28Z
Storage Type    file
Cluster Name    vault-cluster-2f48696e
Cluster ID      e109d3d3-5d52-c081-667f-8fefcc8067f6
HA Enabled      false

これでSealedの値がfalseになり、unseal状態になりました。

$ kubectl get pod -n vault 
NAME                                       READY   STATUS    RESTARTS   AGE
my-vault-agent-injector-6b66d76649-xmvbc   1/1     Running   0          4m47s
my-vault-0                                 1/1     Running   0          4m47s

この通りunsealされるとPodはready状態になり、利用準備ができました。

HAクラスターでのインストール

以下は、スタンドアロンでデプロイした環境はすべて削除済みで新規に構築しています。

ここからは耐障害性を高めるためのHAクラスタ構成でデプロイします。
※ AntiAffinityの設定によって1pod1nodeデプロイになるため、あらかじめノードは5ノードでKubernetesを構築しています。

$ kubectl get node
NAME           STATUS   ROLES                  AGE     VERSION
b513c8231aad   Ready    <none>                 6h41m   v1.29.1+k3s2
9952de1bc166   Ready    <none>                 6h41m   v1.29.1+k3s2
2f5893c02aa3   Ready    <none>                 6h40m   v1.29.1+k3s2
412b213f3d10   Ready    <none>                 6h41m   v1.29.1+k3s2
4c1f8de42bfe   Ready    control-plane,master   6h41m   v1.29.1+k3s2
8239e04d52ef   Ready    <none>                 6h41m   v1.29.1+k3s2

values.yamlは以下の通り。
configの部分はデフォルトのままですが、本題で使うので含めています。
setNodeIdは無くても良いですが、Vaultノード名が分かりやすくなるので有効にしています。

server:
  # Run Vault in "HA" mode. There are no storage requirements unless the audit log
  # persistence is required.  In HA mode Vault will configure itself to use Consul
  # for its storage backend.  The default configuration provided will work the Consul
  # Helm project by default.  It is possible to manually configure Vault to use a
  # different HA backend.
  ha:
    enabled: true
    replicas: 1


    # Enables Vault's integrated Raft storage.  Unlike the typical HA modes where
    # Vault's persistence is external (such as Consul), enabling Raft mode will create
    # persistent volumes for Vault to store data according to the configuration under server.dataStorage.
    # The Vault cluster will coordinate leader elections and failovers internally.
    raft:

      # Enables Raft integrated storage
      enabled: true
      # Set the Node Raft ID to the name of the pod
      setNodeId: true

      # Note: Configuration files are stored in ConfigMaps so sensitive data
      # such as passwords should be either mounted through extraSecretEnvironmentVars
      # or through a Kube secret.  For more information see:
      # https://developer.hashicorp.com/vault/docs/platform/k8s/helm/run#protecting-sensitive-vault-configurations
      config: |
        ui = true

        listener "tcp" {
          tls_disable = 1
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          # Enable unauthenticated metrics access (necessary for Prometheus Operator)
          #telemetry {
          #  unauthenticated_metrics_access = "true"
          #}
        }

        storage "raft" {
          path = "/vault/data"
        }

        service_registration "kubernetes" {}
helm upgrade --install my-vault hashicorp/vault -n vault --create-namespace -f values.yaml 
$ kubectl get pod -n vault -o wide
NAME                                       READY   STATUS    RESTARTS   AGE     IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-0                                 0/1     Running   0          5m47s   10.42.4.13   9952de1bc166   <none>           <none>
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          5m47s   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-1                                 0/1     Running   0          5m46s   10.42.3.5    8239e04d52ef   <none>           <none>
my-vault-2                                 0/1     Running   0          5m46s   10.42.2.5    4c1f8de42bfe   <none>           <none>

3podがデプロイされましたが、スタンドアロンの場合同様に、初期化とunsealが必要です。

リーダーノードの初期化とunseal

レプリカ3のStatefulSetなので、0番のpodでまず初期化とunsealを実施。

$ kubectl exec -n vault my-vault-0 -- vault operator init
Unseal Key 1: ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
Unseal Key 2: ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
Unseal Key 3: pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM
Unseal Key 4: 6q2Fi8q7AOc0n6iPJeoWTnio6QjCDMlxwZFyD7uw9GqE
Unseal Key 5: zLEVdCLOMXAptoZmBZRYVpSG8o8lCfK/bJi5pQ7jE8Sr

Initial Root Token: hvs.GuR8WbGjN7X3roX6UoJYztON

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.
kubectl exec -n vault my-vault-0 -- vault operator unseal ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
kubectl exec -n vault my-vault-0 -- vault operator unseal ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
kubectl exec -n vault my-vault-0 -- vault operator unseal pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM

これでvault-0がunsealされreadyになりました。

$ kubectl get pod -n vault -o wide
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          11m   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-1                                 0/1     Running   0          11m   10.42.3.5    8239e04d52ef   <none>           <none>
my-vault-2                                 0/1     Running   0          11m   10.42.2.5    4c1f8de42bfe   <none>           <none>
my-vault-0                                 1/1     Running   0          11m   10.42.4.13   9952de1bc166   <none>           <none>

ノードのクラスターへの参加

現時点ではまだシングルノード状態。

/ $ vault login hvs.GuR8WbGjN7X3roX6UoJYztON
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.GuR8WbGjN7X3roX6UoJYztON
token_accessor       GpmzT1ZyBYs1bduOTRYVnALW
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
/ $ vault operator members
Host Name     API Address               Cluster Address                              Active Node    Version    Upgrade Version    Redundancy Zone    Last Echo
---------     -----------               ---------------                              -----------    -------    ---------------    ---------------    ---------
my-vault-0    http://10.42.4.13:8200    https://my-vault-0.my-vault-internal:8201    true           1.15.2     1.15.2             n/a                n/a

HA構成にするため残りの2podをクラスターに参加させるには、それぞれraft join操作をリーダーであるvault-0に対して行います。
podではHTTPでlistenしているので、http://を付与するのをお忘れなく。
URL(pod名service名)はHelmのリリース名に依存するのでそこは読み替えること。

$ kubectl exec -n vault my-vault-1 -- vault operator raft join http://my-vault-0.my-vault-internal:8200
Key       Value
---       -----
Joined    true
$ kubectl exec -n vault my-vault-2 -- vault operator raft join http://my-vault-0.my-vault-internal:8200
Key       Value
---       -----
Joined    true
$ kubectl get pod -n vault -o wide
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          33m   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-1                                 0/1     Running   0          33m   10.42.3.5    8239e04d52ef   <none>           <none>
my-vault-2                                 0/1     Running   0          33m   10.42.2.5    4c1f8de42bfe   <none>           <none>
my-vault-0                                 1/1     Running   0          33m   10.42.4.13   9952de1bc166   <none>           <none>

これでvault-1およびvault-2は初期化済みseal状態になるので、それぞれunsealを行います。

kubectl exec -n vault my-vault-1 -- vault operator unseal ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
kubectl exec -n vault my-vault-1 -- vault operator unseal ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
kubectl exec -n vault my-vault-1 -- vault operator unseal pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM
kubectl exec -n vault my-vault-2 -- vault operator unseal ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
kubectl exec -n vault my-vault-2 -- vault operator unseal ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
kubectl exec -n vault my-vault-2 -- vault operator unseal pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM

これでようやく全podがready状態になり、クラスターに参加できた状態になりました。

$ kubectl get pod -n vault -o wide
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          35m   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running   0          35m   10.42.4.13   9952de1bc166   <none>           <none>
my-vault-1                                 1/1     Running   0          35m   10.42.3.5    8239e04d52ef   <none>           <none>
my-vault-2                                 1/1     Running   0          35m   10.42.2.5    4c1f8de42bfe   <none>           <none>

$ kubectl exec -it -n vault my-vault-0 -- vault operator members
Host Name     API Address               Cluster Address                              Active Node    Version    Upgrade Version    Redundancy Zone    Last Echo
---------     -----------               ---------------                              -----------    -------    ---------------    ---------------    ---------
my-vault-2    http://10.42.2.5:8200     https://my-vault-2.my-vault-internal:8201    false          1.15.2     1.15.2             n/a                2024-02-10T12:30:09Z
my-vault-1    http://10.42.3.5:8200     https://my-vault-1.my-vault-internal:8201    false          1.15.2     1.15.2             n/a                2024-02-10T12:30:07Z
my-vault-0    http://10.42.4.13:8200    https://my-vault-0.my-vault-internal:8201    true           1.15.2     1.15.2             n/a                n/a

これでHA構成でのVaultの利用準備ができました。

スケールアウト/スケールイン

values.yamlreplicas: 3を指定したため、初期状態で3つのpodがデプロイされていますが、StatefulSetのレプリカ数を変更することでスケールできます。
例えば5台構成にするには、普通にkubectl scaleで設定できます。

$ kubectl get sts -n vault 
NAME       READY   AGE
my-vault   3/3     11h
$ kubectl scale -n vault statefulset --replicas 5 my-vault 
statefulset.apps/my-vault scaled

$ kubectl get pod -n vault -o wide 
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          11h   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running   0          11h   10.42.4.13   9952de1bc166   <none>           <none>
my-vault-1                                 1/1     Running   0          11h   10.42.3.5    8239e04d52ef   <none>           <none>
my-vault-2                                 1/1     Running   0          11h   10.42.2.5    4c1f8de42bfe   <none>           <none>
my-vault-3                                 0/1     Running   0          34s   10.42.0.5    2f5893c02aa3   <none>           <none>
my-vault-4                                 0/1     Running   0          34s   10.42.1.5    412b213f3d10   <none>           <none>

ただし、新しく作成されたPodはやはりraft joinunsealが必要です。

kubectl exec -n vault my-vault-3 -- vault operator raft join http://my-vault-0.my-vault-internal:8200
kubectl exec -n vault my-vault-4 -- vault operator raft join http://my-vault-0.my-vault-internal:8200

kubectl exec -n vault my-vault-3 -- vault operator unseal ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
kubectl exec -n vault my-vault-3 -- vault operator unseal ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
kubectl exec -n vault my-vault-3 -- vault operator unseal pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM
kubectl exec -n vault my-vault-4 -- vault operator unseal ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
kubectl exec -n vault my-vault-4 -- vault operator unseal ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
kubectl exec -n vault my-vault-4 -- vault operator unseal pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM

これで5台クラスターとなりました。

$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true
my-vault-3    my-vault-3.my-vault-internal:8201    follower    true
my-vault-4    my-vault-4.my-vault-internal:8201    follower    true

スケールインの場合は、いきなりレプリカ数を減らすのはNGで、まずVault的にクラスターから除外する必要があり、joinと逆でremove-peerを行います。
ここでは追加でスケールしたvault-3とvault-4を削除し、レプリカ3に戻す作業を行ってみます。
といってもコマンドは簡単で、remove-peerの引数にlist-peersでリストされているNode名を指定します。
この例ではvalues.yamlsetNodeId: trueの設定を入れているのでNode名=Pod名になってます。(指定がオフの場合はランダムなハッシュ値になります)

$ kubectl exec -n vault my-vault-0 -- vault operator raft remove-peer my-vault-4
Peer removed successfully!
$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true
my-vault-3    my-vault-3.my-vault-internal:8201    follower    true

これでmy-vault-4クラスターから除外されました。
同じ要領でmy-vault-3も除外します。

$ kubectl exec -n vault my-vault-0 -- vault operator raft remove-peer my-vault-3
Peer removed successfully!
$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true

これでStatefulSetのレプリカ数を3に戻せばOKです。

$ kubectl scale -n vault statefulset --replicas 3 my-vault 
statefulset.apps/my-vault scaled
$ kubectl get pod -n vault -o wide 
NAME                                       READY   STATUS        RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running       0          11h   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running       0          11h   10.42.4.13   9952de1bc166   <none>           <none>
my-vault-1                                 1/1     Running       0          11h   10.42.3.5    8239e04d52ef   <none>           <none>
my-vault-2                                 1/1     Running       0          11h   10.42.2.5    4c1f8de42bfe   <none>           <none>
my-vault-4                                 1/1     Terminating   0          19m   10.42.1.5    412b213f3d10   <none>           <none>
my-vault-3                                 1/1     Terminating   0          19m   10.42.0.5    2f5893c02aa3   <none>           <none>

remove-peerを行わずにスケールインを行うと、削除された分のPodはクラスターには残ったままunhealth状態で残ったままとなります。
いきなりレプリカ3にすると、以下のようになります。

peersのリストは5台ある状態。

$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true
my-vault-3    my-vault-3.my-vault-internal:8201    follower    true
my-vault-4    my-vault-4.my-vault-internal:8201    follower    true

ただし、ステータスはhealthがfalseになります。

$ kubectl exec -n vault my-vault-0 -- vault operator raft autopilot state
Healthy:                         false
Failure Tolerance:               0
Leader:                          my-vault-0
Voters:
   my-vault-0
   my-vault-1
   my-vault-2
   my-vault-3
   my-vault-4
Servers:
   my-vault-0
   :
   :
   my-vault-3
      Name:              my-vault-3
      Address:           my-vault-3.my-vault-internal:8201
      Status:            voter
      Node Status:       alive
      Healthy:           false
   :
   :
   my-vault-4
      Name:              my-vault-4
      Address:           my-vault-4.my-vault-internal:8201
      Status:            voter
      Node Status:       alive
      Healthy:           false

この状態からでもremove-peer my-vault-3 remove-peer my-vault-4を実行すれば3台構成へ変更可能。

自動unsealと自動join (ここから本題)

ここまで手動での初期化とunsealを行ったデプロイについてみてきましたが…
Pod起動時に必ずunsealが必要なため、率直に言ってKubernetesのオートヒーリングなどとの相性も悪く、運用でこのコマンド実行はやってられないと思います。
例えばkubectl delete podして再作成されたpodもnot readyなので、都度unsealする必要があります。

最初はこの仕様を知ってnot readyなpodを検出してunsealを行うプログラムとか書いてやろうと思いましたが、そんなことしなくても標準でクラウドサービスの機密情報を保持する機能と連携してauto unsealを行う機能が備わっています。
本エントリでは、AzureのKey Vaultサービスを使ったauto unsealについて説明します。

※ ここまでは単にVaultと雑に記載してましたが、ここからはAzure Key Vaultとややこしくなるので、「HashiCorp Vault」「Azure Key Vault」と表記します。

auto unseal with Azure Key Vault

公式ドキュメントにチュートリアルがあります。

developer.hashicorp.com

チュートリアルの流れをざっくり紹介すると、以下の通り

  1. TerraformでAzureにVMやAzure Key Vaultなどのリソース作成
  2. VM上にAzure Key Vault連携するHashiCorp Vaultのデプロイ

なので仕組みを把握するにはこの中身を追っていけばOK
言い方を変えると「自分の環境のVaultで自動unsealをするには」を実現するには中身を読み解く必要がありました。

アプリケーションとシークレットの作成

アプリの登録」で、「新規登録」から新しくAuto-unseal用のトークン発行のためのアプリを登録します。

作成できたら、概要ページで以下の値をメモ

  • アプリケーション(クライアント)ID
  • ディレクトリ(テナント)ID

次に「証明書とシークレット」で「クライアント シークレット」を新規作成。

今回は動作確認用なので、デフォルトの180日期限。

作成できたらシークレットの値(IDでなく)をメモ(一度画面を離れると非表示になるので注意)。

次に「APIのアクセス許可」で「アクセス許可の追加」し、公式ドキュメントは「Azure Active Directory Graph」を追加するよう記載されてるけど2022年6月に非推奨になったようなので、かわりに「Microsoft Graph」を選択。

まず「委任されたアクセス許可」を押下し、「User」の項目にある「User.Read」にチェックされているのを確認(デフォルトでこれがチェックされてる)。

次に「アプリケーションの許可」を押下し、以下をチェック

  • 「Application」の項目にある「Application.ReadWrite.All」
  • 「Directory」の項目にある「Directory.ReadWrite.All」

最後に下部の「アクセス許可の追加」を押下。
APIのアクセス許可の画面に戻ると、状態が「既定のディレクトリに付与されていません」になっているので、「既定のディレクトリに管理者の同意を与えます」を押下。

「既定のディレクトリに付与されました」になればOK

次にサブスクリプションの画面で、まずサブスクリプションIDを確認。

次に「アクセス制御(IAM)」の画面で「+追加」から「ロール割り当ての追加」を押下。

「ロール」タブでOwnerを選択とあるけど……おそらく「特権管理者ロール」で「所有者」で検索して出てくるものを選択。ちょっと権限強すぎそうだけど、とりあえず。

次に「メンバー」タブを押下し、「アクセスの割り当先」に「ユーザー、グループ、またはサービスプリンシパル」をチェック、「メンバーを選択する」で、先ほど作成したアプリケーション名を入力。

条件はドキュメントには何も書かれてない(「所有者」を選択するとこれが必要)けど、デフォルトのまま進めると割り当てできないので、適当だけど「ロールとプリンシパルの選択」から「ロールの制約」->「ロールの追加」で「所有者」をチェック。これで「レビューと割り当て」が可能になります。

キーコンテナーの作成と権限付与

ここからはチュートリアルのドキュメントでなく、資材として用意されてるTerraformのコードを参考にします。

github.com

まずはキーコンテナーを作成します。

アクセス構成の画面では、「自分」に操作権限を付与するために、「アクセス許可モデル」を「コンテナーのアクセスポリシー」にチェックし、下部に追加される「アクセスポリシー」で自分をチェック。

ネットワークはデフォルトのままで作成します。

作成が完了したら、再度作成済みのキーコンテナーに移動し、「アクセスポリシー」画面で「作成」し、「キーのアクセス許可」で以下3つにチェックを入れます。

  • 取得
  • キーの折り返しを解除
  • キーを折り返す

プリンシパルでは作成済みアプリケーションのIDか名前を入力して選択、あとはそのまま作成します。

次に「キー」に移動し、unsealに使うキーを作成します。
パラメタはデフォルトのRSA2048ビットのままでOKです。

TerraformのコードにはキーのオプションでwrapKey/unwrapKeyがありますが、作成時に選択できる画面がないのでそのまま作成します。
作成後に詳細を見ると必要な項目はチェックがついてるのでそのまま使用します。

これでAzure Key Vaultの名前と、キーの名前がそろいました。

HashiCorp Vault設定

Azure Key Vaultを使った設定はこちら。

developer.hashicorp.com

auto unseal設定

取得できた各値をセットして再デプロイします。
自動join設定はまだ入れてないので、いったんレプリカ数は1にし、消えたPodが使っていたストレージ(pvc/pv)も削除しておきます。

server:
  # Run Vault in "HA" mode. There are no storage requirements unless the audit log
  # persistence is required.  In HA mode Vault will configure itself to use Consul
  # for its storage backend.  The default configuration provided will work the Consul
  # Helm project by default.  It is possible to manually configure Vault to use a
  # different HA backend.
  ha:
    enabled: true
    replicas: 1


    # Enables Vault's integrated Raft storage.  Unlike the typical HA modes where
    # Vault's persistence is external (such as Consul), enabling Raft mode will create
    # persistent volumes for Vault to store data according to the configuration under server.dataStorage.
    # The Vault cluster will coordinate leader elections and failovers internally.
    raft:

      # Enables Raft integrated storage
      enabled: true
      # Set the Node Raft ID to the name of the pod
      setNodeId: true

      # Note: Configuration files are stored in ConfigMaps so sensitive data
      # such as passwords should be either mounted through extraSecretEnvironmentVars
      # or through a Kube secret.  For more information see:
      # https://developer.hashicorp.com/vault/docs/platform/k8s/helm/run#protecting-sensitive-vault-configurations
      config: |
        ui = true

        listener "tcp" {
          tls_disable = 1
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          # Enable unauthenticated metrics access (necessary for Prometheus Operator)
          #telemetry {
          #  unauthenticated_metrics_access = "true"
          #}
        }

        storage "raft" {
          path = "/vault/data"
        }

        service_registration "kubernetes" {}

        seal "azurekeyvault" {
          client_id      = "YOUR-APP-ID"
          client_secret  = "YOUR-APP-PASSWORD"
          tenant_id      = "YOUR-AZURE-TENANT-ID"
          vault_name     = "Azure Key Value name on Azure"
          key_name       = "Key name on Azure"
        }
helm upgrade --install my-vault hashicorp/vault -n vault --create-namespace -f values.yaml 

ただし、設定のConfigMapは更新されるけど、Podは再作成されず設定が反映されないのでPodを削除します。

$ kubectl delete pod -n vault my-vault-0 
pod "my-vault-0" deleted
$ kubectl get pod -n vault -o wide 
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          17h   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 0/1     Running   0          3s    10.42.4.18   9952de1bc166   <none>           <none>

起動しましたが、まだAuto-unseal設定が反映されていないためnot readyになります。ここで-migrateオプションをつけてunsealを行います。

$ kubectl exec -n vault my-vault-0 -- vault operator unseal -migrate ZQOZnaKVQBx8ppKkkChuIZ0NHRDbeyb0WaK1FALh4lB2
Key                           Value
---                           -----
Recovery Seal Type            shamir
Initialized                   true
Sealed                        true
Total Recovery Shares         5
Threshold                     3
Unseal Progress               1/3
...

$ kubectl exec -n vault my-vault-0 -- vault operator unseal -migrate ddRbGF6oZ9K1ccy8anz/d/pY5CxFAoxHNL/5HDH9C3hz
Key                           Value
---                           -----
Recovery Seal Type            shamir
Initialized                   true
Sealed                        true
Total Recovery Shares         5
Threshold                     3
Unseal Progress               2/3
...

$ kubectl exec -n vault my-vault-0 -- vault operator unseal -migrate pcwmjTj4HxJxFRS8h/9THcygRUV0BUGo1qWFuAydNJoM
Key                           Value
---                           -----
Recovery Seal Type            shamir
Initialized                   true
Sealed                        false
Total Recovery Shares         5
Threshold                     3
Seal Migration in Progress    true

$ kubectl get pod -n vault -o wide
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          17h   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running   0          49s   10.42.4.22   9952de1bc166   <none>           <none>

これでunsealとともにAuto-unsealも有効になります。

$ kubectl delete pod -n vault my-vault-0 
pod "my-vault-0" deleted

ためしにPodを削除してみると、

$ kubectl get pod -n vault -o wide -w
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          17h   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 0/1     Running   0          1s    10.42.4.25   9952de1bc166   <none>           <none>
my-vault-0                                 1/1     Running   0          5s    10.42.4.25   9952de1bc166   <none>           <none>

この通り、自動でunsealされready状態になります。

auto join設定

この時点で自動unsealは動作しますが、レプリカ数を増やした時のクラスターへのjoinは別途必要です。
が、これも自動で処理するための設定があり、retry_joinでリーダーのサーバーを指定しておけば良いです。

developer.hashicorp.com

      config: |
        ui = true

        listener "tcp" {
          tls_disable = 1
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          # Enable unauthenticated metrics access (necessary for Prometheus Operator)
          #telemetry {
          #  unauthenticated_metrics_access = "true"
          #}
        }

        storage "raft" {
          path = "/vault/data"

          retry_join {
            leader_api_addr = "http://my-vault-0.my-vault-internal:8200"
          }
          retry_join {
            leader_api_addr = "http://my-vault-1.my-vault-internal:8200"
          }
          retry_join {
            leader_api_addr = "http://my-vault-2.my-vault-internal:8200"
          }
        }

        service_registration "kubernetes" {}

        seal "azurekeyvault" {
          :
        }

これでレプリカを3にすると、

$ kubectl scale -n vault statefulset --replicas 3 my-vault 
statefulset.apps/my-vault scaled
$ kubectl get pod -n vault -o wide -w
NAME                                       READY   STATUS    RESTARTS   AGE    IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          18h    10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running   0          2m9s   10.42.4.25   9952de1bc166   <none>           <none>
my-vault-1                                 0/1     Pending   0          2s     <none>       <none>         <none>           <none>
my-vault-2                                 0/1     Pending   0          2s     <none>       <none>         <none>           <none>
my-vault-1                                 0/1     Pending   0          3s     <none>       8239e04d52ef   <none>           <none>
my-vault-1                                 0/1     ContainerCreating   0          3s     <none>       8239e04d52ef   <none>           <none>
my-vault-1                                 0/1     Running             0          4s     10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-1                                 0/1     Running             0          4s     10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-1                                 0/1     Running             0          9s     10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-1                                 0/1     Running             0          9s     10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-1                                 1/1     Running             0          9s     10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-2                                 0/1     Pending             0          9s     <none>       b513c8231aad   <none>           <none>
my-vault-2                                 0/1     ContainerCreating   0          9s     <none>       b513c8231aad   <none>           <none>
my-vault-2                                 0/1     ContainerCreating   0          17s    <none>       b513c8231aad   <none>           <none>
my-vault-2                                 0/1     Running             0          18s    10.42.5.14   b513c8231aad   <none>           <none>
my-vault-2                                 0/1     Running             0          28s    10.42.5.14   b513c8231aad   <none>           <none>
my-vault-2                                 0/1     Running             0          28s    10.42.5.14   b513c8231aad   <none>           <none>
my-vault-2                                 1/1     Running             0          28s    10.42.5.14   b513c8231aad   <none>           <none>

$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true

この通り、自動でクラスターへ追加とunsealが行われるようになりました。

auto remove設定

この状態ならスケーリングの際の運用上の支障はほぼなくなったと思いますが、ついでなのでスケールインの際のクラスターからの除外も自動化してみます。
これはraftのautopilotを使います。

$ kubectl exec -n vault my-vault-0 -- vault operator raft autopilot state
Healthy:                         true
Failure Tolerance:               1
Leader:                          my-vault-0
Voters:
   my-vault-0
   my-vault-1
   my-vault-2
Servers:
   my-vault-0
   :
   :

現在3台構成のため、耐障害性は1(1台だけならダウンしてもサービス可能)の状態。

ここにautopilotの設定を追加していきます。(ただ、コマンドかAPIで入れるしか例がないんだけど、設定ファイルでは不可?)

現在の設定

$ kubectl exec -n vault my-vault-0 -- vault operator raft autopilot get-config
Key                                   Value
---                                   -----
Cleanup Dead Servers                  false
Last Contact Threshold                10s
Dead Server Last Contact Threshold    24h0m0s
Server Stabilization Time             10s
Min Quorum                            0
Max Trailing Logs                     1000
Disable Upgrade Migration             false

ここに「停止サーバーの削除判定閾値を1分」「停止サーバーを自動削除」「最小台数を3」「正常状態とみなすまでの時間」をセットします。

$ kubectl exec -n vault my-vault-0 -- vault operator raft autopilot set-config \
  -dead-server-last-contact-threshold=1m \
  -cleanup-dead-servers=true \
  -min-quorum=3 \
  -server-stabilization-time=60

$ kubectl exec -n vault my-vault-0 -- vault operator raft autopilot get-config
Key                                   Value
---                                   -----
Cleanup Dead Servers                  true
Last Contact Threshold                10s
Dead Server Last Contact Threshold    1m0s
Server Stabilization Time             1m0s
Min Quorum                            3
Max Trailing Logs                     1000
Disable Upgrade Migration             false

ではまずレプリカを5にセット。

$ kubectl scale -n vault statefulset --replicas 5 my-vault 
statefulset.apps/my-vault scaled

$ kubectl get pod -n vault -o wide 
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running   0          18h   10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running   0          28m   10.42.4.25   9952de1bc166   <none>           <none>
my-vault-1                                 1/1     Running   0          26m   10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-2                                 1/1     Running   0          26m   10.42.5.14   b513c8231aad   <none>           <none>
my-vault-3                                 1/1     Running   0          33s   10.42.1.12   412b213f3d10   <none>           <none>
my-vault-4                                 1/1     Running   0          33s   10.42.2.14   4c1f8de42bfe   <none>           <none>

$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true
my-vault-3    my-vault-3.my-vault-internal:8201    follower    true
my-vault-4    my-vault-4.my-vault-internal:8201    follower    true

$ kubectl exec -n vault my-vault-0 -- vault operator raft autopilot state
Healthy:                         true
Failure Tolerance:               2
Leader:                          my-vault-0
Voters:
   my-vault-0
   my-vault-1
   my-vault-2
   my-vault-3
   my-vault-4

5台構成(耐障害は2台まで)になったので、次は手動のクラスターから除外せずにレプリカを3に設定

$ kubectl get pod -n vault -o wide 
NAME                                       READY   STATUS        RESTARTS   AGE    IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-9284p   1/1     Running       0          18h    10.42.5.12   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running       0          30m    10.42.4.25   9952de1bc166   <none>           <none>
my-vault-1                                 1/1     Running       0          28m    10.42.3.10   8239e04d52ef   <none>           <none>
my-vault-2                                 1/1     Running       0          28m    10.42.5.14   b513c8231aad   <none>           <none>
my-vault-4                                 1/1     Terminating   0          3m5s   10.42.2.14   4c1f8de42bfe   <none>           <none>
my-vault-3                                 1/1     Terminating   0          3m5s   10.42.1.12   412b213f3d10   <none>           <none>
$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true
my-vault-3    my-vault-3.my-vault-internal:8201    follower    true
my-vault-4    my-vault-4.my-vault-internal:8201    follower    true

# 約60秒後

$ kubectl exec -n vault my-vault-0 -- vault operator raft list-peers
Node          Address                              State       Voter
----          -------                              -----       -----
my-vault-0    my-vault-0.my-vault-internal:8201    leader      true
my-vault-1    my-vault-1.my-vault-internal:8201    follower    true
my-vault-2    my-vault-2.my-vault-internal:8201    follower    true

自動削除も動作確認できました。
なおこの機能は、最小ノード数が3のようなので、1台に戻すときには使えない模様(3台未満の時点でHAではなくなるから関係ないけどね)

初期構築で自動unseal設定にする

前述の手順は非auto unsealのHashicorp Vaultをauto unsealにする-migrateを行うものでしたが、初期構築からauto unseal状態でデプロイももちろん可能です。

$ helm upgrade --install my-vault hashicorp/vault -n vault --create-namespace -f values.yaml
$ kubectl get pod -n vault -o wide 
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-0                                 0/1     Running   0          23s   10.42.4.28   9952de1bc166   <none>           <none>
my-vault-agent-injector-6b66d76649-7vq86   1/1     Running   0          23s   10.42.5.16   b513c8231aad   <none>           <none>

$ kubectl exec -n vault my-vault-0 -- vault status
Key                      Value
---                      -----
Recovery Seal Type       azurekeyvault
Initialized              false
Sealed                   true
Total Recovery Shares    0
:

初期デプロイ時は未初期化のため最初の初期化を行います。

$ kubectl exec -n vault my-vault-0 -- vault operator init
Recovery Key 1: ck667PevKK5uImgLyxWIuo5Gjnbtegi+m64koc5jC1GC
Recovery Key 2: Vz3NMGw7C2EiTMqcicF0oHbcTZrGXvvYBM3cT7ElITo6
Recovery Key 3: /HhvNK2UuQEAtYBSqbcbavEZ5b4SSB51gJA08jpPk6Vt
Recovery Key 4: GQz/MJfpysvc/cokvq1ynMFV65nId6JS+JD84DGnExE8
Recovery Key 5: yDilNc0RQpEa1B+Fyle35fMQvkJ4s1GTmKaAmlBwXRHP

Initial Root Token: hvs.a1CBYXlcb0t7nq5eMGZdLxqC

Success! Vault is initialized

Recovery key initialized with 5 key shares and a key threshold of 3. Please
securely distribute the key shares printed above.

すると、この時点でunsealとなります。

$ kubectl get pod -n vault -o wide
NAME                                       READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
my-vault-agent-injector-6b66d76649-7vq86   1/1     Running   0          97s   10.42.5.16   b513c8231aad   <none>           <none>
my-vault-0                                 1/1     Running   0          97s   10.42.4.28   9952de1bc166   <none>           <none>

まとめ

以上、Hashicorp VaultとAzure Key Vaultを使った自動unsealや自動joinについて動作確認してみました。
auto unsealはクラウドサービスと連携、クラスターへのauto joinはretry_join設定、クラスターかrのauto removeはautopilot機能で実現できることを確認しました。

最後に繰り返しになりますが、本文中はunseal keyやroot tokenを全部書き出してますが、これは記事用のためで、実際は機密情報なので厳重に管理してください。

参考