APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

HashiCorp Vault で Azure のサービスプリンシパルを安全に管理しよう

こんにちは、ACS 事業部の埜下です。

最近社内では「Vault 筋*1」というワードが飛び交っており、HashiCorp Vault への熱量の高まりを感じています。 Vault 筋、興味ありますよね? そんな皆さんに向けて Vault ネタを一つ紹介しようと思います。 Vault 筋を鍛えていきましょう💪💪💪

はじめに

HashiCorp Vault には「動的シークレット (Dynamic secrets)」という Vault を象徴する機能があります。 動的シークレットを使うと、Vault は対応するサービス(データベースやクラウドサービス)に一時的な認証情報を作成します。「一時的」というのは、Vault 側で認証情報の有効期限を持っており、有効期限が切れたものは Vault が自動的に削除してくれるため「一時的な認証情報」となります。

動的シークレットを使うことで従来のような静的な認証情報を利用者に公開する必要がなくなりますし、特定のアプリケーションでしか使われない認証情報のため使いまわしもできないため、認証情報に関連したセキュリティリスクを下げられます。

この動的シークレットが対応しているサービスはいくつかあり、もちろん Azure にも対応しています。 Azure 用の動的シークレットを設定することで、クライアントからの要求に応じて Vault が Entra ID にサービスプリンシパルを作成します。 設定次第では既存サービスプリンシパルを指定してクライアント シークレットのみ作成、ということもできますが、本記事では「新規サービスプリンシパルを作成する」ことを前提に説明しています。

そこで、今回は Azure に対する動的シークレットの概要と手順について紹介します。

次回以降の投稿では「動的シークレットで作成したサービスプリンシパルを使って Terraform Cloud から Azure へ接続する方法」と「Terraform Cloud のノーコードプロビジョニングで Vault 動的シークレットを使う方法」の紹介を予定しています。 後者の「ノーコードプロビジョニングで~」の記事を投稿するための前準備として Azure に対する動的シークレットを解説することが本記事の目的だったりします。

利用シーン

サービスプリンシパルはアプリケーションやサービスが Azure リソースにアクセスする際に使われる ID です。 たとえば、作成したアプリケーションから Azure Blob Storage に格納されたデータにアクセスする際、Azure Blob Storage に対するロールが付与されたサービスプリンシパルをアプリケーションが利用することでデータへのアクセスが可能になります。

Vault で作られた一時的なサービスプリンシパルも基本的には同じ使い方になりますが、サービスプリンシパルのライフサイクルが異なります。 通常のサービスプリンシパルは各アプリケーションごとに作られた後にクライアント シークレットなどの認証情報を作成します。 この認証情報をアプリケーションに配布して Azure への認証に使ってもらいます。 サービスプリンシパルの認証情報の有効期限は最大 2 年間のため運用中に認証情報のローテーションが必要になりますが、サービスプリンシパル自体は残したままになります。

一方、動的シークレットで作られたサービスプリンシパルは有効期限が切れるとサービスプリンシパル自体が削除されます。 そのためアプリケーションが利用するサービスプリンシパルを入れ替えていくことになります。

整理すると、通常のサービスプリンシパルと Vault の動的シークレットで作られたサービスプリンシパルの特性には以下の違いがでてきます。


サービスプリンシパル アプリケーションが利用するクライアント ID クライアント シークレット
通常 同じもの 有効期限ごとにローテーション
動的シークレット 別もの サービスプリンシパルと共に作成/削除


通常のサービスプリンシパルを使った場合、クライアント シークレットの有効期限を短くするとローテーション頻度が上がり、長くするとクライアント シークレットが不正利用などの被害に遭う可能性が高まります。 Vault の動的シークレットを使うことでこのようなリスクを下げることができます。

Azure を利用されている方は「マネージド ID を使えばいいじゃないか」と思われるかもしれませんね。 Azure 上のサービスはマネージド ID という機能を使って他の Azure リソースにアクセスすることができます。 ここでは簡単な説明にとどめますが、マネージド ID を使うことでサービスプリンシパルで発生していたクライアント シークレットのローテーションなどの運用の手間を省けます。

サービスプリンシパルのデメリットを解消したマネージド ID ですが、「利用できるのは Azure 上のサービスに限定される」という特徴があります。 たとえば、GitHub Actions から Terraform を使って Azure リソースをプロビジョニングする場合、または Terraform Cloud のマネージドサービスを使う場合はマネージド ID は利用できませんので、必然的にサービスプリンシパルを使うことになります。

そのため、Azure に対して Vault の動的シークレットを使う場合の利用シーンとしては「Azure の外のアプリケーション/サービスから Azure リソースにアクセスする」ことがメインになります。

