本文へ移動
購読
GUIDE#security更新 2026.10.18

HashiCorp Vaultの使い方:スクリプトの秘密情報をAppRoleで渡す

公式イメージのVault 2.2.0で検証。-devモードとの違い、Raftの初期化とアンシール、KV v2の版と巻き戻し、1つのパスだけ読めるAppRoleでスクリプトに秘密情報を渡す手順と、監査ログでの確認まで。

level
中級
time
約60分
stack
hashicorp/vault:2.2.0(Vault v2.2.0 2891bfd2, built 2026-10-14、Alpine Linux 3.24.2。サーバーとクライアントで同じイメージ) / 統合ストレージ(Raft)1台構成 / jq 1.8.2(Alpine apk) / Docker Engine 29.1.3
author
@ryoma_t
// 前提条件
Dockerが動くLinuxホスト(サーバー用に512MiB、クライアント用に256MiBのメモリ上限で動かします) / Docker Hubからイメージを取得できること / シェルスクリプトとcronの基本
AppRoleでVaultにログインしたスクリプト用トークンが自分のパスだけを読み、別のパスの読み取りは403で拒否されるターミナル画面
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

HashiCorp Vaultとは、パスワードやAPIキーなどの秘密情報を暗号化して保存し、どのプログラムがどのパスを読めるかをポリシーで決め、読んだ記録を監査ログに残すサーバーソフトウェアです。人がブラウザで開いて使うパスワードマネージャーとは役割が違い、スクリプトやアプリケーションがAPI経由で値を受け取る場面のための道具です。公式Dockerイメージhashicorp/vault:2.2.0(Vault v2.2.0)を使い、バックアップスクリプトが.envファイルから読んでいたDBのパスワードを、AppRoleという認証方式でVaultから受け取る形に置き換えるところまでを実際に動かしました。

対象は、cronで動くシェルスクリプトや社内ツールのために、DBやAPIのパスワードをサーバー上の.envに平文で置いている情シス・インフラ担当の方です。読み終えると、再起動のたびに必要になるアンシール作業の重さを確かめたうえで、スクリプト1本に1つのパスだけを短時間読ませる設定、拒否されたときに出るエラー、監査ログで「いつ・どのロールが・どこから読んだか」を確かめるコマンドが手元に残ります。最後に、VaultとOpenBao、クラウドのシークレット管理サービス、パスワードマネージャー、権限600の.envのどれを選ぶかの判断基準を置きました。

この記事でわかること

  • -devモードで書いた値が再起動で消えることと、統合ストレージ(Raft)の1台構成を初期化・アンシールする手順、再起動後にシールされた状態の実際
  • KVバージョン2に版が残る仕組みと、vault kv rollbackで前の値に戻したときに起きること
  • 1つのパスだけを読めるポリシーとAppRoleの設定値、スクリプトのトークンが拒否される操作(別のパス・書き込み・期限切れ)
  • 監査ログで読み出しを追う方法と、値がHMACで記録される仕組み、ログを書けないとVaultが要求を断ること
  • mlock、ロックアウト、使用回数の制限で実際に出たエラーと直し方、Vault・OpenBao・クラウド・パスワードマネージャー・.envの選び方

検証環境と、この記事で使う名前

検証は、このサイトの検証用サーバーのDocker Engine 29.1.3で行いました。イメージはHashiCorp公式のhashicorp/vault:2.2.0で、執筆時点ではDocker Hubのlatestと同じものです。サーバー役とスクリプト役の2つのコンテナを同じイメージから起動し、スクリプト側もイメージに入っているvaultコマンドをそのまま使います。実機のLinuxにCLIだけを入れる手順(HashiCorpのパッケージリポジトリを使う方法など)は、この環境では実行していません。

サーバー役のコンテナのホスト名は nagoyadevlab02、スクリプトを動かす側は nagoyadevlab01、スクリプトを実行するOSユーザーは ryoma です。どれも検証用に付けた名前なので、コマンドや出力に出てくるこれらの名前、hashicorp-vault-approle-secrets-で始まるコンテナ・ボリューム・ネットワーク名(検証サーバー上で他の作業と区別するための接頭辞です)、kv/backup/dbなどのパス、ポリシー名backup-read、ロール名backup-script、/home/ryomaのようなパスは、自分の環境の値に読み替えてください。

記事に出てくるアンシールキー、rootトークン、パスワード(Lab-Pass-0001など)は、この検証のために作り、検証後にコンテナとボリュームごと削除した使い捨ての値です。トークン、アクセサー、role_id、secret_id、アンシールキー、コンテナやネットワークのID、時刻、トークンの残り時間(ttl)、IPアドレス、HMACの値は実行のたびに変わるので、出力から写して使う値ではありません。コンテナは--privilegedなしで、メモリ上限はサーバー512MiB、クライアント256MiBにし、Vaultの8200番ポートはホストに公開していません。2つのコンテナは専用のDockerネットワークでだけつながります。

ホストで実行するのは次のコマンドです。最後のdocker image inspectは、このイメージがvaultユーザーで動き、引数なしならserver -devで起動し、/vault/fileと/vault/logsをボリュームとして宣言していることを確かめるためのものです。この3点は後の設定に効いてきます。

terminal(ホスト)
docker version --format '{{.Server.Version}}'
29.1.3
docker pull -q hashicorp/vault:2.2.0
docker.io/hashicorp/vault:2.2.0
docker run --rm hashicorp/vault:2.2.0 version
Vault v2.2.0 (2891bfd23893ebd302307050edaf03d871ea8351), built 2026-10-14T15:24:08Z
docker image inspect hashicorp/vault:2.2.0 --format '{{.Config.User}} {{json .Config.Cmd}} {{json .Config.Volumes}}'
vault ["server","-dev"] {"/vault/file":{},"/vault/logs":{}}
docker network create hashicorp-vault-approle-secrets-net
4539dc0574e8611abc3a54fbda894ea6b1e87d38719dd26fe0980fe1f4179fd4
docker volume create hashicorp-vault-approle-secrets-data
hashicorp-vault-approle-secrets-data
docker volume create hashicorp-vault-approle-secrets-logs
hashicorp-vault-approle-secrets-logs

-devモードは「コマンドを試す場所」:再起動で値が消える

-devモードとは、vault server -devで起動する練習用のサーバーで、データをメモリだけに持ち、起動した時点で初期化とアンシールが済んだ状態になります。公式イメージは引数なしで起動するとこのモードになるので、まずその性質を確かめます。ログが出そろうまで少しかかるので、docker runのあと数秒おいてからdocker logsを実行します。

terminal(ホスト)
docker run -d --name hashicorp-vault-approle-secrets-dev --memory 256m hashicorp/vault:2.2.0
ebf55e97301bb847cad34e1a0d0ae51df22b7ea2b41d73e93e0402a6ed85906b
docker logs hashicorp-vault-approle-secrets-dev 2>&1 | grep -E 'Storage:|Mlock:|WARNING|Unseal Key:|Root Token:'
                   Mlock: supported: true, enabled: false
                 Storage: inmem
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
Unseal Key: g2Ao8P78umw5RYorsraCY2quk2Ah7GTIasPZrm8eqX0=
Root Token: hvs.HQJEWfgsaLAIK6wvCIChPTWq

Storage: inmemがメモリ上の保存先を示しています。アンシールキーとrootトークンはdocker logsにそのまま出るので、そのホストでdockerを操作できる人なら誰でもrootトークンを読めます。次に値を1つ書き、コンテナを再起動してから読み直します。再起動のあとは、起動が終わるまで少し待ってから次のコマンドを実行してください。

