セキュリティ

コードをコミットおよびデプロイするCIボットのための最小権限

広範な書き込み権限と無制限のデプロイ権限を持つ自動化ボットは、1回の不正な実行をシステム全体の大規模障害に変えてしまいます。ここでは、その影響範囲を縮小する方法を説明します。

この記事は英語の原文をAIモデルが翻訳したものです。表現が原文と異なる場合があります。 英語の原文を読む

コードをコミットし、ブランチをプッシュし、デプロイをトリガーする自動化ボットは、本番環境へのアクセス権を持ち、人間が介在しないマシンアカウントです。簡単なセットアップでは、長期的なトークン、リポジトリ全体への書き込みアクセス権、そして何でもデプロイできる権限が与えられます。これは最初の試みで機能するため、広まっていきます。これはまた、一度の誤作動、プロンプトインジェクション、またはトークンの漏洩が、誰かが気づく前にあらゆるファイルを書き換え、あらゆるサービスをリリースできることも意味します。この記事は、その爆風範囲(blast radius)を縮小することについてです。各ボットに独自のアイデンティティ、短期的な認証情報、限定的なパスの許可リスト、そして署名付きの出力を与えることで、不正な実行による損害が全体ではなく隅に留まるようにします。

なぜボットの影響範囲は人間よりも大きいのか

広範なアクセス権を持つ人間でも、人間の速度という制約を受けます。いくつかのファイルを開き、考え、変更を加え、同僚がプルリクエストをレビューします。間違いは、発見できるほどに小さく、ゆっくりと行われる傾向があります。

ボットにはそうしたブレーキが一切ありません。ボットはミリ秒単位で動作し、到達可能なすべてのリポジトリに対して同じアクションを繰り返し、変更が間違っているように見えても立ち止まって考えることはありません。ロジックに欠陥がある場合や、アカウントが乗っ取られた場合、セットアップを便利にしたその広範な権限が、今度は攻撃者がトークンの全範囲にわたってマシン速度で移動することを可能にします。便利なデフォルト設定と最悪の事態は、順調な日と不運な日という状況の違いでしかない、同じ権限セットなのです。

したがって、設計目標は「ボットを信頼する」ことではありません。ボットは、人間が信頼されるような意味では信頼できません。なぜなら、その背後には判断力がなく、ルールと与えられた入力しか存在しないからです。目標は、完全に侵害された実行であっても被害が小規模に留まるように、ボットができることに上限を設けることです。すべての自動化アカウントを、信頼されたアクタではなく、限定的な能力として扱ってください。

各ボットにそれぞれのアイデンティティを付与する

最初の間違いは、1つの強力なアカウントを多くのジョブで共有することです。テストランナー、リリースジョブ、ドキュメントパブリッシャー、そして3つの自動化スクリプトがすべて認証に使用する単一のマシンアイデンティティは、完全な単一障害点となります。これらのジョブのいずれか1つが侵害されると、共有アカウントができることすべての権限が渡ってしまいます。

それらを分割してください。個別のジョブにはそれぞれ、そのジョブが必要とする権限のみを持つ独自のアイデンティティを付与します。ドキュメントを公開するアイデンティティは、決済サービスをデプロイできるべきではありません。テストを実行するアイデンティティは読み取りアクセスのみを持つべきです。なぜなら、テストは本番環境に何かを書き込む必要がないからです。

アイデンティティを分離すると、監査ログも読みやすくなります。1つのアカウントがすべてを実行する場合、「ciアカウントがコミットをプッシュしました」というログ行は、ほとんど何も教えてくれません。ドキュメントボットが独自の名前を持っている場合、ドキュメントボットがドキュメントディレクトリの外に書き込みを行っていることを示すエントリは、アラートを発することができる明白な異常です。アイデンティティごとのスコープ設定は、アクセスログを有効な侵入検知シグナルに変えます。

正確なパスとリソースに権限のスコープを限定する

最小権限とは、プラットフォームではなく、ジョブに一致する権限を付与することを意味します。changelogを更新するボットは、ツリー全体ではなく、1つのファイルへの書き込みアクセス権を必要とします。1つのサービスをデプロイするボットは、プロジェクト内のすべてのサービスではなく、そのサービスへのデプロイ権限を必要とします。

