🔐

HashiCorp Vault + GitHub Actions OIDC を使った workflow からの安全なシークレットアクセス

2022/02/16に公開

GitHub Actions の OIDC (OpenID Connect) サポート

https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/

昨年 GitHub Actions で OIDC がサポートされるようになったのは記憶に新しいイベントです。
GitHub Actions workflow と GitHub OIDC Provider が互いに連携できるようになったことで、GitHub Actions から任意の Cloud Provider に対して直接 OIDC Token を送り、Cloud Provider から Access Token (short-lived) を受け取ることが可能になりました。

GitHub Actions で OIDC を使うことのメリット

GitHub Actions を利用する上で避けて通れないのが外部システムへのアクセスです。例えば workflow によって AWS 上の任意のシステムに変更を加えたい場合、当然 workflow は AWS の認証情報を保有する必要があります。同様に、任意の Third-Party API エンドポイントにリクエストを投げたい場合、API token などのシークレットが必要になります。

従来のアプローチでは、こうした情報を GitHub Secrets に追加し、workflow 内から参照することでシークレットへアクセスを行っていました。

個人的なプロジェクトや小規模の組織ではこの方法でも十分に実運用可能ですが、プロジェクト・組織が次第にスケールしていくに連れて、シークレットがあらゆる外部システムに散らばった状態で全てを正しく管理するのは、実に骨の折れる作業になります。

例えばあるシークレットAが GitHub Secrets を含む複数のプラットフォームに登録・管理されていたとして、このシークレットAを更新する、という作業を "きちんと" やるには、それなりに作り込まれた自動化システムが必要になるでしょうし、そうすると今度は GitHub Secrets を GitHub API を介して更新するための GitHub の認証情報の作成・管理が別に必要になったりと、多くの場合「鶏が先か、卵が先か」のようなジレンマを生み出すことになりそうです。

OIDC を使うことでこうした問題を解決することができます。OIDC をある2者間で利用するためには、まず事前にお互いが trust relationship を構成している必要があります。trust relationship の構成に際して、従来のように何かシークレット情報を追加する、という必要はありません。通常は GitHub Actions から OIDC を介して利用したい Cloud Provider 側で、必要な Role の作成、信頼するルールの定義 (OIDC token に含まれる各種 claim values に基づいた filter rule) などを行います。

例えば、特定の job environment、event (e.g., pull_request)、branch name からのイベントのみを信頼する、といった定義ができます。
参考: Configuring the OIDC trust with the cloud

これにより、GitHub Actions は job 実行毎に OIDC を介して Cloud Provider から Access Token を動的に取得することが可能になり、シークレットを GitHub 側に事前に定義しておく必要がなくなります。またこの Access Secret は job の実行中のみ利用できればよいため、TTL を持たせるなどして寿命を短く持たせる事ができ、より安全性を高めることができます。

HashiCorp Vault を使ったシークレットの集中管理

※ここでは HashiCorp Vault が何か、という基本的な説明は省略します。Vault をご存知ない方は下記のリソースなどが参考になるかもしれません。

static/dynamic secrets

Vault のコア機能の1つはシークレット管理です。様々な場所で散らばって管理されがちな各種 static secret (e.g., third-party API token, 社内ツール用 token, etc) を安全に1箇所で管理し、各アプリケーション・ユーザーからのシークレットへのアクセスを、Vault が提供する様々な Auth Methods によって提供・一元管理します。

また、Vault の大きな特徴の1つである dynamic secret 機能を使うと、各アプリケーション・ユーザーからのシークレットへのアクセス毎に、動的な一意の secret が作成され、利用が終了すると即座に revoke することができます。この dynamic secret では、例えば 各種 Databases、AWS GCP Azure などのメジャーなクラウドベンダーがサポートされています。

HachiCorp Vault を使った GitHub Actions でのシークレット運用

今回の例では、基本的な概念を理解しつつ説明を簡単にするため、下記のようなシンプルな構成を取ることにします。

まず前提として、任意の場所でホストされている Vault instance が GitHub Actions から接続可能であり、third-party API のシークレット (static) が Vault に保管されているとします。

GitHub Actions の OIDC サポートを利用することで、workflow からのフローの流れは下記のようになります。

  1. OIDC を介して Vault から一時的な access token を取得
  2. 取得できた access token を使って Vault から目的の secret を取得
  3. 取得した secret を使って third-party API に対してリクエストを行う