また、サービスプリンシパルはアプリケーションやサービスだけでなく「人」も使えないこともないので、やろうと思えば動的シークレットによる一時的なアクセス権の配布もできます。 しかし、このような場合には Microsoft Entra Privileged Identity Management (PIM) を利用することもご検討ください。 Microsoft Entra ID P2 ライセンスが必要ですが、PIM を使うことで Just-In-Time の特権アクセスをユーザに付与できます。

動的シークレットの概要

Vault で Azure に対する動的シークレットを使うとサービスプリンシパルが作られると冒頭で述べましたが、ただ Vault で設定を有効化するだけでは動的シークレットは機能しません。

では、どのような準備が必要でしょうか。 その説明の前に、Vault が一時的な認証情報としてのサービスプリンシパルを作るシナリオについて説明します。 次の図は Azure に対する動的シークレットが使えるようにし状態の各要素の関係です。

Azure 動的シークレットの流れ

このシナリオには以下の登場人物がいます。

  • HashiCorp Vault
    • 今回の主役です
  • クライアント
    • GitHub Actions や Terraform Cloud など Azure にアクセスしたいアプリケーションです
    • 実用的ではないですが人も使えます
  • Vault 用サービスプリンシパル
    • Vault が一時的なサービスプリンシパルを作るときに使うサービスプリンシパルです
    • サービスプリンシパルを作る権限(後述)を持っています
  • 一時的なサービスプリンシパル
    • Vault 用サービスプリンシパルによって作られるサービスプリンシパルです
    • Vault で定義した Azure RBAC のロールが割り当てられます
  • Azure
    • クライアントのアクセス先です


まず、クライアントが Vault に対して認証情報を要求します。 Vault にはシークレットエンジンと呼ばれるコアコンポーネントがあり、Azure シークレットエンジンには対象のテナントやサブスクリプションが事前に設定されています(設定するのは Vault 管理者)。

developer.hashicorp.com

あわせて、作成するサービスプリンシパルにどのようなロールを付与するかも Vault 内の「ロール」で定義されています。 Azure と Vault で同じ名称が使われていて紛らわしいので、ここでは「Azure ロール」と「Vault ロール」という使い分けをします。 クライアントが認証情報を要求する際、どの Vault ロールを使用するかも指定します。

要求を受け取った Vault は、Entra ID に用意されている Vault 用サービスプリンシパルを使って新しいサービスプリンシパルを作成します。 作成したサービスプリンシパルは Azure サブスクリプションに対して何も権限を持っていないため、Vault は Vault ロールで定義された Azure ロールを新規サービスプリンシパルに付与します。

Vault からの応答として、作られたサービスプリンシパルのクライアント ID とクライアント シークレットがクライアントに渡されます。

このようにして、クライアント ID とクライアント シークレットを受け取ったクライアントは、一時的なサービスプリンシパルを使って Azure にアクセスできるようになります。

動的シークレットをやってみよう

それでは、実際に Vault の動的シークレットを使って Azure の一時的なサービスプリンシパルを作ってみましょう。 とは言っても HashiCorp の公式ドキュメントには豊富なチュートリアルが用意されていて、Azure に対する動的シークレットのチュートリアルも用意されています。

developer.hashicorp.com

1. Entra ID

まずは Microsoft Entra ID 側を設定として次の作業をします。

  1. Vault 用サービスプリンシパルの作成
  2. Vault 用サービスプリンシパルに権限付与
  3. Vault 用サービスプリンシパルにロール付与

これらの作業を遂行するためには作業者に「アプリケーション管理者」や「ユーザーアクセス管理者」のロールが付与されている必要があります。 もし権限がない場合は管理者の方に相談してください。 また、今回 Entra ID の作業はすべて Azure Portal で実施しています。

Entra ID の「アプリの登録」から新規アプリケーション(サービスプリンシパル)を登録します。

Vault 用サービスプリンシパル作成

作られたアプリケーションの「証明書とシークレット」から「クライアント シークレット」を追加します。 説明や有効期限は任意で構いません。

クライアント シークレットの追加

作成されたシークレットの値は Vault に設定しますので控えておいてください。

クライアント シークレットの値を控えておく

つぎに、Vault 用サービスプリンシパルが他のサービスプリンシパルを作成するための権限を付与します。 「API のアクセス許可」から「アクセス許可の追加」を選択します。

アクセス許可の追加

「API アクセス許可の要求」が表示されるので、必要な許可を探していきます。 まずは「Microsoft Graph」の「アプリケーションの許可」を選び、表示される一覧の中から次の許可を選択して追加します。

  • Application.ReadWrite.OwnedBy
  • GroupMember.ReadWrite.All

Microsoft Graph を選択

アプリケーションの許可を選択

Application.ReadWrite.OwnerdBy

GroupMember.ReadWrite.All

許可を追加した直後は「状態」列が「<テナント名>に付与されていません」と表示されているので、「<テナント名>に管理者の同意を与えます」を選択して管理者の同意を与えると「<テナント名>に付与されました」と表示が変わります。