ここでは、2つの制限がほとんどの役割を果たします。ボットが操作できるリソースを制限し、ボットが書き込めるパスを制限します。デプロイIDは特定のサービスまたは名前空間にバインドされるべきです。そうすれば、有効なトークンであっても他のものに触れることはできません。コミットボットは、その担当範囲外の変更を拒否するブランチ保護ルール、または差分が逸脱した場合に実行を失敗させるチェックのいずれかによって、ディレクトリに制約されるべきです。

# Per-bot permission model. Each identity gets only what its job requires.
identities:
  changelog-bot:
    repo_write: true
    path_allowlist:
      - "CHANGELOG.md"
      - "docs/releases/**"
    deploy: none

  service-deploy-bot:
    repo_write: false          # deploys, does not commit
    deploy:
      target: "web-frontend"   # this one service, nothing else
      environments: ["staging", "production"]

  test-bot:
    repo_write: false          # read-only; tests never write prod
    deploy: none

モデルを書き出すことの要点は、「書き込みアクセス権を与える」という決定をより正確に下せるようになるという点です。ほとんどすべてのボットは、完全な権限付与よりもはるかに少ない権限しか必要とせず、より狭い権限付与は一度設定すれば実行時にコストはかかりません。

広範なアクセスと最小権限の並列比較

この2つのセットアップは、通常時は似ているように見えます。しかし、問題が発生した日には、全く異なる様相を呈します。以下の表は、それぞれのモデルにおける同じ自動化ボットを示しています。

プロパティ 広範なアクセス 最小権限
アイデンティティ 全ジョブで1つの共有アカウント ジョブごとに1つのアイデンティティ
リポジトリへの書き込み ツリー全体 許可リストに登録されたパスのみ
デプロイ範囲 あらゆるサービス、あらゆる環境 指名された1つのサービスと環境
認証情報の有効期間 長期トークン、有効期限なし 実行ごとに発行、数分で失効
漏洩したトークンが与える権限 無期限の完全な書き込みとデプロイ トークンが失効するまで、1つのパスまたは1つのサービス
誤った実行の影響範囲 あらゆるファイル、あらゆるサービス ボットのレーンのみ
監査シグナル 「ciアカウントが何かを実行した」 指名されたアクター、レーン外のアクションに関するアラート
ローテーションの負担 手動、忘れやすい なし、認証情報は一時的なもの

最も重要な行は、漏洩と誤った実行に関する行です。広範なアクセスでは、1つの侵害されたトークンまたは1つのバグのある実行が、すべてに影響を及ぼします。最小権限では、同じ障害は単一のパスまたは単一のサービスに限定され、認証情報の有効期間が短いため、盗まれたトークンは実行が終了するとほぼ無価値になります。

短期的な認証情報で長期的なシークレットを排除する

シークレットに保存された長期的なトークンは、継続的な負債です。それは期限切れにならず、ログや侵害された依存関係を通じて漏洩する可能性があり、もし漏洩しても、盗まれたトークンは本物のボットと全く同じに見える呼び出しを生成するため、気づかないことがよくあります。ローテーションは手作業の雑務であるため、トークンは蓄積され、それを必要としたジョブよりも長く存続します。

より良いパターンは、実行ごとに新しく発行され、数分後に期限切れとなる認証情報です。多くの自動化プラットフォームは、実行に対して署名付きのアイデンティティアサーションを提示でき、ターゲットシステムは、特定の1つの呼び出し元に対してそのアサーションを信頼し、短期的なアクセストークンと交換するように設定できます。永続的なシークレットは保存されないため、漏洩したり、ローテーションしたり、数ヶ月後にボールトで忘れ去られているのを発見したりするものはありません。信頼は正確なアイデンティティ、そして多くの場合、正確なブランチに固定されているため、他の場所からのアサーションは、実際の権限に到達する前に交換に失敗します。

プラットフォームが実行ごとの認証情報を発行できず、保存されたトークンを使用せざるを得ない場合は、短いローテーションでスコープを限定したものにし、上記の最小権限モデルと同様にスコープを狭く保ちます。スコープが厳密に限定された短期的なキーは、防御可能なフォールバックです。広範で、決して期限切れにならないものは、設計から排除すべきものです。

1つの漏洩が連鎖しないように、コンテンツの書き込みとデプロイを分離する

便利なため、すべての権限を1つのクレデンシャルにまとめるのは、巧妙な罠です。もし同じトークンがコードのコミットとデプロイのトリガーの両方を行える場合、その漏洩はチェーン全体を明け渡すことになります。攻撃者は悪意のある変更を書き込み、2つ目の障壁なしにワンアクションでそれをシップしてしまいます。