これにより、GitHub 側には事前に一切シークレットを定義することなく Vault から動的に目的にシークレットを取得することができます。

Vault dev server を使って構成を試す

上記で説明したものは全て Vault OSS 版に含まれる標準機能です。
Vault 自体は独立した単体のバイナリファイルなので、特別なインストール作業などは必要ありません。

今回は検証として、ローカル PC で Vault server を dev mode で起動し、ngrok を使って Vault を外部公開した後、GitHub Actions との連携を確認します。

dev server の起動

https://www.vaultproject.io/downloads から自身の PC に合ったバイナリをダウンロード・展開し、パスを通すかバイナリを直接実行します。

$ vault server -dev -log-level=debug

起動に成功すると、ターミナルに下記のような output が確認できるはずです。

WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variable:

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

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: ****
Root Token: ****

Development mode should NOT be used in production installations!

上記の情報を適当な場所にコピーして保存しておきます。
Vault dev server はこのまま実行し続ける必要があるため、ターミナルは閉じずにそのままにします。

次に新しいターミナルウィンドウ・タブを開き、VAULT_ADDRVAULT_TOKEN env variable を設定します。

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

$ export VAULT_TOKEN="**** (Root Token)"

続けて同じターミナルから vault コマンドを実行し、正しく認証されていることを確認します。

$ vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.9.3
Storage Type    inmem
Cluster Name    vault-cluster-ae284ee2
Cluster ID      4adcf90d-2e3a-7cb4-141a-d6c230851dd3
HA Enabled      false

$ vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_850f9e5f    per-token private secret storage
identity/     identity     identity_c4730092     identity store
secret/       kv           kv_49c98434           key/value secret storage
sys/          system       system_edc870a5       system endpoints used for control, policy and debugging

Vault の設定 (for GitHub Actions OIDC)

先に説明した通り、Vault には事前に trust relationship を構成しておく必要があります。
Vault 用の GitHub 公式ガイドも公開されていますが、具体的な Vault の設定については省略されているため、ここでは実際に Vualt CLI 用いて設定を行います。

JWT Auth Method を有効にする

$ vault auth enable jwt
Success! Enabled jwt auth method at: jwt/

GitHub の公式ガイドに従って、 有効にした JWT auth method config の bound_issueroidc_discovery_url フィールドを設定します。

$ vault write auth/jwt/config \
  bound_issuer="https://token.actions.githubusercontent.com" \
  oidc_discovery_url="https://token.actions.githubusercontent.com"
Success! Data written to: auth/jwt/config

static secret を追加する

テスト用のシークレットを Vault に保存しておきます。今回は Fastly API の token を static secret として利用したいので、Vault KV secret engine を使います。

# token を追加 (put)
$ vault kv put secret/fastly/api token=****
Key                Value
---                -----
created_time       2022-02-16T01:20:00.625003Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

# 追加されたことを確認 (get)
$ vault kv get secret/fastly/api
======= Metadata =======
Key                Value
---                -----
created_time       2022-02-16T01:20:00.625003Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

==== Data ====
Key      Value
---      -----
token    ****

次に、この token への read アクセスだけを許可する専用の ACL Policy を作成します。

$ vault policy write github-actions-demo -<<EOF
path "secret/data/fastly/api" {
  capabilities = ["read"]
}
EOF
Success! Uploaded policy: github-actions-demo

JWT auth backend role の作成

$ vault write auth/jwt/role/github-actions-demo -<<EOF
{
  "role_type": "jwt",
  "user_claim": "actor",
  "bound_claims": {
    "repository": "username/actions-test"
  },
  "policies": ["github-actions-demo"],
  "ttl": "1h"
}
EOF

ここでは policies に先程作成した github-actions-demo ACL Policy を設定しています。これにより、この Role を介して払い出された Access Token へは、ACL Policy で指定された permission のみが許可されます。今回の例では Fastly API token に対する read アクセスだけです。

また、bound_claims には、GitHub OIDC Provider から送られてくる OIDC Token に含まれている claims を元に任意の許可条件を記述できます。今回の例では、リクエストが username/actions-test という GitHub repository からのものであれば許可しています。

ngrok の起動

一通り上記の Vault 側の設定が完了したら、Vault を外部に公開します。