追加した許可の状態が「<テナント名>に付与されていません」となっている

これで Vault 用サービスプリンシパルが他のサービスプリンシパルを作れるようになりましたが、これだけでは作成したサービスプリンシパルに Azure ロールを割り当てることはできません。 Vault 用サービスプリンシパルに任意スコープ(サブスクリプション、リソースグループ、リソース)の「ユーザーアクセス管理者」ロールを割り当てます。

以上で Entra ID 側の作業は完了です。 ここまでの作業でクライアント シークレットの値を控えているはずですが、追加で「テナント ID」と「サブスクリプション ID」、「クライアント ID」を控えておいてください。 これらの情報を元に Vault を設定していきましょう。

  • テナント ID
  • サブスクリプション ID
  • クライアント ID
  • クライアント シークレットの値

2. Vault

続いて Vault 側の作業になります。 やることとしては次の 3 つです。

  1. Azure シークレットエンジンの有効化
  2. Azure シークレットエンジンの設定
  3. Vault ロールの作成

Vault の環境については、コミュニティ版の Vault をインストールしてローカルで Dev Server モードとして起動、またはマネージドサービスの HCP Vault を利用するのが簡単です。 HCP Vault は(HCP 全体で)いくらか無料利用枠があります。

Vault の操作は API や Web UI でもできますが、今回は CLI で実施しています。 また、今回は検証目的としてルートトークンによる Vault へのログインをしていますが、本番環境などでは要件に応じて使用するトークンを使い分けてください。

まずは対象の Vault のアドレスを環境変数に設定します。 以下は Dev Server モードを使っている場合の環境変数ですので、HCP Vault など他の環境を利用される場合は適切なアドレスに書き換えてください。

$ export VAULT_ADDR='http://127.0.0.1:8200'

vault login コマンドを実行するとトークンの入力を求められるので、ルートトークンなどの強い権限を持つトークンを使います。

$ vault login
Token (will be hidden): 
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.yHyaGJKYtB1Lu8gO9YniMqFC
token_accessor       iMAn0R7kl1M8R24k0J4Xlr34
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

Vault にログインできたら Azure シークレットエンジンを有効化します。

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

Azure シークレットエンジンを有効にするだけでは Vault からどの Azure テナントに繋げばいいか分かりません。 そこで、Entra ID の作業時に控えていた各種情報を使います。 ここでは環境変数に ID などをセットして vault write azure/config コマンドで接続先と Vault 用サービスプリンシパルの情報を設定しています。

$ vault write azure/config \
     subscription_id=$SUBSCRIPTION_ID  \
     client_id=$CLIENT_ID \
     client_secret=$CLIENT_SECRET \
     tenant_id=$TENANT_ID \
     use_microsoft_graph_api=true
Success! Data written to: azure/config

最後に、一時的なサービスプリンシパルに付与する Azure ロールを Vault ロールで定義します。 次の Vault ロール app-demo はサービスプリンシパルに vault-demo リソースグループの共同作成者 (Contributor) ロールを付与する定義です。

この場合、Vault 用サービスプリンシパルは vault-demo リソースグループ、またはそのリソースグループを持つサブスクリプションに対して「ユーザーアクセス管理者」のロールを持っている必要があります。

$ vault write azure/roles/app-demo azure_roles=-<<EOF
    [
      {
        "role_name": "Contributor",
        "scope": "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/vault-demo"
      }
    ]
EOF
Success! Data written to: azure/roles/app-demo

もし Vault 用サービスプリンシパルに必要な権限がない場合、上記のコマンドを実行すると次のようなエラーが出力されます。 親切設計ですね。

Error writing data to azure/roles/app-demo: Error making API request.

URL: PUT http://127.0.0.1:8200/v1/azure/roles/app-demo
Code: 500. Errors:

* 1 error occurred:
        * unable to lookup Azure role: authorization.RoleDefinitionsClient#List: Failure responding to request: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. Status=403 Code="AuthorizationFailed" Message="The client '00000000-0000-0000-0000-000000000000' with object id '00000000-0000-0000-0000-000000000000' does not have authorization to perform action 'Microsoft.Authorization/roleDefinitions/read' over scope '/subscriptions/00000000-0000-0000-0000-000000000000' or the scope is invalid. If access was recently granted, please refresh your credentials."

以上で Vault の準備は完了です。

3. 認証情報の要求

それでは、実際に Azure にアクセスするためのサービスプリンシパルを要求してみましょう。 さきほどまで Vault の設定で使っていたトークンは強い権限を持っているため、認証情報の要求も可能です。

