🔑

Vault の AppRole 認証メソッドについて

2023/12/25に公開

Vaultの認証メソッドのAppRoleについて少し会話する事があったので、まとめてみました。この後出てくるSecretIDの取り扱いについては、もっと良い方法があるのかもしれませんが、ご参考までに。

About AppRole

AppRoleとはVaultの認証メソッドの一つです。AppRoleを利用するクライアントとしては、主にマシンやアプリケーションが想定されています。
マシンやアプリケーション向けの主な認証メソッドとして以下が利用出来ますが、これらが利用出来ない環境等において、マシンやアプリケーションを認証する手段として利用出来ます。

AppRoleは、事前に定義されたロールでクライアントを認証します。認証されるとトークンがレスポンスされる訳ですが、ロールに付与されたポリシーで定義された権限の範囲で、Vaultへのアクセスが許可されます。

ロールで認証を行う際に必要になるのが、ロールに紐づくRoleIDSecretIDになります。

これらはパスワード認証で言うと以下の様な位置付けになります。パスワード認証と異なる部分としては、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リソースを利用して、以下の用に設定します。

main.tf
# 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のパスはデフォルト設定で、事前に有効化しているという前提で進めていきます。

AppRole有効化例
vault auth enable approle

AppRoleのロールtestvault_approle_auth_backend_role リソースとvault_approle_auth_backend_role データソースを用いて、設定しています。

main.tf
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,
  ]
}
outputs.tf
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には、ポリシーdefaultapprole-demo-policyを付与し、Vaultへの権限を設定しています。
また、ロールを介して生成されるトークンに対する制約も設定する事が出来ます。詳細はドキュメントをご確認下さい。
https://developer.hashicorp.com/vault/api-docs/auth/approle#create-update-approle

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_uses3と設定されているため、この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のベストプラクティスがドキュメントとして提供されています。
https://developer.hashicorp.com/vault/tutorials/auth-methods/approle-best-practices

ここでも記載されている通り、先ず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を設定しています。

main.tf
# 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_idsecret_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の利用側に関して、今までの内容を全てまとめてシェルスクリプトにしてみると、以下のような形でしょうか。

approle-test.sh
#!/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にはアクセスすることが出来ない様に設計されています。

cubbyhole secrets engine
$ 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から実際のシークレット情報を取得する事が出来ます。

チュートリアルが用意されているので、こちらのステップを確認して頂くと実際のイメージが付きやすいかもしれません。
https://developer.hashicorp.com/vault/tutorials/secrets-management/cubbyhole-response-wrapping

References

Discussion