シークレット管理をうまく設計するには、セキュリティと利便性の繊細なバランスが肝心です。シークレットは、所定のユーザーがビルドやデプロイの際に簡単に利用できる状態であると同時に、ローテーションしやすい形で適切に保護されている必要もあります。この記事では、この難しい課題の解決方法として、CircleCI に HashiCorp Vault を連携して、短期間だけ有効な OpenID Connect (OIDC) 認証トークンを使用してシークレットを取得する方法を説明します。

シークレット管理のアンチパターン

これまで、多くのチームがセキュリティよりも利便性を優先してきました。たいていは、”一度設定したら忘れる” スタイルで、シークレットを CI/CD プラットフォームの設定ファイルに埋め込むケースや、バージョン管理システム (VCS )にチェックインするケースさえあります。この方法はほとんど労力がかからないので、一時的な措置としてよく行われていました。しかし、一時的な措置はそのまま永続的な措置になる傾向があります。その結果、シークレットが当初の意図以上に長い期間にわたりそのまま放置されてしまうのです。このようなシークレットは、攻撃経路になりえます。ソースコードが漏洩した場合や、エンジニアが退職した場合、シークレットのローテーションをすぐに行えるでしょうか?また、手動でのシークレットのローテーションをスケジュール化しているチームでさえ、ローテーションが遅れたり、不十分だったり、まったく行わなかったりすることがありました。シークレットを便利に利用できたとしても、会社のセキュリティ態勢は危険な状態だったのです。

一方で、シークレットへのアクセスを厳格に管理しているチームもありました。ただし、そのソリューションは利便性に乏しく、サービスを使うたびに手動アクセス限定のパスワードマネージャーや、手動の多要素認証 (MFA) を使わなければなりませんでした。その結果、開発速度が落ちるだけでなく、皮肉にも、セキュリティも低下する事態が頻発しました。多くのエンジニアが、認証情報をローカルにコピーしたり、面倒な正規プロセスを避けるためにバックドアアカウントを使ったりしたからです。シークレットは、理論上は非常に強いセキュリティで守られるはずでしたが、実際はセキュアではない場所に不適切に保管されていることが多く、おまけに、開発者のアジリティが妨げられていました。

OIDC 認証はこれら 2 つの対極的なケースの中間的な方法であり、利便性とプラットフォームのセキュリティを両立できます。CircleCI は OIDC をサポートしているので、開発者はビルド、テスト、デプロイの各ジョブで一時的な認証トークンを使用し、長期間有効な認証情報につきもののリスクを排除できます。OIDC を実装すると、セキュリティを高めながら、プロジェクトの CI/CD パイプライン全体にわたり手間を減らせるので、これまで以上に迅速かつ効率的に開発ができます。OIDC の基本と、CircleCI OIDC トークンを使って AWS と Google Cloud で認証する方法については、以前のブログ記事で説明しました。今回は、OIDC を使用して HashiCorp Vault で認証する方法を説明します。

HashiCorp Vault とは?

HashiCorp Vault とは、ID ベースのシークレットおよび暗号化管理システムです。最近では、GitOps と Infrastructure-As-Code の導入に舵を切る IT 企業が増えています。そのような企業がよく直面している難題が、機密性が高いので VCS にチェックインできず、よく使うからといって作業環境に放置しておくこともできない情報をどうしたらセキュアに保存、アクセスできるのか、というものです。この問題を解決し、セキュアな中央システムでシークレットの保管、利用、ローテーションを便利に行えるのが、HashiCorp Vault です。OIDC を使用して CircleCI から Vault に認証すると、長期間有効な認証情報を Vault の外部で保管する必要がなくなるので、セキュリティを強化できます。

このチュートリアルでは、CircleCI の OIDC トークンを使用して既存の Vault クラスタに認証する方法を説明します。

ステップ 0: 準備

始める前に、以下を用意する必要があります。

また、CircleCI の UI から以下の情報を集めます。

  • 組織の名前と ID: CircleCI Web アプリ を開いて [Organization Settings (組織設定)] > [Overview (概要)] に移動すると、両方とも確認できます。
  • プロジェクト ID: CircleCI Web アプリを開いてプロジェクトのページに移動し、[Project Settings (プロジェクト設定)] > [Overview (概要)] の順にクリックします。
  • パーソナルアクセストークン: 作成方法は、こちらをご覧ください。

注: CircleCI から Vault インスタンスに接続する必要があります。インスタンスへのパブリック アクセスを許可している場合は、CircleCI の IP アドレスの範囲をホワイトリストに追加し、CircleCI マシンからのトラフィック送信のみを受け付けるようにして、アクセスを制限します。インスタンスをプライベートネットワーク上に配置している場合は、プライベートネットワーク内の CircleCI ランナーで Vault 認証ジョブを実行します。

ステップ 1: Vault を設定する

Vault インスタンスで JWT 認証方法を有効にする必要があります。Vault にログインして、以下のように JWT 認証方法を有効にします。