terminal(ホスト・再起動後の読み取りはエラー)
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv put secret/try hello=world
= Secret Path =
secret/data/try

======= Metadata =======
Key                Value
---                -----
created_time       2026-10-18T06:21:35.332593295Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv get -field=hello secret/try
world
docker restart hashicorp-vault-approle-secrets-dev
hashicorp-vault-approle-secrets-dev
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv get secret/try
No value found at secret/data/try
docker logs hashicorp-vault-approle-secrets-dev 2>&1 | grep 'Root Token:'
Root Token: hvs.HQJEWfgsaLAIK6wvCIChPTWq
Root Token: hvs.Q88QIreC6L1AmvdZpBBGqvwC
docker rm -fv hashicorp-vault-approle-secrets-dev
hashicorp-vault-approle-secrets-dev

再起動後はNo value foundになり、ログにはrootトークンが2つ並びました。再起動のたびにまっさらな別のVaultが立ち上がるということです。-devモードはコマンドやポリシーの書き方を試すためだけに使い、値を入れたら消える前提で扱います。docker rmに-vを付けているのは、イメージが宣言している2つのボリュームが名前なしで作られ、付けないと残るからです。

サーバーの設定ファイルvault.hclを1行ずつ決める

本番と同じ動かし方は、設定ファイルを渡してserverで起動する形です。ホストの作業用ディレクトリで、次のコマンドでvault.hclを作ります(このファイルにはパスワードなどの秘密は入りません)。

terminal(ホスト・作業ディレクトリ)
cat > vault.hcl <<'EOF'
disable_mlock = true

storage "raft" {
  path    = "/vault/file"
  node_id = "nagoyadevlab02"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = true
}

api_addr     = "http://nagoyadevlab02:8200"
cluster_addr = "https://nagoyadevlab02:8201"
EOF
  • disable_mlock = true:Vault 2.2.0は、統合ストレージ(Raft)を使うときにこの値がtrueかfalseのどちらかに決まっていないと起動しません。falseにしてメモリのロック(mlock)を使おうとすると、既定のvaultユーザーで動くこのイメージでは、--cap-add IPC_LOCKを付けてもFailed to lock memoryで止まりました(どちらのエラーも「つまずきポイント」に全文を載せています)。trueにした代わりに、Vaultのメモリの中身がスワップに書き出される可能性が残ります。ホストでswapon --showを実行し、スワップがあるなら暗号化するか無効にするかを決めてください。
  • storage "raft":Vault自身がデータを持つ統合ストレージです。データはボリュームhashicorp-vault-approle-secrets-data(コンテナ内の/vault/file)に置きます。node_idはホスト名にそろえました。1台構成なので、このコンテナが止まればスクリプトは値を受け取れません。
  • listener "tcp"のaddress = "0.0.0.0:8200":コンテナの中のすべてのアドレスで待ち受けます。ホストには-pで公開しないので、届くのは同じDockerネットワークにいるコンテナだけです。
  • tls_disable = true:検証のために平文のHTTPにしています。Vaultは既定ではTLSを前提にしていて、この1行で明示的に外しています。
  • api_addrとcluster_addr:クライアントから見たこのノードのURLと、ノード間の通信に使うURLです。どちらもDockerネットワーク上の名前nagoyadevlab02で書きました。
  • 書かなかった設定:ui(ブラウザの管理画面。既定は無効で、スクリプト用途には要らないので有効にしていません)と、ポートのホストへの公開です。

tls_disable = trueのままでは、secret_idもトークンも取り出したパスワードも平文でネットワークを流れます。この検証ではホストの外に出ないDockerネットワークに閉じているので外しましたが、サーバーとスクリプトが別のマシンになる実運用では、listener "tcp"にtls_cert_fileとtls_key_fileで証明書を指定してください。TLSの設定はこの環境では実行していません。

サーバーとスクリプト用のコンテナを起動する

サーバーは名前付きボリュームを2つ(データ用と監査ログ用)付け、vault.hclを読み取り専用でマウントしてserverで起動します。スクリプト用のコンテナは同じイメージをsleep infinityで起動しておき、スクリプトを動かすユーザーを作ります。

terminal(ホスト・作業ディレクトリ)
docker run -d --name hashicorp-vault-approle-secrets-srv \
  --hostname nagoyadevlab02 \
  --network hashicorp-vault-approle-secrets-net --network-alias nagoyadevlab02 \
  --memory 512m \
  -e VAULT_ADDR=http://127.0.0.1:8200 \
  -v hashicorp-vault-approle-secrets-data:/vault/file \
  -v hashicorp-vault-approle-secrets-logs:/vault/logs \
  -v "$PWD/vault.hcl":/vault/config/vault.hcl:ro \
  hashicorp/vault:2.2.0 server
110ea427bd61ec8e22f6f39428db8d6f4a9f367f5ce99e7d13b01ce5cc2313d0
docker run -d --name hashicorp-vault-approle-secrets-cli \
  --hostname nagoyadevlab01 \
  --network hashicorp-vault-approle-secrets-net \
  --memory 256m \
  -e VAULT_ADDR=http://nagoyadevlab02:8200 \
  hashicorp/vault:2.2.0 sleep infinity
d19c7b0fcbc6762434f74b3a54e1360e87216fd1fb8369db4efe1f212946148d
docker exec -u root hashicorp-vault-approle-secrets-cli adduser -D ryoma
docker logs hashicorp-vault-approle-secrets-srv 2>&1 | grep -E 'Mlock:|Storage:|Version:'
              Go Version: go1.27.2
                   Mlock: supported: true, enabled: false
                 Storage: raft (HA available)
                 Version: Vault v2.2.0, built 2026-10-14T15:24:08Z
docker exec hashicorp-vault-approle-secrets-srv vault status | grep -E '^(Initialized|Sealed|Storage Type)'
Initialized             false
Sealed                  true
Storage Type            raft