トークンを機能ごとに分離してください。コンテンツを書き込むクレデンシャルはデプロイができないようにし、デプロイするクレデンシャルはコンテンツを書き込めないようにすべきです。こうすることで、書き込み側の漏洩ではファイルを汚染できても、それ単体で本番に反映させることはできなくなり、デプロイ側の漏洩では既存のアーティファクトを再デプロイできても、新しい悪意のあるコードを導入することはできなくなります。攻撃者がエンドツーエンドの侵害を完了するには両方が必要となり、これは1つだけを必要とする場合よりもはるかに高いハードルです。

# Two credentials, two jobs, no overlap.
content-writer:
  can_commit: true
  can_deploy: false      # writing alone cannot reach production

deployer:
  can_commit: false      # deploying alone cannot introduce new code
  can_deploy: true
  target: "web-frontend"

これは、読み取りと書き込みを分離しておくのと同じ論理を、一段階上に適用したものです。責務を複数の認証情報に分割することは、盗まれた単一のシークレットが、ソースの変更から本番トラフィックに至るまでの実行全体を担うことがないことを意味します。

オリジンを検証できるようにコミットとアーティファクトに署名する

スコープ設定はボットができることを制限します。署名は、ボットが実際に何をしたかを証明できるようにします。ボットがそのコミットとビルドするアーティファクトに署名すると、すべての出力に検証可能なオリジンのマークが付与され、署名されていないものや間違った鍵で署名されたものは、明らかに異質なものとして識別されます。

これが重要なのは、権限セットを狭くしても、混乱したボットや乗っ取られたボットがその権限の範囲内で出力を生成する余地が残るからです。署名はその出力を、チェックできるものに変えます。デプロイステップでは、期待されるビルドIDで署名されていないアーティファクトのシップを拒否できます。そのため、署名されていないバイナリをパイプラインに紛れ込ませた攻撃者は、シップされるのではなく、ゲートで拒否されます。署名されたコミット履歴は、攻撃者がボットの署名鍵を保持していないため、リリースボットからのものであると主張する偽造コミットが検証に失敗することを意味します。

ボットの出力が後で実行される何かに流れ込む場合、署名はセットアップする価値があります。これを上記の短期的な認証情報と組み合わせ、署名鍵自体は最小限かつ分離した状態に保つことで、オリジンの検証は手動の信頼判断ではなく、自動的なチェックになります。

トレードオフと、シンプルにしておくべき場合

これらはどれも無料ではありません。正直なコストは、セットアップの複雑さと継続的な構造です。広範な権限を持つトークンを使った1つの共有アカウントは、数回のクリックで済みます。最小権限バージョンは、複数のID、コミットボットごとのパス許可リスト、デプロイボットごとのリソースバインディング、短命なクレデンシャルのためのフェデレーショントラスト、コンテンツトークンとデプロイトークンの分割、そして独自の検証ステップを持つ署名キーで構成されます。これは実際の作業であり、特に実行ごとのクレデンシャルトラストは、意図したよりも多くの呼び出し元を信頼してしまうという、緩い方向への微妙な間違いを犯しがちです。一見すると機能しているように見えますが。これらの信頼条件を正確に記述し、間違ったレーンからのIDが実際に拒否されることをテストするための時間を確保してください。

その見返りは、コストは一度支払えばその後はほとんどなくなり、それが取り除くリスクは常時存在し、繰り返し発生するものであるという点です。ローテーションするトークンはなく、スコープが静かに拡大していく共有アカウントもなく、侵害された実行はシステム全体ではなく、1つのパスまたは1つのサービスに限定されます。

それでも判断は必要です。実際のデータがなく、寿命が数日間の使い捨てサンドボックスは、完全な仕組みを必要としません。なぜなら、プロジェクト全体が来週削除されるのであれば、影響範囲はすでに小さいからです。エミュレーターに対して実行されるローカルスクリプトは、本番環境のクレデンシャルを全く必要としません。完全なモデルがそのコストに見合う価値を発揮するのは、ボットが重要なもの、つまり人々が依存するリポジトリ、実際のユーザーへのデプロイパス、本番環境で実行されるアーティファクトに対して常時アクセスを持つ場合です。ライブシステムに対して独自にコミットやデプロイを行う自動化にとって、広範な権限付与は、今日数分を節約する代わりに、それが存在する限りシステム全体にわたる負債を抱えることになります。最小権限は、一度だけ長い午後を費やすコストがかかりますが、その後はすべての不正な実行を小規模に抑えます。