export VAULT_ADDR="your vault instance address"
export VAULT_TOKEN="your vault token"
vault login $VAULT_TOKEN
vault auth enable jwt

次に、CircleCI の組織から OIDC トークンを受け取るように、JWT 認証方法を構成します。

vault write auth/jwt/config \
    oidc_discovery_url="https://oidc.circleci.com/org/<your org id>" \
    bound_issuer="https://oidc.circleci.com/org/<your org id>"

以下のコマンドを実行して、設定を確認します。

curl --header "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/auth/jwt/config" | jq

次は、Vault ロール用の [Vault ポリシー]((https://developer.hashicorp.com/vault/docs/concepts/policies){: target=”_blank”}を作成します。このチュートリアルでは、secret/data/circleci-demo/ パスの下にあるすべてのシークレットに対する読み取りアクセス権を CircleCI に付与します。

vault policy write circleci-demo-policy - <<EOF
# Grant CircleCI project <your project name> RO access to secrets under the 'secret/data/circleci-demo/*' path
path "secret/data/circleci-demo/*" {
  capabilities = ["read", "list"]
}
EOF

以下のコマンドを実行して、ポリシーが作成されたことを確認します。

vault policy read circleci-demo-policy

JWT 認証方法に、CircleCI 用の Vault ロールを circleci-demo という名前で作成します。このロールの属性で、OIDC トークンの送信元、トークン所持者に付与するアクセス許可の種類、トークンに含める追加クレームを Vault に伝えます。追加クレームを Vault ロールの bound_claims フィールドで使用することで、ロールでアクセス可能なプロジェクトやジョブを限定し、指定のコンテキストを利用できます。

このチュートリアルでは、project_id という追加クレームを使用して、アクセスをこのチュートリアルのプロジェクトのみに限定します。CircleCI OIDC トークンのクレームの詳細については、OIDC に関する CircleCI ドキュメントをご覧ください。

vault write auth/jwt/role/circleci-demo -<<EOF
{
  "role_type": "jwt",
  "user_claim": "sub",
  "user_claim_json_pointer": "true",
  "bound_claims": {
    "oidc.circleci.com/project-id": "<your project id>"
  },
  "policies": ["default", "circleci-demo-policy"],
  "ttl": "10m"
}
EOF

以下のコマンドを実行して、ロールが作成されたことを確認します。

vault read auth/jwt/role/circleci-demo

最後に、CircleCI からアクセスするシークレットをいくつか作成します。まず、新しいシークレットエンジンを有効にして secret/ にマウントします。

vault secrets enable -version=2 -path=secret kv

シークレットを作成します。

vault kv put secret/circleci-demo/demo-secrets \
  username="paul.atreides" \
  password="lis@n-al-g4ib"

以下のコマンドを実行して、新しいシークレットが作成されたことを確認します。

vault kv get secret/circleci-demo/demo-secrets

ステップ 2: CircleCI を設定する

Vault 接続シークレットのための CircleCI コンテキストを作成する

Vault のエンドポイントとロール名は、VCS にチェックインすると漏洩のおそれがありますが、 CircleCI コンテキストならセキュアに保存できます。このチュートリアルでは、CircleCI CLI を使用してコンテキストを作成し、設定する方法について説明します。

注: CircleCI スタンドアロン組織を使用している (例: GitLab を VCS として使用している) 場合、コンテキストの作成と設定には CircleCI CLI ではなく CircleCI UI を使う必要があります。

以下のコマンドを実行して、CircleCI CLI をセットアップします。プロンプトが表示されたら、お使いのパーソナルアクセストークンを入力します。

circleci setup

CLI の構成が終わったら、以下のコマンドを使用して circleci-vault-demo という名前のコンテキストを作成します。VCS の値は gh (GitHub) と bb (Bitbucket) のどちらかです。

export CIRCLE_VCS=<your vcs>
export CIRCLE_ORG_NAME=<your org name>
circleci context create $CIRCLE_VCS $CIRCLE_ORG_NAME circleci-vault-demo

以下のコマンドを実行して、新しいコンテキストが作成されたことを確認します。

circleci context list $CIRCLE_VCS $CIRCLE_ORG_NAME

次に、以下のコマンドを使用して、Vault 接続シークレットを新しいコンテキストに追加します。各コマンドの実行後にシークレットの入力を求められるので、値を用意していてください。以下の表に、値の例を示します。

circleci context store-secret $CIRCLE_VCS $CIRCLE_ORG_NAME circleci-vault-demo VAULT_ADDR
circleci context store-secret $CIRCLE_VCS $CIRCLE_ORG_NAME circleci-vault-demo VAULT_ROLE
コンテキスト変数名
VAULT_ADDR Vault インスタンスのポート番号付き URL (例: https://vault.example.com:8200)
VAULT_ROLE CircleCI 用の Vault ロール (これまでのステップに従って作業した場合、circleci-demo)

以下のコマンドを実行して、シークレットが作成されたことを確認します。

circleci context show $CIRCLE_VCS $CIRCLE_ORG_NAME circleci-vault-demo

OIDC を使用して Vault で認証する CircleCI 設定ファイルを作成する

いよいよ CircleCI 設定ファイルを作成します。設定ファイルでは、CircleCI OIDC トークンを使用して Vault で認証し、先ほど作成したシークレットを Vault の自動認証機能を使って取得した後、CircleCI ジョブ用に環境変数としてエクスポートする処理を記述します。

このチュートリアル用に作成したリポジトリに、.circleci/config.yml ファイルを作成し、以下のコードを入力します。

version: 2.1
commands:
  install-vault:
    steps:
      - run:
          name: Install Vault and prereqs
          command: |
            vault -h && exit 0 || echo "Installing vault"
            # only runs if vault command above fails
            cd /tmp
            wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip
            unzip vault_1.12.2_linux_amd64.zip
            sudo mv vault /usr/local/bin
            vault -h
  vault-auto-auth:
    description: "Use Vault auto auth to load secrets"
    steps:
      - run:
          name: Auto-authenticate with Vault
          command: |
            # Write the CircleCI provided value to a file read by Vault
            echo $CIRCLE_OIDC_TOKEN > .circleci/vault/token.json
            # Substitute the env vars in our context to render the Vault config file
            sudo apt update && sudo apt install gettext-base
            envsubst < .circleci/vault/agent.hcl.tpl > .circleci/vault/agent.hcl
            # This config indicates which secrets to collect and how to authenticate
            vault agent -config=.circleci/vault/agent.hcl
      - run:
          name: Set Environment Variables from Vault
          command: |
            # In order to properly expose values in Environment, we _source_ the shell values written by agent
            source .circleci/vault/setenv
jobs:
  setup-vault-and-load-secrets:
    docker:
      - image: cimg/base:2023.01
    steps:
      - checkout
      - install-vault
      - vault-auto-auth
      - run:
          name: Use secrets retrieved from Vault in a subsequent step
          command: |
            echo "Username is $SECRET_DEMO_USERNAME, password is $SECRET_DEMO_PASSWORD"
workflows:
  vault:
    jobs:
      - setup-vault-and-load-secrets:
          context:
            - circleci-vault-demo
# VS Code Extension Version: 1.5.1

次は、以下のコードを使用して、Vault エージェント設定ファイルのテンプレートを .circleci/vault/agent.hcl.tpl に作成します。このファイルは、接続先の Vault サーバーと認証方法を Vault エージェントに指示するためのものです。

pid_file = "./pidfile"
exit_after_auth = true
vault {
  address = "${VAULT_ADDR}"
  retry {
    num_retries = -1
  }
}
auto_auth {
  method "jwt" {
    config = {
      role = "${VAULT_ROLE}"
      path = ".circleci/vault/token.json"
      remove_jwt_after_reading = false
    }
  }
  sink "file" {
    config = {
      path = "/tmp/vault-token"
    }
  }
}
template_config {
  exit_on_retry_failure = true
}
template {
  source      = ".circleci/vault/secrets.ctmpl"
  destination = ".circleci/vault/setenv"
}

最後に、取得したシークレットを使って行う処理を Vault エージェントに指示するための Consul テンプレートを作成します。このチュートリアルでは、パイプラインで使用できるよう、単に環境変数としてエクスポートします。.circleci/vault/secrets.ctmpl ファイルを作成して、以下のコードを追加します。

# Export the secrets as env vars for use in this job
# Also writes them to $BASH_ENV so that they'll be available as env vars in subsequent jobs for this pipeline
{{ with secret "secret/circleci-demo/demo-secrets" }}
    export SECRET_DEMO_USERNAME="{{ .Data.data.username }}"
    export SECRET_DEMO_PASSWORD="{{ .Data.data.password }}"
    echo "export SECRET_DEMO_USERNAME=\"{{ .Data.data.username }}\"" >> $BASH_ENV
    echo "export SECRET_DEMO_PASSWORD=\"{{ .Data.data.password }}\"" >> $BASH_ENV
{{ end }}

変更をプッシュします。これで、CircleCI でパイプラインがトリガーされます。ローカルのリポジトリディレクトリから、以下のコマンドを実行します。

circleci open

これで、CircleCI Web アプリでプロジェクトが開きます。すべての設定が適切であれば、以下のように、最後のステップに Vault から取得したシークレットが表示されます。

Success!

まとめ

この記事では、OIDC 認証を使用して、CircleCI から HashiCorp Vault にシークレットをセキュアに保管し、アクセスする方法について説明しました。OIDC 認証を使うと、セキュアなシステムの外部に長期間有効な認証情報を保管する必要がなくなるので、開発速度を低下させることなく組織のセキュリティ態勢を強化できます。CircleCI での OIDC 認証の詳細については、CircleCI のドキュメントまたは以下の記事をご覧ください。