# Vault dev server は http://127.0.0.1:8200 で起動中
$ ngrok http 8200

GitHub Actions workflow の例

最後に workflow を用意します。

name: Vault Simple OIDC test
on: [ workflow_dispatch ]
jobs:
  vault-simple-oidc-job:
    permissions:
      id-token: write
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Get Secrets
        id: secret
        uses: hashicorp/vault-action@v2.4.0
        with:
          url: https://your-unique-hostname.ngrok.io
          method: jwt
          role: github-actions-demo
          secrets: secret/data/fastly/api token | FASTLY_TOKEN
      - name: Send Fastly API requests
        run: |
          curl -sv https://api.fastly.com/datacenters -H "fastly-key: ${{ steps.secret.outputs.FASTLY_TOKEN }}" --fail -o datacenters.json
      - name: Count Fastly POPs
        run: |
          jq -r '. | length' datacenters.json

この例では、HashiCorp official の hashicorp/vault-action を利用します。

  • url: Vault instance が稼働している URL (今回の例では ngrok によって払い出される URL)
  • method: jwt
  • role: 先に作成した JWT auth backend role 名を指定 (今回の例では github-actions-demo)
  • secrets: 取得したいシークレットへの path 及び key name

secrets に関しては複数記述することもできます (参考: Multiple Secrets)。今回は secret/fastly/api という path 配下に token という key name でシークレットを追加したため、上記のような記述となっています。ACL Policy 追加時にも気づいたかもしれませんが、data/ という path が追加されています。これは Vault の仕様で、KV (v2) secret engine から任意の key=value データを読み出す時には必ず data/ prefix を付与しないといけない事になっています。これを忘れるとシークレットの取得に失敗してしまいます。

細かな注意点としては、workflow 内の permissions 設定です。
GitHub のドキュメントにも書いてありますが、OIDC を利用するためには少なくとも id-token: write scope を明示的に追加する必要があります。これは workflow の top-level、もしくは各 job レベル単位で設定することになります。
参考: Adding permissions settings
permissions を明示的に追加すると、宣言されていない他の scope は絞られてしまうため、既存の workflow に混ぜる際には、これに引きずられて今まで実行できていた step が失敗しないよう、必要な scope を一緒に追加する必要がありそうです。

参考: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token

また、hashicorp/vault-action からセットされたシークレットはその job 内でのみ有効です。workflow 内の他の job に引き継ぐことはできません。

workflow の実行・確認

上記の workflow ではデモのため on: [ workflow_dispatch ] を指定しています。よってマニュアルでのトリガーが必要です。workflow を repository に追加したら、Actions から実際に実行してみます。

無事 job が成功し、remote の Vault instance から OIDC を介して取得した Fastly API token を使って API へのアクセスが完了し、Fastly のデータセンター (POP) の数が現在97個であることが分かりました。

デバッグ (ngrok Web UI)

GitHub Actions が失敗してしまう場合には、ローカルで実行している ngrok の Web UI (default: http://127.0.0.1:4040) が便利です。ここで実際の Vault server からのエラーレスポンスなどを確認できます。

Vault の Production 利用について

今回は簡単なデモを目的とし、Vault を dev mode でローカルで起動しました。
これまで全く HashiCorp Vault を利用したことが無い方に向けた案内にはなりますが、Vault を本番環境で利用する際には、self-hosted (自身が管理するサーバー上で Vault を走らせて運用する)、または HashiCorp Cloud Platform を利用して HashiCorp-managed の Vault を運用する方法が選択できます。

https://cloud.hashicorp.com/products/vault

HashiCorp Cloud Platform を利用することで、Vault のバージョンアップやサーバー管理 (e.g., Vault Cluster) などのタスクを HashiCorp に任せることができ、日々の運用がとても楽になります。また、Virtual Network 機能を利用することで、現在自身で利用中の AWS VPC <-> HashiCorp Cloud Platform 間で VPC peering 構成したり、Amazon Transit Gateway を使って任意のポイントとのコネクションを構成したりすることも可能です。現時点では、HashiCorp Cloud Platform 上で作成される Vault は AWS 上の指定されたリージョンに展開されます (サポートするリージョン・Cloud Provider は今後増えて行く予定)。

https://cloud.hashicorp.com/docs/hcp/regions

Discussion