一方、実際に Vault を運用する場合はクライアント側には限られた権限のみ付与したいですよね。 Vault には「ポリシー」という、Vault 内での特定の操作に対する許可/拒否を定義する機能があります。 ここでは「Azure シークレットエンジンを呼び出してサービスプリンシパルの情報を取得できる」ポリシーを定義し、そのポリシーが割り当てられたトークンを作成して認証情報の要求に使います。

次のコマンドで Azure シークレットエンジンに対する app-demo ポリシーを定義します。

$ vault policy write app-demo - <<EOF
path "azure/creds/app-demo" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: app-demo

次に、app-demo ポリシーを割り当てたトークンを作成します。

$ vault token create -policy=app-demo
Key                  Value
---                  -----
token                hvs.CAESIHOMSuIiTqtnHnRfEiEgnt0XUA7MWrpsPFH9Yy5JaV0zGh4KHGh2cy5qUWRhOURZVTkyank3bGo0YTFnV2pDc2Y
token_accessor       YkCDK9lgH4Mr1v4uAWutbt0B
token_duration       768h
token_renewable      true
token_policies       ["app-demo" "default"]
identity_policies    []
policies             ["app-demo" "default"]

コマンドを実行すると色々な情報が出力されますが、その中でも token が認証情報の要求に使用するトークンになります。 また、policies を見ると app-demo ロールが割り当てられていることが確認できますね。

このトークンをそのまま利用してもよいのですが、コマンド実行時にそのままトークンを入力してしまうとコマンド履歴にトークン情報が残ってしまうので、以下のようにトークン情報だけ抜き出して変数にセットします。

$ APPS_TOKEN=$(vault token create -policy=app-demo -field=token)

ちなみに、1 回目に作られたトークンと 2 回目の変数にセットしたトークンの値は異なります。 echo $APPS_TOKEN で確認してみてください。

それでは、アプリケーションを想定したクライアントから認証情報を要求してみましょう。

$ VAULT_TOKEN=$APPS_TOKEN vault read azure/creds/app-demo
Key                Value
---                -----
lease_id           azure/creds/app-demo/GlSrpScuN1FRPT6vxO8f1zDi
lease_duration     768h
lease_renewable    true
client_id          00000000-0000-0000-0000-000000000000
client_secret      QiP8Q~j7XuRNrg.ItDLN3uMlMSx9qplHjMb2ubn6

コマンドは無事に成功して、サービスプリンシパルのクライアント ID とクライアント シークレットが返ってきました!

Azure Portal でも確認してみると……

動的シークレットで作られたサービスプリンシパル

無事に一時的なサービスプリンシパルが作られているようですね。 しかし、よく見てみると……。 クライアント シークレットがない!!!

クライアント シークレットが見えない

驚きました、Azure Portal からはクライアント シークレットの存在自体見えないのですね(今回の記事を書いてて気づきました)。 せっかくなので、この認証情報を使ってログインできるか試してみましょう。

learn.microsoft.com

実際に成功したことを証明するのはなかなか難しいですが、以下のようにサービスプリンシパルを使ったログインに成功しました。

$ az login --service-principal \
    -u <client_id> \
    -p <client_secret> \
    --tenant $TENANT_ID
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "00000000-0000-0000-0000-000000000000",
    "id": "00000000-0000-0000-0000-000000000000",
    "isDefault": true,
    "managedByTenants": [],
    "name": "nnstt1",
    "state": "Enabled",
    "tenantId": "00000000-0000-0000-0000-000000000000",
    "user": {
      "name": "00000000-0000-0000-0000-000000000000",
      "type": "servicePrincipal"
    }
  }
]

以上で Vault の動的シークレットを使った一時的なサービスプリンシパルを作成、そしてサービスプリンシパルへのログインを確認できました。

おわりに

今回は HashiCorp Vault の動的シークレットを使って Azure に一時的なサービスプリンシパルを管理する方法を紹介しました。

今回の記事を読んでいただくことで Vault がどうやってサービスプリンシパルを作成するかお分かりいただけたかと思いますが、一方で疑問を抱いた方もいらっしゃるかもしれません。 たとえば、「Vault が使うサービスプリンシパルのシークレットはどうするの?」とか、「サービスプリンシパルが消される条件は?とか。 これらの疑問はごもっともで、まだまだ紹介できていない機能があってその中で解消されるものもあります。

気になる方はぜひ公式ドキュメントも参照しつつ、手元でも Vault を触ってみてください。


【PR】
私達 ACS 事業部は Azure・AKS などのクラウドネイティブ技術を活用した内製化のご支援をしております。

www.ap-com.co.jp

一緒に働いていただける仲間も募集中です!
まだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。
我々の事業部の CultureDeck はコチラ。

www.ap-com.co.jp

本記事の投稿者: 埜下 太一
Azure をメインにインフラ系のご支援を担当しています。
Vault 試験に向けて勉強中です。
個人ブログ

*1:Vault を扱うスキルと私は解釈して使ってますが、実際にそういう筋肉があるかもしれません