さくらのクラウドでHashiCorp Vaultを動かすメモ

この記事は [さくらインターネット Advent Calendar 2023](https://qiita.com/advent-calendar/2023/sakura) 1日目の記事です。

さくらインターネットの大久保です。ご無沙汰しております。

実は、昨日ちょっとした新機能のリリースを予定していて、今日はそれについての記事を書こうと思ってたんですが、 **リリースが延期になってしまったので** :innocent:

代わりに、最近 **HashiCorpさんのVault** を触る機会があったので、今回はさくらのクラウド上で動かす方法を紹介したいと思います。（といいつつ、ほぼ自分向けメモです）

# HashiCorp Vaultとは？

HashiCorpさんが開発されているオープンソースソフトウェアです。バックエンドシステムにてやりとりされるパスワードやクレデンシャルなど、シークレット情報を保管するためのシステムです。Webサイトはこちら。

https://www.vaultproject.io/

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

様々なサイトで分かりやすい説明がなされているので、こちらではVault自体の詳細説明は割愛します。「HashiCorp Vaultとは」でググっていただくと、いろいろと情報が出てくるかと思います :blush: 

https://www.google.com/search?q=HashiCorp+Vault%E3%81%A8%E3%81%AF

「オープンソース版」に加え、有償ライセンスにて利用できる「エンタープライズ版」が存在します。エンタープライズ版については、オープンソース版に無い機能、例えば遠隔地でDRを行うための冗長化機能やHSMとの連携機能などが追加されていたり、サポートを受けることができたりします。

なお、Vaultのソースコードは、Business Source License(BSL)に基づいて配布されています。商用利用には制限が発生する場合があるため、特にクラウド/ホスティング事業者における導入については、十分に確認が必要です。昨今、BSLについては話題になりがちなので、ライセンスに違反しないように注意しましょう。

BSLに変更された経緯は、以下Publickeyさんの記事にて分かりやすく解説されています。

https://www.publickey1.jp/blog/23/hashicorpbsl.html

# 今回説明する構成例

さて、今回構築するサーバ構成は以下のようになります。

![サーバ構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/78ebddea-abc6-0060-ccc2-7878aeb27351.png)

- 3台のサーバでVaultクラスタを組む(Raftでの冗長化)
- サーバはプライベートネットワーク(スイッチ)に接続する
- VPCルータを設置し、外部からサーバへのアクセスはVPN(WireGuard)を経由して行う

# VPCルータとスイッチの作成

サーバを作成する前に、まずVPCルータとスイッチをさくらのクラウド上に作成します。

コントロールパネルから、以下のように設定をしていきます。

VPCルータの作成
![VPCルータの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/a9de7075-9e80-8b7c-e50e-6e141c7670b9.png)

インターフェイスの設定とスイッチの作成
![インターフェイスの設定とスイッチの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/11392185-15f0-310b-b4c6-d3907c5d4c8e.png)


VPN(WireGuard)の設定
![VPN(WireGuard)の設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/5a439d95-a1e0-86bd-dd5f-cd93fe2756eb.png)


参考までに、WireGuardクライアントの設定はこんな感じになります。

```
[Interface]
PrivateKey = sPDA2ysL...
Address = 10.0.0.2/24

[Peer]
PublicKey = Bgs856f8... <= VPCルータに表示される公開鍵を入力
AllowedIPs = 192.168.1.0/24
Endpoint = 133.242.xxx.xxx:51820 <= VPCルータのグローバル側IPアドレスを入力
```

WireGuard安定して接続でき、シンプルで非常にいいですよね。~~今までのIPsecの苦労はなんだったんだろうと思います。~~

# サーバの作成

Vaultを動作させるサーバを作成します。今回は以下の仕様で3台作成します。

- Rocky Linux 8.8
- CPU 2コア
- Memory 2GB
- SSD 20G

:::note info
Raftクラスタを組む場合の推奨スペックについては、こちらにドキュメントがあります。商用環境で動作させる場合はこちらを参考にすると良いでしょう。
https://developer.hashicorp.com/vault/tutorials/day-one-raft/raft-reference-architecture
:::

3台のホスト名とIPアドレスは以下を割り当てます。

| | ホスト名 | IPアドレス |
| -- | -- | --|
| 1台目 | test-vault1 | 192.168.1.11/24 |
| 2台目 | test-vault2 | 192.168.1.12/24 |
| 3台目 | test-vault3 | 192.168.1.13/24 |

さくらのクラウドのコントロールパネルから以下のようにサーバを作成します。

![SnapCrab_NoName_2023-11-30_19-39-39_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/b4e92877-57cc-5215-ee62-4058ff393677.png)
![SnapCrab_NoName_2023-11-30_19-39-53_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/e0064166-e7e6-53ac-c8e4-efbe30e51fd9.png)
![SnapCrab_NoName_2023-11-30_19-40-13_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/b20ab2c7-15e7-6484-5125-05ff66a1fc66.png)
![SnapCrab_NoName_2023-11-30_19-40-49_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/a3010952-52a3-acfc-9e4c-eda10b9f8e93.png)

同じ要領で2台目、3台目も作成します。

サーバの作成が完了したら、手元の端末からWireGuard越しに各サーバにSSHログインし、以下セットアップを進めます。

# Vaultのインストール

3台のサーバにてVaultをインストールします

```
$ sudo yum install -y yum-utils
$ sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
$ sudo yum -y install vault
```

インストール結果、以下のようになりました。

```
$ yum info vault
インストール済みパッケージ
名前         : vault
バージョン   : 1.15.2
リリース     : 1
Arch         : x86_64
サイズ       : 369 M
ソース       : vault-1.15.2-1.src.rpm
リポジトリー : @System
repo から    : hashicorp
概要         : Vault is a tool for secrets management, encryption as a service, and privileged access management.
URL          : https://github.com/hashicorp/vault
ライセンス   : BUSL-1.1
説明         : Vault is a tool for secrets management, encryption as a service, and privileged access management.

$ vault -v
Vault v1.15.2 (cf1b5cafa047bc8e4a3f93444fcb4011593b92cb), built 2023-11-06T11:33:28Z
```

# Vaultの初期設定と起動

設定ファイルを以下の内容で作成します。こちらは1台目の例です。

/etc/vault.d/vault.hcl

```
storage "raft" {
  path = "/opt/vault/data"
  node_id = "test-vault1"                     <= 2台目、3台目は書き換える
}
 
listener "tcp" {
  address = "0.0.0.0:8200"
  cluster_address = "0.0.0.0:8201"
  tls_disable = true
}
 
api_addr = "http://192.168.1.11:8200"        <= 2台目、3台目は書き換える
cluster_addr = "http://192.168.1.11:8201"    <= 2台目、3台目は書き換える
```

2台目、3台目も同様に設定ファイルを作成します。node_idとIPアドレス部分を書き換えてください。

手順簡略化のために、一旦TLSは無効化しています。TLSは、後ほどクラスタを組んだ後に有効化するとよいでしょう。

3台全てでVaultサービスを起動します。

```
$ systemctl start vault
$ systemctl status vault
● vault.service - "HashiCorp Vault - A tool for managing secrets"
   Loaded: loaded (/usr/lib/systemd/system/vault.service; disabled; vendor preset: disabled)
   Active: active (running) since Thu 2023-11-30 19:55:57 JST; 9s ago
     Docs: https://developer.hashicorp.com/vault/docs
 Main PID: 34035 (vault)
    Tasks: 7 (limit: 12382)
   Memory: 126.5M
   CGroup: /system.slice/vault.service
           └─34035 /usr/bin/vault server -config=/etc/vault.d/vault.hcl

11月 30 19:55:57 test-vault1 vault[34035]:            Recovery Mode: false
11月 30 19:55:57 test-vault1 vault[34035]:                  Storage: raft (HA available)
11月 30 19:55:57 test-vault1 vault[34035]:                  Version: Vault v1.15.2, built 2023-11-06T11:33:28Z
11月 30 19:55:57 test-vault1 vault[34035]:              Version Sha: cf1b5cafa047bc8e4a3f93444fcb4011593b92cb
11月 30 19:55:57 test-vault1 vault[34035]: ==> Vault server started! Log data will stream in below:
11月 30 19:55:57 test-vault1 vault[34035]: 2023-11-30T19:55:57.026+0900 [INFO]  proxy environment: http_proxy="" https_proxy="" no_proxy=""
11月 30 19:55:57 test-vault1 vault[34035]: 2023-11-30T19:55:57.059+0900 [INFO]  incrementing seal generation: generation=1
11月 30 19:55:57 test-vault1 vault[34035]: 2023-11-30T19:55:57.287+0900 [INFO]  core: Initializing version history cache for core
11月 30 19:55:57 test-vault1 vault[34035]: 2023-11-30T19:55:57.287+0900 [INFO]  events: Starting event system
11月 30 19:55:57 test-vault1 systemd[1]: Started "HashiCorp Vault - A tool for managing secrets".
```

なお、サーバ間でVaultが通信できるようにするため、一旦firewalldは無効化しておきます。

```
$ systemctl stop firewalld
$ systemctl disable firewalld
```

# データベースの初期化と認証情報の生成

続いて、1台目でデータベースの初期化と認証情報の生成を行います(2台目、3台目では実行不要です)。

```
! vaultコマンドは、デフォルトでhttpsでvaultデーモンにリクエストを投げるようになっているため、環境変数で "http" を指定する
$ export VAULT_ADDR='http://127.0.0.1:8200'

$ vault operator init -key-shares=1 -key-threshold=1
Unseal Key 1: uMXru9Reu9...

Initial Root Token: hvs.GmzOc2Q9...

Vault initialized with 1 key shares and a key threshold of 1. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 1 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 1 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.
```

ここで表示される `Unseal Key` と `Initial Root Token` は、後で必要になります。漏洩や紛失しないよう、厳重に保管しておきましょう。

:::note info
VaultにはSeal/Unsealという仕組みが備わっています。Vaultが預かるシークレット情報などは、Unseal Keyに紐づく暗号鍵で暗号化された状態でディスクに保存されます。

このUnseal Keyは、Vault外で管理する必要があり、Vaultデーモンを起動する際に入力します。これをUnseal操作と呼びます。Unsealが成功すると、ディスクに暗号化して保存されているシークレット情報などが読み取れる状態となり、Vaultの動作が開始します。

詳しくは以下を参照ください。
https://developer.hashicorp.com/vault/docs/concepts/seal
:::

:::note info
Unseal Keyは、デフォルトではシャミアの秘密分散法により5つ生成されます。5つのうち、いずれか3つを入力することでUnsealを行えます。

今回は手順を簡略化するため、`-key-shares=1` `-key-threshold=1` オプションを指定し、秘密分散せずに1つのUnseal Keyを生成するようにしています。実際の運用では、Unseal Keyをどのように管理するかに応じて調整するとよいでしょう。
:::

初期化が完了したら、一度Unseal操作を行う必要があります。

```
$ vault operator unseal
Unseal Key (will be hidden): <= ここに先ほど表示されたUnseal Keyを入力
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
Total Shares            1
Threshold               1
Version                 1.15.2
Build Date              2023-11-06T11:33:28Z
Storage Type            raft
Cluster Name            vault-cluster-1df2da86
Cluster ID              dec208b8-ccfb-b043-b164-c38f6641ca53
HA Enabled              true
HA Cluster              n/a
HA Mode                 standby
Active Node Address     <none>
Raft Committed Index    31
Raft Applied Index      31
```

上記出力中の Sealed が false になっていれば、Unseal成功です。

続いて、 `vault login` コマンドを用いて、Vaultにアクセスするための認証トークンを保存しておきます。これで、vaultコマンドによる操作が可能な状態になります。

``` 
$ vault login
Token (will be hidden): <= ここに先ほど表示されたInitial Root Tokenを入力
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.GmzOc...
token_accessor       D9LvgmIdJ...
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
```

ここで入力したトークンは `/root/.vault-token` ファイルに記録されます。

# Raftクラスタをセットアップする

1台目の準備ができたので、2台目、3台目をクラスタに参加させていきます。なお、Raftクラスタについては、以下にドキュメントがありますので、適宜参考にしてみてください。

https://developer.hashicorp.com/vault/docs/configuration/storage/raft


2台目、3台目のサーバにて、以下のように1台目のIPアドレスを指定する形で、クラスタに参加します。

```
$ vault operator raft join http://192.168.1.11:8200
Key       Value
---       -----
Joined    true
```

:::note info
ポート番号は8201ではなく、8200を指定します。設定中にcluster_addrで記載したポート番号8201は、クラスタの構築後、ノード間でraftの通信を行うために用いられます。
:::

クラスタに参加できたらUnsealを行います。Unseal Keyは1台目で生成されたものを入力します。

```
$ vault operator unseal
Unseal Key (will be hidden): <= ここに1台目で生成されたUnseal Keyを入力
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       1
Threshold          1
Unseal Progress    0/1
Unseal Nonce       n/a
Version            1.15.2
Build Date         2023-11-06T11:33:28Z
Storage Type       raft
HA Enabled         true

! しばらくしてstatusを確認すると、Sealedがfalseになる

$ vault status
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
Total Shares            1
Threshold               1
Version                 1.15.2
Build Date              2023-11-06T11:33:28Z
Storage Type            raft
Cluster Name            vault-cluster-1df2da86
Cluster ID              dec208b8-ccfb-b043-b164-c38f6641ca53
HA Enabled              true
HA Cluster              https://192.168.1.11:8201
HA Mode                 standby
Active Node Address     http://192.168.1.11:8200
Raft Committed Index    187
Raft Applied Index      187
```

2台目、3台目のサーバにておいても、1台目と同様にloginしておきます。

```
$ vault login
Token (will be hidden): <= ここに1台目で生成されたInitial Root Tokenを入力
```

Raftクラスタに参加できたか確認します。

```
$ vault operator raft list-peers
Node           Address              State       Voter
----           -------              -----       -----
test-vault1    192.168.1.11:8201    leader      true
test-vault2    192.168.1.12:8201    follower    true
test-vault3    192.168.1.13:8201    follower    true
```

3台見えるようになったら、クラスタは完成です！

:::note info
クラスタを組んだ場合、1台がリーダとして選出され、残りの2台はフォロワーとなります。フォロワーであるVaultが受け取ったリクエストは、自動的に内部でリーダに転送され、リーダにて処理されます。したがって、Vaultへのリクエストは、どのノードに対して送っても(フォロワーに送っても)問題ありません。
:::

# Key Value Storeにシークレット情報を格納してみる

実際にVaultにシークレット情報を格納してみましょう。

ここでは、VaultのKey Valueシークレットエンジンを用いて、パスワードっぽい文字列を保存してみます。Key Valueシークレットエンジンのドキュメントは以下にあります。

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

まずは、Key Valueシークレットエンジンを有効化します。以下のコマンドはどのノードで実行しても構いません。

```
$ vault secrets enable -path=secret kv-v2
Success! Enabled the kv-v2 secrets engine at: secret/

$ vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_13dd3f07    per-token private secret storage
identity/     identity     identity_b82486a0     identity store
secret/       kv           kv_08ce0a6e           n/a
sys/          system       system_b301d513       system endpoints used for control, policy and debugging
```

これで準備ができました。

値の保存、取得ができるか確認します。

```
$ vault kv put -mount=secret micho passwd1=himitsu passwd2=naisho
== Secret Path ==
secret/data/micho

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-30T13:25:32.803582885Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

$ vault kv get -mount=secret micho
== Secret Path ==
secret/data/micho

======= Metadata =======
Key                Value
---                -----
created_time       2023-11-30T13:25:32.803582885Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

===== Data =====
Key        Value
---        -----
passwd1    himitsu
passwd2    naisho
```

# トランジットシークレットエンジンを使ってみる

Vaultの面白い機能としてトランジットシークレットエンジンがあります。これは、暗号鍵がVault内部に生成され、与えられたデータの暗号化や復号が行える機能です(※)。値の保存は行われません。

※ 正確には、共通鍵暗号を使用する場合は暗号化と復号、公開鍵暗号を使用する場合は署名と検証、の動作になります。

トランジットシークレットエンジンのドキュメントは以下にあります。

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

トランジットシークレットエンジンで内部に生成された暗号鍵は、Vaultの外に取り出すことができないようになっています。したがって、これをRoot of Trust(信頼の基点)として利用することができます。ちょうど、HSM(Hardware Security Module)のような動作をイメージすると分かりやすいでしょうか。

まず、トランジットシークレットエンジンを有効化します。

```
$ vault secrets enable transit
Success! Enabled the transit secrets engine at: transit/
```

続いてエンドポイントを作成します。これにより、暗号鍵が内部的に生成され、該当エンドポイントを通じて暗号化や復号が行えるようになります。

```
$ vault write -f transit/keys/micho
Key                       Value
---                       -----
allow_plaintext_backup    false
auto_rotate_period        0s
deletion_allowed          false
derived                   false
exportable                false
imported_key              false
keys                      map[1:1701353983]
latest_version            1
min_available_version     0
min_decryption_version    1
min_encryption_version    0
name                      micho
supports_decryption       true
supports_derivation       true
supports_encryption       true
supports_signing          false
type                      aes256-gcm96
```

上記の通り、暗号アルゴリズムは、デフォルトでAES256-GCM96が設定されます。

では、実際にトランジットシークレットエンジンを使い、とある文字列を暗号化してみます。入力はbase64でエンコードされている必要があります。

```
$ vault write transit/encrypt/micho plaintext=$(echo "hello new world!" | base64)
Key            Value
---            -----
ciphertext     vault:v1:JDLegPK2XvGkpn2rojksRVOi0vLz7mAbVbuf2t5h2Qx8OWx9MUsYgfk0UGKH
key_version    1
```

今度は、これを復号してみましょう。

```
$ vault write transit/decrypt/micho ciphertext=vault:v1:JDLegPK2XvGkpn2rojksRVOi0vLz7mAbVbuf2t5h2Qx8OWx9MUsYgfk0UGKH
Key          Value
---          -----
plaintext    aGVsbG8gbmV3IHdvcmxkIQo=

$ echo aGVsbG8gbmV3IHdvcmxkIQo= | base64 -d
hello new world!
```

無事に復号できました。

# トランジットシークレットエンジンを暗号鍵生成機として利用する

トランジットシークレットエンジンには、内部で(暗号論的安全な)乱数を生成し、それを暗号化したものを一発で出力する機能があります。

なにかしらのデータを、共通鍵暗号(AESなど)を用いて暗号化する際の暗号鍵(DEK=Data Encryption Key)を生成するような場合にこの機能が使えるでしょう。

この場合、トランジットシークレットエンジンは、DEKを暗号化する暗号鍵(KEK=Key Encryption Key)として振る舞うことになります。

例えば、AES256に使用する暗号鍵を生成したい場合は、以下のようなコマンドになります。

```
$ vault write -f transit/datakey/wrapped/micho bits=256
Key            Value
---            -----
ciphertext     vault:v1:LHadAmu31hqZ/3UcB9kisjJ+7sKiVB4bNtgJ+ZW0CSEJeKBiGuhq7kt6W8sfy44mivL74Y2JUb+2OXZS
key_version    1
```

この暗号化された文字列をデータベースなどに保存しておき、実際に使用する時に、復号して生の暗号鍵を取り出します。復号はさっきと同じです。

```
$ vault write transit/decrypt/micho ciphertext=vault:v1:LHadAmu31hqZ/3UcB9kisjJ+7sKiVB4bNtgJ+ZW0CSEJeKBiGuhq7kt6W8sfy44mivL74Y2JUb+2OXZS
Key          Value
---          -----
plaintext    ZnA/ewaQAQiKH+B7BDIOhtFYLv8SrcH11IXnH/OytFs=

$ echo 'ZnA/ewaQAQiKH+B7BDIOhtFYLv8SrcH11IXnH/OytFs=' | base64 -d | hexdump -C
00000000  66 70 3f 7b 06 90 01 08  8a 1f e0 7b 04 32 0e 86  |fp?{.......{.2..|
00000010  d1 58 2e ff 12 ad c1 f5  d4 85 e7 1f f3 b2 b4 5b  |.X.............[|
```

この32バイト(256ビット)のバイナリ列をDEKとして利用することになります。

# まとめ

Vaultは非常に多機能で、ここで全てを紹介しきれませんが、さくらのクラウド上でVaultクラスタを組み、簡単な利用方法を説明しました。

ここまでお読みいただきありがとうございました。

# 社会を支えるパブリッククラウドを一緒に作りませんか？

ところで先日、「さくらのクラウド」はデジタル庁よりガバメントクラウドとして条件付き認定をいただきました。ひとえに皆様のご支援のおかげです。改めて感謝申し上げます。

https://www.sakura.ad.jp/corporate/information/newsreleases/2023/11/28/1968214232/

https://www.digital.go.jp/news/67d21847-46e8-4482-a6fe-97ab07590143

ここに記載のあるとおり、2025年度末までに全ての技術要件を満たすことが条件となっています。これから数年がかりで、機能を充足するため開発を推進し、サービス運営体制を整備していくことになります。

この大きなチャレンジを是非一緒にやってみませんか！？

[![sakura.54060f72.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/5377ecb4-941b-e505-a1c9-9f2de6b16398.png)](https://www.sakura.ad.jp/recruit/)

ご興味ありましたら、こちらに求人情報掲載しておりますのでご覧ください。
カジュアル面談も実施していますので、気軽にお声がけいただけるとうれしいです :smile: