PATも秘密鍵も管理せずにGoogle CloudからGitHub APIにアクセスする

Google Cloud上のアプリケーションからGitHub APIにアクセスするとき、PATあるいはGitHub AppのInstallation Access Tokenが必要となります。しかしPATはユーザに紐付くため管理が厄介ですし、Appを作れば秘密鍵の管理について考えなければなりません。

この記事では、すでに運用しているOcto STSとGoogle CloudのOIDC ID Tokenを組み合わせることで、新たなGitHub Appを作らずにGitHub APIへのアクセスを実現した事例を紹介します。10XではOcto STSを使ったToken運用の改善で紹介したように、GitHub Actions上での社内の様々なユースケースでGitHub AppのInstallation Access Token(以下GitHub App Token)の発行にOcto STSを活用していますが、今回はGitHub Actions以外からの利用事例を紹介します。

全体のフローは以下の通りです。

sequenceDiagram
    participant App as Google Cloud上のアプリ
    participant Meta as GCE Metadata Server
    participant STS as Octo STS
    participant GH as GitHub API

    App->>Meta: OIDC ID Token を要求
    Meta-->>App: ID Token (iss: accounts.google.com)
    App->>STS: Token Exchange (Bearer ID Token)
    STS->>STS: Trust Policy で検証
    STS-->>App: GitHub Installation Token
    App->>GH: API呼び出し (Installation Token)
    GH-->>App: レスポンス

Google Cloudのメタデータサーバーから取得したOIDC ID TokenをOcto STSに渡し、短命なGitHub App Tokenに交換することで、このTokenでGitHub APIにアクセスできます。Octo STSで利用しているGitHub Appのパーミッションの範囲に収まるユースケースであれば、新しいGitHub Appの作成も鍵の管理も不要です。とても便利ですね!

以下、自分たちがこの仕組みに至った背景と、技術的な詳細を書きます。

何を作ったか

Slackのメッセージにリアクションを付けるだけでGitHub Issueが自動作成されるSlack botの機能を作りました。botはGoogle App Engine(GAE)上で動いており、GitHub APIにアクセスするためには何らかのCredentialが必要でした。

モチベーション: Slack起点のタスク管理

Slackを非同期コミュニケーションのメインツールとして使っていると、会話の中からタスクが発生することは珍しくありません。質問がそのままタスクになったり、非エンジニアのメンバーからSlack上で直接依頼が来たりします。

自分たちのチームでもSlack起点のタスク管理は長年の課題で、いくつかのアプローチを試してきました。

Zapier + Notion(2022〜2023年)

Slackのメッセージに特定のリアクション(:asksre:)を付けると、チームチャンネルに展開され、NotionのDatabaseにも登録される仕組みをZapierで構築しました。

  • 良かった点
    • どこで発生した会話でもチームチャンネルに展開されるので即座に可視化でき、翌朝のチーム定例で会話できる
  • 課題
    • Zapierの連携がよく壊れる。気づかないうちにタスクが拾えていないことがあった
    • SlackやNotionのAPIトークンをZapierに渡す必要があり、セキュリティ面でも不安があった

Slack リスト(2024年〜)

Zapierの不安定さから、Slackのリスト機能に切り替えました。

  • 良かった点
    • Slackに閉じた管理ができる
    • 外部サービスへのシークレット共有が不要。No codeでタスク管理できる手軽さ
  • 課題
    • チームチャンネルへの自動コメントのようなワークフローが組めない。Slackが用意した仕組みから外れるようなことをしようとすると結局コードを書かないといけない。コードを書かないといけないならお手軽なリスト機能を使い続ける理由がない
    • リスト機能自体の使い勝手にも難があった。カンバンUIのカードの挙動がピーキーだったり、ステータス管理では後から追加したステータスの並び順を変更できず、Doneを最初に作ってしまうとリスト内がDoneから始まるなど、細かいが日常的に使うものとしては気持ちよくない部分が多い

共通の課題: Issue化忘れ

どの方式でも一番の問題はIssue化の忘れでした。共有するだけで済む話ならその場の会話で終わりですが、タスクとして残すべきものはGitHub Issueにしたい。Issue化を忘れると、Slackのスレッドやリストに残ったまま誰にもアサインされず、タスクの内容もIssueとして記録されません。

いっそのことVibeで欲しいものを一気に作ってしまおうということで、リアクションを付けた瞬間にGitHub Issueまで作る仕組みを実装することにしました。

認証方式の検討

10Xでは10x_botというSlack botを2022年からGAE上で運用しています。もともとは感謝を送り合うCheers機能や勤怠打刻(King of Time連携)など、社内の汎用的な機能を集約するbotとして作られたものです。GAEを採用しているのは、Slackのwebhookイベントを常時受け付ける必要があることと、VPC経由でのDB接続やSecret Managerとの連携が必要だったためです。

このbotにリアクション→Issue自動作成の機能を追加するにあたり、GitHub APIの認証方式を検討しました。

なお、先に紹介したように10XではOcto STSを導入しています。Octo STSは、OIDCのID Tokenを受け取り、Trust Policyに基づいて検証した上で、スコープを絞ったGitHub App Tokenを発行するサービスです。リポジトリに配置したYAMLファイル(Trust Policy)で「誰が」「どのリポジトリに」「どの権限で」アクセスできるかを宣言的に管理できます。詳しくは以前の記事をご覧ください。

最初に思いついた選択肢は2つです。

  1. 新規GitHub Appを作成する: 秘密鍵をSecretをKMSに保存し、GAEから直接トークンを発行する
  2. GitHub Actions経由でOcto STSを使う: botからGitHub Actionsのワークフローをトリガーし、ワークフロー内でOcto STSのidentityを使ってIssueを作成する

