Vault の AppRole 認証メソッドについて
Vaultの認証メソッドのAppRoleについて少し会話する事があったので、まとめてみました。この後出てくるSecretIDの取り扱いについては、もっと良い方法があるのかもしれませんが、ご参考までに。
About AppRole
AppRoleとはVaultの認証メソッドの一つです。AppRoleを利用するクライアントとしては、主にマシンやアプリケーションが想定されています。
マシンやアプリケーション向けの主な認証メソッドとして以下が利用出来ますが、これらが利用出来ない環境等において、マシンやアプリケーションを認証する手段として利用出来ます。
- AWS auth method
- Azure auth method
- Google Cloud auth method
- JWT/OIDC auth method
- Kubernetes auth method
AppRoleは、事前に定義されたロールでクライアントを認証します。認証されるとトークンがレスポンスされる訳ですが、ロールに付与されたポリシーで定義された権限の範囲で、Vaultへのアクセスが許可されます。
ロールで認証を行う際に必要になるのが、ロールに紐づくRoleIDとSecretIDになります。
これらはパスワード認証で言うと以下の様な位置付けになります。パスワード認証と異なる部分としては、SecretIDは動的な値になり、AppRoleを使ってVaultへログインを試みるタイミング毎に生成する形になります。
- RoleID -> ユーザー名
- SecretID -> パスワード
SecretIDは上記の様な位置付けになるため、シークレット情報として扱う必要があり、AppRoleを利用する場合、SecretIDをどうセキュアに取得するかという点がポイントになります。
AppRoleのロールを定義する際にSecretIDの利用に色々な制約をかける事が出来たり、レスポンスラッピングという仕組みを利用する事で、よりセキュアにSecretIDを扱う事が出来ます。AppRoleの設定を通じて、その辺りを確認していきたいと思います。
Test AppRole
Configure KV-v2 secrets engine and policy
AppRoleのテスト用にKV-v2 シークレットエンジンとポリシーの設定をVault terraform providerのvault_mount
, vault_kv_secret_v2
, vault_policy
リソースを利用して、以下の用に設定します。
# KV-v2 secrets engine
resource "vault_mount" "example" {
path = "approle-test"
type = "kv"
options = { version = "2" }
description = "kv version 2 secrets engine mount for approle test"
}
# KV-v2 for approle test
resource "vault_kv_secret_v2" "example" {
mount = vault_mount.example.path
name = "secrets"
cas = 1
delete_all_versions = true
data_json = jsonencode(
{
password = "abcdefg123456"
}
)
depends_on = [
vault_mount.example
]
}
# Policy for approle test
resource "vault_policy" "example" {
name = "approle-demo-policy"
policy = <<EOT
# Login with AppRole
path "auth/approle/login" {
capabilities = ["update"]
}
# Permits token creation
path "auth/token/create" {
capabilities = ["update"]
}
path "approle-test/data/secrets" {
capabilities = ["read"]
}
EOT
}
Configure AppRole auth method
AppRoleのパスはデフォルト設定で、事前に有効化しているという前提で進めていきます。
vault auth enable approle
AppRoleのロールtest
をvault_approle_auth_backend_role
リソースとvault_approle_auth_backend_role
データソースを用いて、設定しています。
resource "vault_approle_auth_backend_role" "example" {
backend = "approle"
role_name = "test"
secret_id_bound_cidrs = ["${var.cidr}"]
secret_id_num_uses = 3
secret_id_ttl = 180
token_policies = ["default", "approle-demo-policy"]
}
data "vault_approle_auth_backend_role_id" "example" {
backend = "approle"
role_name = vault_approle_auth_backend_role.example.role_name
depends_on = [
vault_approle_auth_backend_role.example,
]
}
output "role_id" {
description = "The RoleID of the role for test"
value = data.vault_approle_auth_backend_role_id.example.role_id
}
AppRoleのロールを設定する際、ロールを利用する際の制約を定義する事が出来ます。上記のTerraform 設定だと、SecretIDを利用可能なIPアドレスレンジの制限、SecretIDの利用回数制限、SecretID自体のTTLを設定しています。
また、ロールtest
には、ポリシーdefault
とapprole-demo-policy
を付与し、Vaultへの権限を設定しています。
また、ロールを介して生成されるトークンに対する制約も設定する事が出来ます。詳細はドキュメントをご確認下さい。
Use AppRole auth method
ロールtest
のRoleIDは、アウトプットrole_id
として設定しているので、Terraformのアウトプットから取得できます。この環境ではf706c18f-f760-e290-ab4f-bb24e49f1ebf
になります。
次にSecretIDを生成します。この後の操作は管理者トークンをVAULT_TOKEN
に設定し、VAULT_NAMESPACE
, VAULT_ADDR
を適切な値で設定した上で実施しています。
$ vault write -f auth/approle/role/test/secret-id
Key Value
--- -----
secret_id ecf2c781-6b05-3703-7e7e-7dbd9f72b3f6
secret_id_accessor eda69236-001b-2bc9-5c6e-b10786fbe33a
secret_id_num_uses 3
secret_id_ttl 3m
SecretIDは、auth/<AppRole認証メソッドのパス>/role/<ロール名>/secret-id
のAPIパスに対して、リクエストを行うと動的に生成されます。再度同じパスに対してリクエストを行うと、secret_id
の値が動的に生成されている事が分かるかと思います。
$ vault write -f auth/approle/role/test/secret-id
Key Value
--- -----
secret_id bb0c5cdf-d446-a621-c002-c682823882a2
secret_id_accessor 3673803c-ab5f-87f7-0ec1-eb7fc72197ba
secret_id_num_uses 3
secret_id_ttl 3m
この様にSecretIDに関しては、AppRoleでログインを行う必要があるタイミングで動的に生成し、ロールに紐づくRoleIDと一緒に使います。
vault_approle_auth_backend_role
リソースを通じてロールに設定した制約に関しても確認してみます。
secret_id_num_uses
は3
と設定されているため、このSecretIDは3回までしか利用出来ないです。最初に生成したSecretIDで実際に試してみます。
$ vault write auth/approle/login \
role_id="f706c18f-f760-e290-ab4f-bb24e49f1ebf" \
secret_id="ecf2c781-6b05-3703-7e7e-7dbd9f72b3f6"
Key Value
--- -----
token hvs.xxx
token_accessor UjFEv2X9snYV80ykOnhSo5Av.jBRkw
token_duration 1h
token_renewable true
token_policies ["approle-demo-policy" "default"]
identity_policies []
policies ["approle-demo-policy" "default"]
token_meta_role_name test
3回目までは上記の通り、AppRoleでの認証が通り、トークンがレスポンスされますが、4回目のログインを試行すると以下の用にエラーがレスポンスされ、ロールに設定した制約が有効である事を確認出来ます。
$ vault write auth/approle/login \
role_id="f706c18f-f760-e290-ab4f-bb24e49f1ebf" \
secret_id="ecf2c781-6b05-3703-7e7e-7dbd9f72b3f6"
Error writing data to auth/approle/login: Error making API request.
Namespace: admin/
URL: PUT https://xxx.z1.hashicorp.cloud:8200/v1/auth/approle/login
Code: 400. Errors:
* invalid role or secret ID
続いて、SecretIDをどうセキュアに取得していくかについて確認していきます。
How to get SecretID
AppRoleのベストプラクティスがドキュメントとして提供されています。
ここでも記載されている通り、先ずRoleIDとSecretIDは異なるチャネルで生成するのが一般的で、例えば以下の様な形が方法としては取りえると考えられます。
- RoleID: Vaultの管理者がVaultのGUI,CLI,APIのいずれかを介して生成し、Vaultのユーザーに提供
- SecretID: Vaultのユーザー(開発チーム)がAppRoleを利用するアプリケーション(ex. CIのツールチェーン)中で生成
ここで、SecretIDをどう取得するのか?という部分がポイントになってきます。SecretIDを取得するためには、当然の事ながらトークンが必要となり、それではそのトークンをどう取得するのか、、、悩ましいところです。
一つの方法として、ロールのsecret_id
のパスに対して必要なアクションのみの権限を持ったトークンを事前に生成し、それをVaultのユーザー側に提供し、そのトークンを利用し、SecretIDのみ生成出来るようにするというのはどうでしょうか。
先ず、以下のポリシードキュメントでポリシーapprole-demo-gen-secretid-policy
を設定しています。
# Policy for approle test to generate secret_id
resource "vault_policy" "gen_secret_id" {
name = "approle-demo-gen-secretid-policy"
policy = <<EOT
# Generate SecretID
path "auth/approle/role/test/secret-id" {
capabilities = ["update"]
}
EOT
depends_on = [
vault_approle_auth_backend_role.example
]
}
このポリシーはパスauth/approle/role/test/secret-id
に対して、update
権限のみを持ちます。
このポリシーのみをアタッチして、認証メソッドを経ずに直接トークン(hvs.CAESIISROx3...
)を生成します。
$ vault token create -policy=approle-demo-gen-secretid-policy
Key Value
--- -----
token hvs.CAESIISROx3...
token_accessor W4WdXu5VPWUWKIhbQGXnuxNc.jBRkw
token_duration 1h
token_renewable true
token_policies ["approle-demo-gen-secretid-policy" "default"]
identity_policies []
policies ["approle-demo-gen-secretid-policy" "default"]
このトークンを実際にSecretIDを生成するアプリケーション側に提供し、SecretIDが必要なタイミングでこのトークンを利用し、SecretIDだけを生成してもらいます。
export VAULT_TOKEN="hvs.CAESIISROx3..."
この状態でsecret_id
を生成します。
$ vault write -f auth/approle/role/test/secret-id
Key Value
--- -----
secret_id 86de968e-e8cc-f271-1a13-1048d38f66d1
secret_id_accessor 483766bd-a616-7b47-c278-98f1af86e90a
secret_id_num_uses 3
secret_id_ttl 3m
アプリケーション側には既にrole_id
が提供されているはずなので、role_id
とsecret_id
を用いてAppRoleでログインを行う事が出来ます。
Response wrapping
さらに、SecretIDをよりセキュアに取得する方法として、SecretIDの生成時に-wrap-ttl
フラグを使用する方法があります。
この方法を利用して頂く事で、前述の様にSecretIDが平文でレスポンスされる代わりに、トークン使用回数が1回に限定された短いTTLのラッピングトークンがレスポンスされます。
実際のSecretIDは、このラッピングトークンのCubbyholeにストアされ、ラッピングトークンを持っているクライアントのみがアンラップ処理を行う事で、実際のSecretIDを取得する事が可能になります。
Notes: Cubbyholeに関しては、記事の後ろに記載しましたので、ご興味あればご確認下さい。
それでは実際にこの方法を試してみます。secret_id
を生成するタイミングで-wrap-ttl
を付与して、リクエストを送ります。secret_id
ではなく、wrapping_*
という情報がレスポンスされている事が分かると思います。
$ vault write -wrap-ttl=60s -f auth/approle/role/test/secret-id
Key Value
--- -----
wrapping_token: hvs.CAESIO...
wrapping_accessor: kmD3khcklJe3n9QMvF9x6u2g.jBRkw
wrapping_token_ttl: 1m
wrapping_token_creation_time: 2023-12-22 04:31:10.680783529 +0000 UTC
wrapping_token_creation_path: auth/approle/role/test/secret-id
wrapped_accessor: 943423bf-dd93-c3e9-7e74-377b29c80805
wrapping_token
の値をアンラップする事で、実際のSecretIDを取得する事が出来ますが、このラッピングトークン(hvs.CAESIO...
)は、指定したTTLの1分間の間しか利用出来ません。かつ上記の通り1度しか使えないため、ダイレクトにSecretIDを取得するよりセキュアな方法になるかと思います。
以下の通り、アンラップする事でsecret_id
の値を取得出来ます。
export VAULT_TOKEN="hvs.CAESIO..."
$ vault unwrap
Key Value
--- -----
secret_id 7bc3451c-0dce-9816-0598-d765739ab473
secret_id_accessor 943423bf-dd93-c3e9-7e74-377b29c80805
secret_id_num_uses 3
secret_id_ttl 3m
ラッピングトークンを一度利用した後に、再度アンラップ処理を実行しようとすると、エラーになります。
$ vault unwrap
Error unwrapping: Error making API request.
Namespace: admin/
URL: PUT https://xxx.z1.hashicorp.cloud:8200/v1/sys/wrapping/unwrap
Code: 400. Errors:
* wrapping token is not valid or does not exist
Summary
AppRoleの利用側に関して、今までの内容を全てまとめてシェルスクリプトにしてみると、以下のような形でしょうか。
#!/bin/bash
# Vault管理者側から提供される事を想定した環境変数
VAULT_ADDR="https://xxx.z1.hashicorp.cloud:8200"
VAULT_NAMESPACE="admin"
ROLEID="f706c18f-f760-e290-ab4f-bb24e49f1ebf"
TOKEN_FOR_SECRETID="hvs.xxxx"
# ラッピングトークンを取得
WRAPPING_TOKEN=$(curl --header "X-VAULT-TOKEN: $TOKEN_FOR_SECRETID" --header "X-Vault-Namespace: $VAULT_NAMESPACE" --header "X-Vault-Wrap-TTL: 60s" --request POST $VAULT_ADDR/v1/auth/approle/role/test/secret-id | jq -r .wrap_info.token)
# ラッピングトークンをアンラップして、SecretIDを取得
SECRETID=$(curl --header "X-VAULT-TOKEN: $WRAPPING_TOKEN" --header "X-Vault-Namespace: $VAULT_NAMESPACE" --request POST $VAULT_ADDR/v1/sys/wrapping/unwrap | jq -r .data.secret_id)
# AppRoleを用いてログイン
VAULT_CLIENT_TOKEN=$(curl --header "X-Vault-Namespace: $VAULT_NAMESPACE" --request POST --data '{"role_id": "'"$ROLEID"'", "secret_id": "'"$SECRETID"'"}' $VAULT_ADDR/v1/auth/approle/login | jq -r .auth.client_token)
# シークレット情報(abcdefg123456)を取得
PASSWORD=$(curl --header "X-VAULT-TOKEN: $VAULT_CLIENT_TOKEN" --header "X-Vault-Namespace: $VAULT_NAMESPACE" $VAULT_ADDR/v1/approle-test/data/secrets | jq -r .data.data.password)
echo $PASSWORD
AppRoleのロール設定事に定義できる制約やSecretID取得時の工夫によって、SecretIDをセキュアに取得できる様な仕組みをVaultとしては提供していますが、環境やユースケースによって実際にどう実装するのかは色々と検討する必要がある部分かと思います。
例えば、secret_id
のみ生成するために作成したトークンですが、この環境だとTTLが1h
なので、作成してから1時間経過すると使えなくなってしまいます。
ですので、定期的にvault token renew
を行い、このトークンを使える様にしておく必要があるですとか。
また、シークレットの取得でVault Agentを利用する場合、Auto-Auth機能を利用する事でVaultへの認証とトークンの更新をVault Agent側にオフロード出来たりしますので、環境によってはVault Agentを利用して頂くのも一つの方法になるかと思います。
マシンやアプリケーションの認証メソッドとして、AppRoleを利用される際の参考になれば幸いです。
Appendix
Cubbyhole
cubbyholeシークレットエンジンはデフォルトで有効化されており、無効化したり、パスを移動したり、複数有効化する事が出来ない特別なシークレットエンジンです。
パスはトークン毎にスコープされるため、どのトークンも他のトークンのcubbyholeにはアクセスすることが出来ない様に設計されています。
$ vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ ns_cubbyhole ns_cubbyhole_9a8d05ad per-token private secret storage
Cubbyhole response wrapping
AppRoleのSecretID取得の際に利用したレスポンスラッピングですが、Vaultへリクエストを行う際に、-wrap-ttl
フラグを付けると、Vaultは1回しか使えない特別なトークン(ラッピングトークン)を作成し、そのトークンのcubbyholeにレスポンス(シークレット)を短いTTLで挿入します。
ラッピングトークンを持っている事が期待されるクライアントだけが、短いTTLの間に、ラッピングトークンのアンラップ処理を行う事で、ラッピングトークンのchubbyholeから実際のシークレット情報を取得する事が出来ます。
チュートリアルが用意されているので、こちらのステップを確認して頂くと実際のイメージが付きやすいかもしれません。
Discussion