ログのMlock: supported: true, enabled: falseがdisable_mlock = trueの結果です。Initialized falseとSealed trueは、まだ初期化されていない新しいサーバーの状態です。-e VAULT_ADDRはvaultコマンドの接続先で、サーバー側では自分自身(http://127.0.0.1:8200)、クライアント側ではhttp://nagoyadevlab02:8200を指します。これを省くとvaultコマンドはhttps://で接続しに行き、「つまずきポイント」の2番目のエラーになります。

以降、サーバー側の操作はdocker exec -it hashicorp-vault-approle-secrets-srv shで入ったシェル(イメージの既定ユーザーvault)、スクリプト側の操作はdocker exec -it -u ryoma -w /home/ryoma hashicorp-vault-approle-secrets-cli shで入ったシェルで実行します。ホストで実行するコマンドはブロック名に「ホスト」と書きました。記事の出力は端末を割り当てないdocker execで取ったので、対話シェルでは色が付くなど見え方が少し変わります。

初期化とアンシール:鍵5本のうち3本で開ける

初期化(vault operator init)は、データを暗号化する鍵を作り、それを開くための鍵をアンシールキーとして分割して渡す作業です。既定では5本に分割され、そのうち3本がそろうと開きます(シャミアの秘密分散)。出力にはアンシールキー5本とrootトークンがまとめて出るので、ここではサーバーのコンテナ内のvaultユーザーのホームに、本人しか読めない権限(umask 077)で保存しました。

terminal(サーバー:vaultユーザー)
umask 077; vault operator init > ~/init.txt
ls -l ~/init.txt
-rw-------    1 vault    vault          901 Oct 18 06:21 /home/vault/init.txt
grep -c '^Unseal Key' ~/init.txt
5
tail -n 10 ~/init.txt
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.

ファイルに保存したのは、この検証では1人で全部の操作をし、後で再起動したときにも同じ鍵が必要になるからです。このファイルはコンテナの書き込み層にあり、最後のdocker rm -fvでコンテナと一緒に消えます。実運用では、初期化の出力をサーバーに残さず、5本を別々の担当者に渡します。アンシールのときは、各担当者がvault operator unsealを引数なしで実行し、表示されるプロンプトに自分の鍵を入力します(入力は画面に出ません)。この対話入力は端末が要るため、この環境では実行していません。代わりに、ファイルの1〜3本目を順に渡します。

terminal(サーバー:vaultユーザー)
for i in 1 2 3; do vault operator unseal "$(awk -v n="$i:" '$3 == n {print $4}' ~/init.txt)" | grep -E '^(Sealed|Unseal Progress)'; done
Sealed                  true
Unseal Progress         1/3
Sealed                  true
Unseal Progress         2/3
Sealed                  false
vault login -no-print "$(awk '/^Initial Root Token:/ {print $4}' ~/init.txt)"
vault token lookup | grep -E '^(display_name|policies|ttl)'
display_name        root
policies            [root]
ttl                 0s
ls -l ~/.vault-token
-rw-------    1 vault    vault           28 Oct 18 06:21 /home/vault/.vault-token
vault audit enable file file_path=/vault/logs/audit.log
Success! Enabled the file audit device at: file/

1本ごとにUnseal Progressが進み、3本目でSealed falseになりました。vault login -no-printはトークンを画面に出さずに~/.vault-token(権限600)へ保存します。rootトークンはttl 0s、つまり期限がなく、すべての操作ができる最も強い資格情報なので、スクリプトには決して渡しません。監査デバイスはこの時点で有効にしました。有効にする前の操作はどこにも記録されないからです。

~/init.txtを読める人は、アンシールもroot操作も1人でできてしまいます。鍵を5本に分けた意味がなくなるので、検証以外ではこのファイルを作らないでください。担当者ごとの保管先には、共有範囲を個人単位で絞れる企業向けのパスワードマネージャーが向いています。鍵が3本集まらなくなるとデータは二度と開けません(初期化の出力にもその警告が出ています)。

KVバージョン2に秘密情報を入れ、前の版に戻す

秘密情報の置き場には、版(バージョン)を残すKVバージョン2(以下KV v2)をkv/というパスで有効にします。DBのユーザー名とパスワードを書き、パスワードを変えたつもりでもう一度書きます。

terminal(サーバー:vaultユーザー・抜粋)
vault secrets enable -path=kv kv-v2
Success! Enabled the kv-v2 secrets engine at: kv/
vault kv put kv/backup/db username=backup_user password=Lab-Pass-0001
== Secret Path ==
kv/data/backup/db

======= Metadata =======
Key                Value
---                -----
created_time       2026-10-18T06:21:55.422468118Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1
vault kv put kv/backup/db username=backup_user password=Lab-Pass-0002
version            2

書き込むたびにversionが1、2と増えます。Secret Pathがkv/data/backup/dbになっている点に注意してください。CLIではkv/backup/dbと書きますが、実際のAPIのパスにはdata/が入り、ポリシーはこちらのパスで書きます。次に、新しいパスワードをDB側にまだ反映していなかった、という想定で版1に戻します。

terminal(サーバー:vaultユーザー・抜粋)
vault kv get kv/backup/db
== Secret Path ==
kv/data/backup/db

======= Metadata =======
Key                Value
---                -----
created_time       2026-10-18T06:21:55.661008302Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            2

====== Data ======
Key         Value
---         -----
password    Lab-Pass-0002
username    backup_user
vault kv get -version=1 -field=password kv/backup/db
Lab-Pass-0001
vault kv rollback -version=1 kv/backup/db
Key                Value
---                -----
created_time       2026-10-18T06:21:56.413599854Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            3
vault kv get -field=password kv/backup/db
Lab-Pass-0001
vault kv metadata get kv/backup/db | grep -E '^(current_version|max_versions|oldest_version)'
current_version         3
max_versions            0
oldest_version          0
vault kv put kv/hr/payroll-api token=Lab-Payroll-0001
version            1

vault kv rollbackは版2を消すのではなく、版1の中身を新しい版3として書き込みました。版2の値は残っているので、あとから-version=2で読めます。max_versions 0は「設定なし」の意味で、HashiCorpのドキュメントによれば、この場合は1つのキーにつき10版を残し、それより古い版は完全に消えます。最後のkv/hr/payroll-apiは、スクリプトに読ませてはいけない別の秘密情報の例で、後で拒否されることを確かめるために入れました。

password=Lab-Pass-0001のように値をコマンド行に書くと、対話シェルの履歴に残ることがあります。この記事の値は使い捨てなのでそのまま書きましたが、本物の値はpassword=-として標準入力から渡せます。ただし、標準入力の末尾の改行もそのまま値の一部として保存されます。-devモードのコンテナで、改行付き(printf '%s\n')と改行なし(printf '%s')で渡し比べた結果が下のブロックで、1回目はod -cの最後に\nが付いています。このまま保存すると、DBに設定したパスワードとは1文字違う値になります。端末から手で入力する方法はこの環境では試していません。

terminal(ホスト)
docker run -d --name hashicorp-vault-approle-secrets-dev --memory 256m hashicorp/vault:2.2.0
ca1a0848ef474720003a8a0479fe48c79acfca33aa6610d6d35c581c02c3e763
printf '%s\n' 'Lab-Pass-0009' | docker exec -i -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv put secret/stdin-test password=- >/dev/null
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv get -field=password secret/stdin-test | od -c
0000000   L   a   b   -   P   a   s   s   -   0   0   0   9  \n
0000016
printf '%s' 'Lab-Pass-0009' | docker exec -i -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv put secret/stdin-test password=- >/dev/null
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp-vault-approle-secrets-dev vault kv get -field=password secret/stdin-test | od -c
0000000   L   a   b   -   P   a   s   s   -   0   0   0   9
0000015
docker rm -fv hashicorp-vault-approle-secrets-dev
hashicorp-vault-approle-secrets-dev

スクリプト1本に1つのパスだけを読ませるポリシー

ポリシーは「どのパスに、どの操作を許すか」をHCLで書いたものです。バックアップスクリプト用に、kv/data/backup/dbの読み取り(read)だけを許すポリシーを作ります。

terminal(サーバー:vaultユーザー)
cat > ~/backup-read.hcl <<'EOF'
path "kv/data/backup/db" {
  capabilities = ["read"]
}
EOF
vault policy write backup-read ~/backup-read.hcl
Success! Uploaded policy: backup-read

kv/の中でこのポリシーに書いていない操作は、すべて拒否されます。一覧(kv/metadata/へのlist)、書き込み(update)、メタデータの読み取りも許していません。逆に、readは同じパスの古い版の読み取りも含むことを実際に確かめました(次の節で-version=2が読めています)。パスワードを変えたあとも古い版はこのロールから読めるので、古い値をDB側で必ず無効にしてから版を残す、という順番で運用します。

AppRoleでスクリプトにログインさせる

AppRoleとは、人ではなくプログラムがVaultにログインするための認証方式です。ロールごとに決まるrole_idと、あとから何本でも発行できるsecret_idの2つを組にしてログインすると、ロールに設定したポリシーと期限を持つトークンが返ります。

terminal(サーバー:vaultユーザー)
vault auth enable approle
Success! Enabled approle auth method at: approle/
vault write auth/approle/role/backup-script token_policies=backup-read token_ttl=2m token_max_ttl=2m secret_id_ttl=720h secret_id_num_uses=0
Success! Data written to: auth/approle/role/backup-script
vault read auth/approle/role/backup-script | grep -E '^(secret_id_num_uses|secret_id_ttl|token_max_ttl|token_num_uses|token_policies|token_ttl|token_no_default_policy) '
secret_id_num_uses         0
secret_id_ttl              720h
token_max_ttl              2m
token_no_default_policy    false
token_num_uses             0
token_policies             [backup-read]
token_ttl                  2m
  • token_policies=backup-read:返すトークンに付けるポリシーです。これとは別に、自分のトークンを調べたり失効させたりするためのdefaultポリシーも付きます(token_no_default_policy false)。
  • token_ttl=2mとtoken_max_ttl=2m:トークンの寿命は2分で、延長もできません。スクリプトは起動直後に値を読むだけなので足ります。その代わり、2分を過ぎてからもう一度Vaultに問い合わせると403で拒否されます(次の節で実際に待って確かめました)。処理の途中でも値が要るスクリプトは、読み直すたびにログインし直す作りにします。
  • secret_id_ttl=720h:secret_idは720時間(30日)で失効します。期限が来るとログインがinvalid role or secret IDで拒否されるので、それより前に新しいsecret_idを配る仕組みが要ります(失効の挙動は短い期限で「つまずきポイント」に載せました)。
  • secret_id_num_uses=0:1本のsecret_idで何回でもログインできます。毎晩動くcronでは、ここを1にすると2回目の実行からログインできなくなります。
  • トークンの使用回数(token_num_uses)は0(無制限)のままにしました。制限を付けたときに何が壊れたかは「つまずきポイント」の4番目に載せています。

role_idとsecret_idは画面にも履歴にも出さずに、サーバーからスクリプト用コンテナのryomaのホームへパイプで流し込みます。ホストで実行します。実機では、このパイプの右側をスクリプトのサーバーへのssh接続の先で動かす形になりますが、ssh経由の受け渡しはこの記事では試していません。

terminal(ホスト)
docker exec hashicorp-vault-approle-secrets-srv vault read -field=role_id auth/approle/role/backup-script/role-id | docker exec -i -u ryoma hashicorp-vault-approle-secrets-cli sh -c 'umask 077; mkdir -p ~/.vault-approle; cat > ~/.vault-approle/role_id'
docker exec hashicorp-vault-approle-secrets-srv vault write -f -field=secret_id auth/approle/role/backup-script/secret-id | docker exec -i -u ryoma hashicorp-vault-approle-secrets-cli sh -c 'umask 077; cat > ~/.vault-approle/secret_id'
terminal(クライアント:ryoma)
ls -l ~/.vault-approle
total 8
-rw-------    1 ryoma    ryoma           36 Oct 18 06:21 role_id
-rw-------    1 ryoma    ryoma           36 Oct 18 06:21 secret_id

role_idとsecret_idの2つのファイルを読める人は、スクリプトとしてVaultにログインできます。AppRoleで秘密情報が手元から消えるわけではありません。変わるのは、手元にあるものが「DBのパスワードそのもの」から「1つのパスを2分だけ読めるトークンの引換券」になり、Vault側で取り消せて、使われるたびに監査ログに残る点です。ファイルの権限(600)と、そのサーバーにログインできる人の管理は.envのときと同じく必要です。ホスト側の基本的な守りはLinuxサーバーの基本ハードニングでまとめています。

.envの代わりにVaultから読むスクリプト

これまでの形は、.envにDB_PASSWORD=…を書いてスクリプトの先頭で読み込むものでした。それをVaultへのログインと読み出しに置き換えたのが次のスクリプトです。値を受け取れたことを示すために、ここではパスワードの文字数だけを表示しています。

terminal(クライアント:ryoma)
mkdir -p ~/bin
cat > ~/bin/backup-db.sh <<'EOF'
#!/bin/sh
set -eu
export VAULT_ADDR=http://nagoyadevlab02:8200
dir="$HOME/.vault-approle"
VAULT_TOKEN=$(vault write -field=token auth/approle/login role_id=@"$dir/role_id" secret_id=@"$dir/secret_id")
export VAULT_TOKEN
DB_USER=$(vault kv get -field=username kv/backup/db)
DB_PASSWORD=$(vault kv get -field=password kv/backup/db)
echo "user=$DB_USER password_length=${#DB_PASSWORD}"
vault token revoke -self
EOF
chmod 700 ~/bin/backup-db.sh
~/bin/backup-db.sh
user=backup_user password_length=13
Success! Revoked token (if it existed)
  • set -eu:vaultコマンドが1つでも失敗したらそこで止めます。Vaultがシールされているときに空のパスワードで処理を進めないためです(後の節で、シール中に終了コード2で止まるのを確かめています)。
  • export VAULT_ADDR=…:cronから動かすと環境変数がほとんど引き継がれないので、接続先はスクリプトの中に書きます。
  • role_id=@"$dir/role_id":@を付けるとファイルの中身を値として送ります。コマンド行にはファイル名しか出ないので、同じマシンの他のユーザーがプロセス一覧を見てもsecret_idは見えません。
  • VAULT_TOKEN:ログインで受け取ったトークンは、このスクリプトのプロセスの環境変数にだけ置き、ファイルには書きません。
  • vault token revoke -self:使い終わったトークンを2分の期限を待たずに失効させます。Success! Revoked tokenの行がその結果です。

スクリプトのトークンが拒否される操作

スクリプトと同じロールでログインしたトークンで、許していない操作を1つずつ試します。手で確かめやすいように、ここではトークンをvault login -no-print -で~/.vault-tokenに保存しました(標準入力からトークンを読みます)。このトークンも2分で失効します。

terminal(クライアント:ryoma・拒否のエラーを含む)
vault write -field=token auth/approle/login role_id=@$HOME/.vault-approle/role_id secret_id=@$HOME/.vault-approle/secret_id | vault login -no-print -
vault token lookup | grep -E '^(display_name|meta|policies|ttl)'
display_name        approle
meta                map[role_name:backup-script]
policies            [backup-read default]
ttl                 2m
vault kv get -field=username kv/backup/db
backup_user
vault kv get kv/hr/payroll-api
Error reading kv/data/hr/payroll-api: Error making API request.

URL: GET http://nagoyadevlab02:8200/v1/kv/data/hr/payroll-api
Code: 403. Errors:

* 1 error occurred:
	* permission denied
vault kv put kv/backup/db username=backup_user password=changed-by-script
Error writing data to kv/data/backup/db: Error making API request.

URL: PUT http://nagoyadevlab02:8200/v1/kv/data/backup/db
Code: 403. Errors:

* 1 error occurred:
	* permission denied
vault kv list kv/
Error listing kv/metadata: Error making API request.

URL: GET http://nagoyadevlab02:8200/v1/kv/metadata?list=true
Code: 403. Errors:

* 1 error occurred:
	* permission denied
vault kv get -version=2 -field=password kv/backup/db
Lab-Pass-0002
ls -l ~/.vault-token
-rw-------    1 ryoma    ryoma           95 Oct 18 06:22 /home/ryoma/.vault-token

許したkv/data/backup/dbの読み取りだけが通り、別のパス、書き込み、一覧はすべてCode: 403のpermission deniedでした。-version=2で古い版(Lab-Pass-0002)が読めているのが、前の節で書いた「readは古い版も含む」の実例です。続けて、ログインから2分を過ぎたあとに同じ読み取りをします。

terminal(クライアント:ryoma・期限切れのエラー)
sleep 125; vault kv get -field=username kv/backup/db
Error making API request.

URL: GET http://nagoyadevlab02:8200/v1/sys/internal/ui/mounts/kv/backup/db
Code: 403. Errors:

* 2 errors occurred:
	* permission denied
	* invalid token

期限が切れたトークンでは、さっき通った読み取りも403になり、理由にinvalid tokenが加わります。エラーのURLがsys/internal/ui/mounts/…になっているのは、vault kv getが値を読む前にこのパスのマウント情報を問い合わせていて、その最初の問い合わせで止まったからです。

監査ログで「どのロールが、どこから、何を読んだか」を見る

監査ログは、1行に1つのJSONが並ぶファイルです。結果は"type":"response"の行に入るので、それを絞り込みます。読みやすく取り出すために、サーバーのコンテナにjqを入れます(ホストで実行。apkはrootで動かす必要があるので-u rootを付けます)。

terminal(ホスト)
docker exec -u root hashicorp-vault-approle-secrets-srv apk add --no-cache jq
(1/2) Installing oniguruma (6.9.10-r0)
(2/2) Installing jq (1.8.2-r0)
Executing busybox-1.37.0-r31.trigger
OK: 9619 KiB in 21 packages
terminal(サーバー:vaultユーザー)
jq --version
jq-1.8.2
ls -l /vault/logs/
total 144
-rw-------    1 vault    vault       144121 Oct 18 06:24 audit.log

ログファイルはvaultユーザーだけが読める権限で、私たちの実行では、ここまでの操作だけでls -lの表示が144121バイトになりました(記録される値の長さが実行ごとに違うので、多少ずれます)。AppRoleでログインしたトークンによるkv/への応答だけを、時刻、ロール名、接続元、操作、パス、許可されたか、パスワード欄の順に取り出します。

terminal(サーバー:vaultユーザー)
jq -c 'select(.type == "response" and .auth.display_name == "approle" and (.request.path | startswith("kv/"))) | {time, role: .auth.metadata.role_name, from: .request.remote_address, op: .request.operation, path: .request.path, allowed: .auth.policy_results.allowed, password: .response.data.data.password}' /vault/logs/audit.log
{"time":"2026-10-18T06:22:00.566982338Z","role":"backup-script","from":"172.19.0.3","op":"read","path":"kv/data/backup/db","allowed":true,"password":"hmac-sha256:c4063d647354a15bd780d598a497b2c010a0ac5077f7b68ebe1b12a027f85878"}
{"time":"2026-10-18T06:22:00.641639915Z","role":"backup-script","from":"172.19.0.3","op":"read","path":"kv/data/backup/db","allowed":true,"password":"hmac-sha256:c4063d647354a15bd780d598a497b2c010a0ac5077f7b68ebe1b12a027f85878"}
{"time":"2026-10-18T06:22:01.734858882Z","role":"backup-script","from":"172.19.0.3","op":"read","path":"kv/data/backup/db","allowed":true,"password":"hmac-sha256:c4063d647354a15bd780d598a497b2c010a0ac5077f7b68ebe1b12a027f85878"}
{"time":"2026-10-18T06:22:02.047593725Z","role":"backup-script","from":"172.19.0.3","op":"read","path":"kv/data/hr/payroll-api","allowed":false,"password":null}
{"time":"2026-10-18T06:22:02.366520018Z","role":"backup-script","from":"172.19.0.3","op":"update","path":"kv/data/backup/db","allowed":false,"password":null}
{"time":"2026-10-18T06:22:02.686814491Z","role":"backup-script","from":"172.19.0.3","op":"list","path":"kv/metadata/","allowed":false,"password":null}
{"time":"2026-10-18T06:22:02.996905847Z","role":"backup-script","from":"172.19.0.3","op":"read","path":"kv/data/backup/db","allowed":true,"password":"hmac-sha256:98a5e86579a2f639e667980cc7ccb0ae7aa2265a42427265cd2c594ae2684b78"}

スクリプトの2回の読み取り、手で試した読み取り、拒否された3つの操作("allowed":false)、古い版の読み取りが、ロール名backup-scriptと接続元のIPアドレス(スクリプト用コンテナ)付きで残っています。パスワードは平文ではなくhmac-sha256:…で記録されます。HMACの計算には監査デバイスごとのソルト(鍵)が使われるので、ログのファイルだけからは元の値を計算できない作りです。一方で、値がわかっている管理者はsys/audit-hashで同じハッシュを計算し、どの版の値が読まれたかを確かめられます。

terminal(サーバー:vaultユーザー)
vault write -field=hash sys/audit-hash/file input=Lab-Pass-0001
hmac-sha256:c4063d647354a15bd780d598a497b2c010a0ac5077f7b68ebe1b12a027f85878

Lab-Pass-0001のハッシュは上の1〜3行目と一致し、最後の行(-version=2で読んだLab-Pass-0002)だけが違う値です。

監査ログを書けないと、Vaultは要求を断る

監査デバイスが1つだけのときは、記録できなかったリクエストは処理されなくなります。容量1MiBのtmpfsを監査ログの置き場にした-devモードのコンテナで、ディスクを埋めて確かめました(ホストで実行)。--tmpfsのuid=100,gid=1000は、最初のidで表示されるこのイメージのvaultユーザーのIDで、Vaultがtmpfsに書けるようにするためのものです。

terminal(ホスト・容量不足のエラーを含む)
docker run --rm hashicorp/vault:2.2.0 id
uid=100(vault) gid=1000(vault) groups=1000(vault),1000(vault)
docker run -d --name hashicorp-vault-approle-secrets-dev --memory 256m --tmpfs /vault/logs:size=1m,uid=100,gid=1000,mode=0700 -e VAULT_ADDR=http://127.0.0.1:8200 hashicorp/vault:2.2.0
b1e18d2d690b72d1ca742cfc38a6a0b7bbc36ca40dd2de3f5eca33829381249c
docker exec hashicorp-vault-approle-secrets-dev vault audit enable file file_path=/vault/logs/audit.log
Success! Enabled the file audit device at: file/
docker exec hashicorp-vault-approle-secrets-dev vault kv put secret/probe ok=yes
== Secret Path ==
secret/data/probe

======= Metadata =======
Key                Value
---                -----
created_time       2026-10-18T06:24:14.942878689Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1
docker exec hashicorp-vault-approle-secrets-dev dd if=/dev/zero of=/vault/logs/filler bs=64k
dd: error writing '/vault/logs/filler': No space left on device
16+0 records in
15+0 records out
1040384 bytes (1016.0KB) copied, 0.001272 seconds, 780.0MB/s
docker exec hashicorp-vault-approle-secrets-dev vault kv get -field=ok secret/probe
Error making API request.

URL: GET http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret/probe
Code: 500. Errors:

* internal error
docker logs hashicorp-vault-approle-secrets-dev 2>&1 | grep 'no space left' | tail -n 1
  | error writing file for sink "/vault/logs/audit.log": unable to re-write to file for sink "/vault/logs/audit.log": write /vault/logs/audit.log: no space left on device
docker exec hashicorp-vault-approle-secrets-dev rm /vault/logs/filler
docker exec hashicorp-vault-approle-secrets-dev vault kv get -field=ok secret/probe
yes
docker rm -fv hashicorp-vault-approle-secrets-dev
hashicorp-vault-approle-secrets-dev

空きがなくなると、読み取りはCode: 500のinternal errorになり、Vaultのログにはno space left on deviceが出ました。空きを作ると、再起動しなくても同じコマンドが通りました。監査ログのボリュームが一杯になった時点で、すべてのスクリプトが値を受け取れなくなるということです。監査ログを置くボリュームは、ディスク使用量を監視の対象に入れ、古いログを別の場所に移す手順を決めておきます。

再起動するとシールされる:運用で払うコスト

Shamir方式のアンシールを選んだ場合の一番大きな運用コストがこれです。サーバーのコンテナを再起動し、少し待ってから状態を見て、スクリプトを動かします。

terminal(ホスト)
docker restart hashicorp-vault-approle-secrets-srv
hashicorp-vault-approle-secrets-srv
docker exec hashicorp-vault-approle-secrets-srv vault status | grep -E '^(Initialized|Sealed|Total Shares|Threshold|Unseal Progress)'
Initialized             true
Sealed                  true
Total Shares            5
Threshold               3
Unseal Progress         0/3
terminal(クライアント:ryoma・シール中のエラー)
~/bin/backup-db.sh
Error writing data to auth/approle/login: Error making API request.

URL: PUT http://nagoyadevlab02:8200/v1/auth/approle/login
Code: 503. Errors:

* Vault is sealed

再起動したVaultはSealed true、Unseal Progress 0/3に戻り、スクリプトはCode: 503のVault is sealedでログインの段階から止まりました。Vaultのドキュメントにも、アンシールした状態はサーバーの再起動で終わると書かれています。ホストの再起動やDockerの更新、メモリ不足によるコンテナの強制終了でVaultのプロセスが立ち上げ直されれば同じ状態になり、3人分の鍵がそろうまでスクリプトは失敗し続けます。鍵を入れ直すと元に戻ります。

terminal(サーバー:vaultユーザー)
for i in 1 2 3; do vault operator unseal "$(awk -v n="$i:" '$3 == n {print $4}' ~/init.txt)" | grep -E '^(Sealed|Unseal Progress)'; done
Sealed                  true
Unseal Progress         1/3
Sealed                  true
Unseal Progress         2/3
Sealed                  false
terminal(クライアント:ryoma)
~/bin/backup-db.sh
user=backup_user password_length=13
Success! Revoked token (if it existed)

夜間のバッチが止まっても朝までに3人が集まれない体制なら、鍵の入力を信頼できる外部の装置やサービス(HSM、クラウドのKMS、別のVaultのTransit)に任せるauto unsealを検討します。auto unsealはこの記事では試していません。どちらも用意できないなら、「シール中はスクリプトが失敗して通知が来る」ことを前提に、失敗を検知する仕組みを先に作ります。

動作確認

ここまでの設定が効いていることを、次の順に確かめます。まずホストからサーバーの状態を見ます。Sealed false、Storage Type raft、Version 2.2.0が出れば、Raftのサーバーがアンシールされています。

terminal(ホスト)
docker exec hashicorp-vault-approle-secrets-srv vault status | grep -E '^(Sealed|Storage Type|Version)'
Sealed                  false
Version                 2.2.0
Storage Type            raft

スクリプト用コンテナで、スクリプトが値を受け取れることと、同じロールのトークンが別のパスを読めないことを確かめます。1つ目はuser=backup_user password_length=13とSuccess! Revoked token、2つ目はCode: 403とpermission deniedが期待どおりの結果です。

terminal(クライアント:ryoma・2つ目はエラーが期待どおり)
~/bin/backup-db.sh
user=backup_user password_length=13
Success! Revoked token (if it existed)
vault write -field=token auth/approle/login role_id=@$HOME/.vault-approle/role_id secret_id=@$HOME/.vault-approle/secret_id | vault login -no-print -
vault kv get -field=token kv/hr/payroll-api
Error reading kv/data/hr/payroll-api: Error making API request.

URL: GET http://nagoyadevlab02:8200/v1/kv/data/hr/payroll-api
Code: 403. Errors:

* 1 error occurred:
	* permission denied

サーバーで、拒否された読み取りが監査ログに残っていることを確かめます。「拒否される操作」の節で1回、いま1回試したので、falseの行が2行出ます。

terminal(サーバー:vaultユーザー)
jq -r 'select(.type == "response" and .auth.metadata.role_name == "backup-script" and .request.path == "kv/data/hr/payroll-api") | [.time, .request.remote_address, .auth.policy_results.allowed] | @tsv' /vault/logs/audit.log
2026-10-18T06:22:02.047593725Z	172.19.0.3	false
2026-10-18T06:24:23.352176477Z	172.19.0.3	false

最後にメモリの使用量です。私たちの実行ではサーバーが63.91MiBで、上限512MiBに対して余裕があり、クライアントのコンテナはsleepしているだけなのでわずかです。

terminal(ホスト)
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' hashicorp-vault-approle-secrets-srv hashicorp-vault-approle-secrets-cli
hashicorp-vault-approle-secrets-srv 63.91MiB / 512MiB
hashicorp-vault-approle-secrets-cli 2.418MiB / 256MiB

つまずきポイント

「disable_mlock must be configured」で起動しない(IPC_LOCKを足しても直らない)

vault.hclからdisable_mlockの行を外した設定と、falseにした設定を、使い捨てのコンテナで起動してみます。作業用ディレクトリ(vault.hclがある場所)で実行します。

terminal(ホスト・作業ディレクトリ)
sed '/^disable_mlock/d' vault.hcl > vault-test.hcl
docker run --rm --name hashicorp-vault-approle-secrets-test --memory 512m -v "$PWD/vault-test.hcl":/vault/config/vault.hcl:ro hashicorp/vault:2.2.0 server
ERROR: disable_mlock must be configured 'true' or 'false': Mlock prevents
memory from being swapped to disk for security reasons, but can cause Vault
on Integrated Storage to run out of memory if the machine was not provisioned
with enough RAM. Disabling mlock can prevent this issue, but can result in
the reveal of plaintext secrets when memory is swapped to disk, so it is only
recommended on systems with encrypted swap or where swap is disabled.Prior
to Vault 1.20, disable_mlock defaulted to 'false' when thevalue was not set
explicitly.
2026-10-18T06:24:26.219Z [INFO]  proxy environment: http_proxy="" https_proxy="" no_proxy=""
sed 's/^disable_mlock = true/disable_mlock = false/' vault.hcl > vault-test.hcl
docker run --rm --name hashicorp-vault-approle-secrets-test --memory 512m --cap-add IPC_LOCK -v "$PWD/vault-test.hcl":/vault/config/vault.hcl:ro hashicorp/vault:2.2.0 server
2026-10-18T06:24:26.891Z [INFO]  proxy environment: http_proxy="" https_proxy="" no_proxy=""
2026-10-18T06:24:26.901Z [INFO]  incrementing seal generation: generation=1
Error initializing core: Failed to lock memory: cannot allocate memory

This usually means that the mlock syscall is not available.
Vault uses mlock to prevent memory from being swapped to
disk. This requires root privileges as well as a machine
that supports mlock. Please enable mlock on your system or
disable Vault from using it. To disable Vault from using it,
set the `disable_mlock` configuration option in your configuration
file.
docker run --rm --name hashicorp-vault-approle-secrets-test --memory 512m --user 0 --cap-add IPC_LOCK -v "$PWD/vault-test.hcl":/vault/config/vault.hcl:ro hashicorp/vault:2.2.0 server
/usr/local/bin/docker-entrypoint.sh: line 97: setcap: not found
rm vault-test.hcl

1つ目は設定の書き漏れで、メッセージのとおりdisable_mlockをtrueかfalseに決めれば先に進みます。2つ目と3つ目は、mlockを使いたくて--cap-add IPC_LOCKを付けた場合です。既定のユーザーvaultのままではFailed to lock memory、rootで起動するとエントリーポイントがsetcapを見つけられず終了コード127で止まりました。私たちが試した範囲では、disable_mlock = trueにしてホストのスワップ側で対策するのが現実的な選択です。

「http: server gave HTTP response to HTTPS client」

TLSを外したサーバーに対して、VAULT_ADDRを設定していないシェルでvaultコマンドを使うと出ます。接続先が既定のhttps://127.0.0.1:8200になり、TLSを外したサーバーがHTTPで答えるためです。サーバーのコンテナで環境変数を外して再現しました。

terminal(ホスト)
docker exec hashicorp-vault-approle-secrets-srv env -u VAULT_ADDR vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": http: server gave HTTP response to HTTPS client

export VAULT_ADDR=http://ホスト名:8200を設定すれば直ります。cronのように環境変数を引き継がない実行方法では、スクリプトの中に書きます。TLSを設定したサーバーならhttps://で指定します。

secret_idの使い回しで「invalid role or secret ID」、続けてロックアウト

1回しか使えないsecret_id(num_uses=1)を発行し、それで6回ログインを試します。1回目は成功してトークンの寿命(秒)が表示され、2回目以降は拒否されます。そのあと、正しいsecret_idでスクリプトを動かします。

terminal(ホスト)
docker exec hashicorp-vault-approle-secrets-srv vault write -field=secret_id auth/approle/role/backup-script/secret-id num_uses=1 | docker exec -i -u ryoma hashicorp-vault-approle-secrets-cli sh -c 'umask 077; cat > ~/.vault-approle/secret_id.once'
terminal(クライアント:ryoma)
for i in 1 2 3 4 5 6; do echo "try $i: $(vault write -field=token_duration auth/approle/login role_id=@$HOME/.vault-approle/role_id secret_id=@$HOME/.vault-approle/secret_id.once 2>&1 | grep -v '^$' | tail -n 1)"; done
try 1: 120
try 2: * invalid role or secret ID
try 3: * invalid role or secret ID
try 4: * invalid role or secret ID
try 5: * invalid role or secret ID
try 6: * invalid role or secret ID
~/bin/backup-db.sh
Error writing data to auth/approle/login: Error making API request.

URL: PUT http://nagoyadevlab02:8200/v1/auth/approle/login
Code: 403. Errors:

* permission denied

正しいsecret_idなのにpermission deniedになりました。原因はユーザーロックアウトです。Vaultのドキュメントによれば、AppRoleでは既定でログインに5回失敗するとそのロール(role_id)が15分ロックされます。ロックはsecret_idではなくrole_idに掛かります(下のロック一覧のalias_identifiersはrole_idで、解除にもrole_idを指定します)。同じロールを共有しているスクリプトは同じrole_idでログインするので、どれもログインできなくなります。サーバーのログとロック一覧で確かめ、解除します。

terminal(ホスト)
docker logs hashicorp-vault-approle-secrets-srv 2>&1 | grep 'locked out' | tail -n 1
2026-10-18T06:24:29.618Z [ERROR] core: login attempts exceeded, user is locked out: request_path=auth/approle/login
terminal(サーバー:vaultユーザー)
vault read sys/locked-users
Key             Value
---             -----
by_namespace    [map[counts:1 mount_accessors:[map[alias_identifiers:[890f95b3-c2fe-90a2-e273-81f2745a0933] counts:1 mount_accessor:auth_approle_2a63cd03]] namespace_id:root namespace_path:]]
total           1
vault write -f "sys/locked-users/$(vault auth list -format=json | jq -r '."approle/".accessor')/unlock/$(vault read -field=role_id auth/approle/role/backup-script/role-id)"
Success! Data written to: sys/locked-users/auth_approle_2a63cd03/unlock/890f95b3-c2fe-90a2-e273-81f2745a0933
terminal(クライアント:ryoma)
~/bin/backup-db.sh
user=backup_user password_length=13
Success! Revoked token (if it existed)
rm ~/.vault-approle/secret_id.once

解除コマンドのauth_approle_…(AppRoleのマウントのアクセサー)とrole_idは環境ごとに違うので、上のようにvault auth listとvault readで組み立てています。期限切れのsecret_idも同じinvalid role or secret IDになります。10秒で切れるsecret_idを発行し、90秒待ってから試した結果です。

terminal(ホスト)
docker exec hashicorp-vault-approle-secrets-srv vault write -field=secret_id auth/approle/role/backup-script/secret-id ttl=10s | docker exec -i -u ryoma hashicorp-vault-approle-secrets-cli sh -c 'umask 077; cat > ~/.vault-approle/secret_id.short'
terminal(クライアント:ryoma)
sleep 90; vault write -field=token_duration auth/approle/login role_id=@$HOME/.vault-approle/role_id secret_id=@$HOME/.vault-approle/secret_id.short
Error writing data to auth/approle/login: Error making API request.

URL: PUT http://nagoyadevlab02:8200/v1/auth/approle/login
Code: 400. Errors:

* invalid role or secret ID
rm ~/.vault-approle/secret_id.short

対策は2つです。ログインがCode: 400で失敗したら、スクリプトは再試行せずに終了して通知を出すこと。もう1つは、スクリプトやサーバーごとにロールを分け、1つのロックが他を巻き込まないようにすることです。

トークンの使用回数を制限したら、スクリプトが最後に失敗する

「スクリプトは2回読むだけだから」とtoken_num_uses=2を付けると、次のようになります。

terminal(サーバー:vaultユーザー)
vault write auth/approle/role/backup-script token_num_uses=2
Success! Data written to: auth/approle/role/backup-script
terminal(クライアント:ryoma)
~/bin/backup-db.sh; echo "exit=$?"
user=backup_user password_length=13
Error revoking token: Error making API request.

URL: PUT http://nagoyadevlab02:8200/v1/auth/token/revoke-self
Code: 403. Errors:

* 2 errors occurred:
	* permission denied
	* invalid token


exit=2
terminal(サーバー:vaultユーザー)
vault write auth/approle/role/backup-script token_num_uses=0
Success! Data written to: auth/approle/role/backup-script

2回の読み取りで使用回数を使い切り、3回目のリクエストであるvault token revoke -selfがinvalid tokenで拒否されて、スクリプトは終了コード2で終わりました。トークンの照会や失効もすべて1回に数えられるので、スクリプトに1行足しただけで壊れます。この記事では0(無制限)に戻し、2分の期限と最後の失効で範囲を絞っています。

HashiCorp Vault・OpenBao・クラウド・パスワードマネージャー・.envの選び方

選ぶ前に、ライセンスの事実を押さえておきます。2023年8月10日、HashiCorpはそれ以降のリリースのライセンスをMPL 2.0からBusiness Source License(BSL)v1.1に変えると発表しました。検証に使ったイメージに入っているライセンス文書を見ると、条件は次のとおりです。

terminal(ホスト)
docker run --rm hashicorp/vault:2.2.0 grep -E '^(Licensor|Licensed Work|Change Date|Change License):' /usr/share/doc/vault/LICENSE.txt
Licensor:             International Business Machines Corporation (IBM)
Licensed Work:        Vault Version 1.15.0 or later. The Licensed Work is (c) 2024
Change Date:          Four years from the date the Licensed Work is published.
Change License:       MPL 2.0
docker run --rm hashicorp/vault:2.2.0 grep -A4 '^Additional Use Grant' /usr/share/doc/vault/LICENSE.txt
Additional Use Grant: You may make production use of the Licensed Work, provided
                      Your use does not include offering the Licensed Work to third
                      parties on a hosted or embedded basis in order to compete with 
                      IBM Corp's paid version(s) of the Licensed Work. For purposes 
                      of this license:

Licensorは買収後のIBMで、Vault 1.15.0以降が対象です。社内のスクリプトに秘密情報を渡す使い方は、「IBMの有償版と競合する形で第三者にホスト・組み込み提供する」には当たらないと私たちは読みましたが、Vaultを組み込んだサービスを社外に提供する場合は、全文を法務と確認してください。OpenBaoは、Linux FoundationのOpenSSFが管理するVaultのフォークで、OSIが承認したオープンソースライセンスで提供することを掲げ、リポジトリのライセンスはMPL-2.0です(いずれも執筆時点、プロジェクトのサイトとリポジトリの表示)。この記事ではOpenBaoを動かしていないので、コマンドや設定の互換性は確かめていません。マネージドサービスのHCP Vault Secretsについては、2025年6月30日以降は新規顧客に販売せず、サービスを終了する(sunset)とHashiCorpのドキュメントのページに書かれていました。執筆時点ではそのURLがHCP Vault Dedicatedの概要ページに転送されるため、同じURLのアーカイブで文面を確認しています。終了日はそのページに書かれていなかったので、ここでは挙げません。

4つの選択肢を、少人数のチームが気にする条件で並べると次のようになります。

選択肢向いている条件増える運用作業誰がいつ読んだかの記録向かない条件
権限600の.envサーバー1台・スクリプト1〜2本で、読み出しの記録を求められていないほぼなし。パスワード変更時は各サーバーのファイルを書き換える残らない(ファイルのアクセス時刻程度)同じパスワードを複数台に置く、監査で読み出しの証跡を求められる
パスワードマネージャー使うのが人(管理画面のログイン情報、共有アカウント)メンバーと共有範囲の管理製品の監査機能に依存無人で動くスクリプトに毎回値を渡す
クラウドのシークレット管理サービススクリプトが同じクラウドの仮想マシンや関数で動き、そのクラウドのIDで認証できるIAMの権限設計。アンシールやサーバー保守は不要クラウド側の監査ログに残るオンプレミスのサーバーが中心で、そのための認証情報を別に置くことになる
Vault/OpenBaoを自前で運用オンプレミスやクラウドをまたいで複数台・複数スクリプトがあり、パスごとの権限と読み出しの記録が要るアンシール、監査ログの容量監視、バックアップ、バージョンアップ、TLS証明書監査デバイスに残る(本記事の形)再起動後に鍵を入れられる人が確保できず、auto unsealの先もない

私たちの判断基準は次のとおりです。スクリプトがクラウド上で動くなら、まずそのクラウドのシークレット管理サービスを使います。認証をクラウドのIDに任せられるので、AppRoleのsecret_idのような「手元に置く引換券」が要らないからです。オンプレミス中心で、秘密情報を読むスクリプトが複数のサーバーにまたがり、監査で読み出しの記録を求められるなら、VaultかOpenBaoを立てる価値があります。その場合でも、アンシールの担当を3人決められること、監査ログのディスクを監視できることが前提です。どちらも用意できない少人数のチームで、サーバー1台にスクリプトが1〜2本なら、所有者だけが読める権限600の.envのほうが、止まる場所が少ないぶん運用の事故を起こしにくい、というのが私たちの判断です。人が使うパスワードは、どの場合でもパスワードマネージャーに分けます。

後片付け

検証が終わったら、ホストでコンテナ、ボリューム、ネットワークを削除します。アンシールキーとrootトークンを書いた~/init.txtはサーバーのコンテナの書き込み層にあるので、docker rm -fvでコンテナと一緒に消えます。-vは、クライアントのコンテナにできた名前なしのボリュームも消すためです。

terminal(ホスト)
docker rm -fv hashicorp-vault-approle-secrets-srv hashicorp-vault-approle-secrets-cli
hashicorp-vault-approle-secrets-srv
hashicorp-vault-approle-secrets-cli
docker volume rm hashicorp-vault-approle-secrets-data hashicorp-vault-approle-secrets-logs
hashicorp-vault-approle-secrets-data
hashicorp-vault-approle-secrets-logs
docker network rm hashicorp-vault-approle-secrets-net
hashicorp-vault-approle-secrets-net

よくある質問

HashiCorpはどこの国の会社ですか?

アメリカの会社です。2012年にMitchell HashimotoとArmon Dadgarが創業し、2024年4月のIBMの発表ではカリフォルニア州サンフランシスコに本社があるとされています。2025年2月27日にIBMが買収を完了し、執筆時点では「an IBM company」と名乗っています。検証したVault 2.2.0のライセンス文書でも、LicensorはIBMでした。

HashiCorp Terraformとは何ですか?

Terraformは、サーバーやネットワークなどのインフラの構成をコードで書き、その内容どおりに作成・変更するHashiCorpのツールです。Vaultが秘密情報を扱うのに対し、Terraformはインフラそのものを作る道具なので役割は別です。AWSのVPCとEC2を例にした使い方はTerraformで始めるIaCのガイドで手順を追っています。

「vault」の読み方は?

英語の vault は金庫室や保管庫を指す語で、カタカナでは「ボールト」と書かれることが多く、「ヴォルト」と書く例もあります。製品名のVaultも同じ読み方で、秘密情報をしまう金庫室という意味の名前です。コマンド名やパスは小文字のvaultです。

OpenBaoとは何ですか?Vaultとどう違いますか?

Linux FoundationのOpenSSFが管理するVaultのフォークがOpenBaoで、リポジトリのライセンスはMPL-2.0です(執筆時点)。VaultのBSLとはライセンスと開発体制が違うので、BSLの条件を避けたい場合の選択肢になります。この記事のコマンドがそのまま動くかは、OpenBaoを動かしていないので確かめていません。

まとめ

Vault 2.2.0をRaftの1台構成で動かすと、スクリプトは1つのパスを2分間だけ読むトークンで値を受け取り、それ以外の操作は403で拒否され、読み出しは監査ログにHMAC付きで残りました。その代わり、再起動のたびに3本の鍵でのアンシールが要り、監査ログが書けないと要求が止まります(監査デバイスが1つの場合)。次の一歩として、いま.envを読んでいるスクリプトとそれが動くサーバーを一覧にし、上の表のどの行に当てはまるかを決めてから、Vaultを使う分だけこの記事の手順でロールを1本ずつ作ってください。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。