案1はシンプルですが、新しいGitHub Appの作成と鍵管理が発生します。案2は鍵管理は不要ですが、GitHub Actions経由の間接的な呼び出しになり、余計なレイテンシがかかります。

Service AccountのOIDC ID TokenでOcto STSを叩く

どちらもしっくり来なかったのでSecurityチームに相談したところ、「GAEのService AccountでOIDC ID Tokenを発行して、Octo STSに直接渡せないか」という提案をもらいました。

Octo STSはOIDC ID Tokenを受け取ってGitHub App Tokenを返すサービスですが、提示するID TokenはGitHub のものに限定されているわけではありません。Octo STSのREADMEにはGoogle accountsをissuerにした例が載っています。

issuer: https://accounts.google.com
subject_pattern: "[0-9]+"
claim_pattern:
  email: ".*@chainguard.dev"

つまり、Google CloudのService AccountからOIDC ID Tokenを発行し、それをOcto STSに渡せば、GitHub Actionsを経由せずに直接GitHub App Tokenを取得できるはずです。

実際、以前の記事でもこの可能性に触れていましたが、当時は未検証でした。今回のSlack bot開発がまさにそれを実証する機会になりました。

仕組み

OIDC ID Tokenの取得

GAE・Cloud Run・GCEなどのCompute環境では、メタデータサーバーを通じてService AccountのOIDC ID Tokenを直接取得できます。IAM Credentials APIを明示的に呼んだり、roles/iam.serviceAccountOpenIdTokenCreatorロールを付与したりする必要はありません。google-auth-libraryを使えばアプリケーションコードから簡単に発行できます。

import { GoogleAuth } from 'google-auth-library';

const OCTO_STS_AUDIENCE = 'octo-sts.example.com';

const auth = new GoogleAuth();
const client = await auth.getIdTokenClient(OCTO_STS_AUDIENCE);
const idToken = await client.idTokenProvider.fetchIdToken(OCTO_STS_AUDIENCE);

発行されるID Tokenは以下のようなclaimsを持ちます。

{
  "iss": "https://accounts.google.com",
  "sub": "123456789012345678901",
  "aud": "octo-sts.example.com",
  "email": "my-app@my-project.iam.gserviceaccount.com",
  "email_verified": true
}

subにはService Accountのメールアドレスではなく数値のユニークIDが入ります。このIDはイミュータブルで、Service Accountが存在する限り変わりません。

Trust Policy

Octo STSは、リポジトリの.github/chainguard/ディレクトリに配置されたTrust Policyファイルに基づいてトークンを検証します。今回は以下のようなPolicyを作成しました。

# .github/chainguard/my_bot_issue_writer.sts.yaml
issuer: https://accounts.google.com
subject: "123456789012345678901"
claim_pattern:
  email: "my-app@my-project\\.iam\\.gserviceaccount\\.com"

permissions:
  issues: write

issuerにGitHuのOIDC Providerではなくhttps://accounts.google.comを指定しています。10Xの既存のTrust Policyはすべてhttps://token.actions.githubusercontent.comを使っていたので、Google Cloud OIDCのissuerを使うのはこれが初めてのケースでした。

subjectにはService Accountの数値IDを指定し、claim_patternでメールアドレスも検証しています。permissionsissues: writeだけに絞っています。

このPolicyファイルはIssueを作成したいリポジトリに配置します。自分たちの場合、GitHub Issueの作成先であるリポジトリに直接置きました。

Token Exchange

ID Tokenを取得したら、Octo STSのエンドポイントにリクエストを送ります。

const response = await fetch(
  `https://octo-sts.example.com/sts/exchange?scope=myorg/myrepo&identity=my_bot_issue_writer`,
  {
    headers: {
      Authorization: `Bearer ${idToken}`,
    },
  },
);

なお、Octo STSは内部でgRPCを使っており、HTTPエンドポイントはgrpc-gateway経由で提供されています。レスポンスは{"token": "ghs_xxx"}のようなJSON形式で返ってくるので、パースしてtokenフィールドを取り出す必要があります。

取得したTokenでOctokitを初期化すれば、あとは普通にGitHub APIを叩けます。

const octokit = new Octokit({ auth: githubToken });
await octokit.issues.create({
  owner: 'myorg',
  repo: 'myrepo',
  title: 'Slackからの問い合わせ',
  body: '...',
});

何が嬉しいのか

この方式の最大の利点は、新しいGitHub Appの作成もCredentialの永続的な管理も不要だということです。

GitHub App方式 Octo STS方式
新規GitHub App 必要 不要
Credential管理 秘密鍵 なし
レイテンシ 数秒 数秒
権限管理 GitHub App設定画面 Trust Policy(コードレビュー可能)

権限管理がTrust Policyファイルとしてリポジトリに置かれる点も重要です。GitHub Appの権限設定は管理画面でしか確認できませんが、Trust Policyはコードとしてレビューでき、変更履歴も残ります。

また、GitHub App Tokenは短命なので、リクエストごとに都度発行して使い捨てにしています。長期間有効なCredentialがどこにも存在しない状態を作れます。

まとめ

今回、GAE上のSlack botからGitHub Issueを作成するにあたり、Google CloudのService AccountのOIDC ID TokenでOcto STSからGitHub App Tokenを方法を採用しました。新しいGitHub Appの作成も鍵管理も不要で、Trust Policyの追加だけで認証が完結します。

以前の記事ではOcto STSがGitHub Actions以外のOIDC Providerでも動くはずだと書きましたが、今回それを実際のプロダクション環境で確認できました。GAE・Cloud Run・GCEなど、メタデータサーバーからID Tokenを取得できる環境であればどこでも同じアプローチが使えるので、CI以外の文脈でもOcto STSの活用を検討してみてください。

参考