0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actions が鍵なしで AWS を触れる理由 ― OIDC と AssumeRole の中身

0
Last updated at Posted at 2026-06-02

はじめに

GitHub Actions から AWS を操作するパイプラインを組んだことがある人は多い。最近は Pulumi や Terraform で IaC(Infrastructure as Code、インフラをコードで定義する手法)を回す。CI のジョブからインフラを構築するのも珍しくない。

その際、昔のように AWS_ACCESS_KEY_ID を Secrets に貼る代わりに、OIDC(OpenID Connect)連携を使う。AWS 側で IAM(Identity and Access Management)Role の信頼ポリシーに GitHub を足す。すると、ワークフローが一時的なクレデンシャルを受け取って AWS を触れる。動くし、便利だ。

だが、ここで一度立ち止まりたい。GitHub にログインしている情報と、AWS にログインしている情報は、いったいどこで突き合わされているのか。 パスワードを共有しているわけでもないのに、なぜ AWS は「この GitHub Actions を信じてよい」と判断できるのか。

この記事を貫くテーマは、次の二つである。

  • 信頼ポリシーは入口の許可リストにすぎない:本人確認の実体は、GitHub が発行する OIDC ID トークン(署名付き JWT=JSON Web Token)が担う。
  • SAML(Security Assertion Markup Language)/ OAuth2.0(OAuth=Open Authorization)/ OIDC を一枚で整理する:そうすると、なぜ「OIDC + AssumeRoleWithWebIdentity による短期クレデンシャル」がデファクトになったのかが見えてくる。

シニアエンジニアが「中身を説明できる」状態を目標に、署名と検証のレイヤーまで降りていく。


1. まず誤解を解く:信頼ポリシーは「認可」であって「認証」ではない

最初にほどきたい誤解がある。「信頼ポリシーに GitHub を書いたから、AWS が GitHub のログインを照合している」という思い込みだ。これは半分正しく、半分間違っている。

セキュリティの世界では、二つの問いを必ず分けて考える。

  • 認証(Authentication / Authn):お前は誰だ。
  • 認可(Authorization / Authz):お前は何をしてよいか。

空港でたとえると分かりやすい。パスポートで「本人であること」を示すのが認証だ。搭乗券で「この便に乗ってよいこと」を示すのが認可である。係員はパスポートそのものを発行しない。すでに発行された本物のパスポートを検証するだけだ。

IAM Role の信頼ポリシー(trust policy)は、この比喩でいう搭乗券の改札ルールに当たる。「どんな素性の相手なら、このロールを引き受けてよいか」を書いた許可条件であって、相手が本物かどうかを証明する装置ではない。

具体的に見てみよう。GitHub Actions 用のロールには、おおよそ次のような信頼ポリシーが付く。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:*"
        }
      }
    }
  ]
}

ここに書いてあるのは、あくまで条件である。意味をほどくとこうだ。「発行者は token.actions.githubusercontent.com か。宛先(aud)は sts.amazonaws.com か。素性(sub)は my-org/my-repo か。すべて満たすなら、引き受けを許す」。

肝心の「その身分証は本物か」は、このポリシーには書かれていない。本物かどうかの判定、つまり認証は別のレイヤーが担う。それが OIDC であり、その検証主体が AWS STS(Security Token Service)だ。

つまり「信頼ポリシーを書いたら照合してくれる」のではない。身分証の検証という土台があってはじめて、信頼ポリシーの条件判定が意味を持つ。 この土台を理解するために、次章で登場人物を整理する。


2. 登場人物を整理する:IdP / RP と、SAML・OAuth2.0・OIDC の関係

認証・認可の話には、決まった登場人物がいる。名前を押さえると一気に見通しがよくなる。

  • IdP(Identity Provider):身分を保証する側。今回でいう GitHub。
  • RP(Relying Party):その保証を信じて受け入れる側。今回でいう AWS。

RP は「信じる側」だから Relying(依存する)Party と呼ばれる。AWS は GitHub の発行した身分証に依存して判断する。自分でパスワードを持つわけではない。

ここで SAML・OAuth2.0・OIDC という三つの言葉が混ざって混乱しがちだ。役割で並べると、こうなる。

規格 主目的 形式 主な使われ方
SAML 認証(SSO=Single Sign-On) XML 企業の SSO、社内システム連携
OAuth2.0 認可(権限の委譲) トークン(JSON/JWT) 「このアプリに私の代わりに○○させる」
OIDC 認証 JWT(OAuth2.0 の上) ログイン、フェデレーション

ポイントは三つある。

第一に、OAuth2.0 は本来「認可」の枠組みだ。 「誰か」を確かめる仕組みではない。たとえば外部アプリに「あなたの Google ドライブを読む権限」を渡すとき、Google が発行するのはアクセストークンだ。これは権限の委譲であって、本人確認ではない。

第二に、OIDC は OAuth2.0 の上に「本人確認」を載せた層だ。 OAuth2.0 のフローをそのまま使いつつ、ID トークン という新しい成果物を追加する。ID トークンは署名付き JWT で、「この発行者が、この主体(sub)を、この宛先(aud)向けに確認した」という事実を運ぶ。

第三に、SAML は同じ「認証」を XML で先に実現した先行規格だ。 やりたいことは OIDC と近いが、XML ベースで重く、ブラウザの SSO 文脈で根付いた。OIDC は同じゴールを JSON/JWT で軽く実現する後発で、API や CI/CD のような非ブラウザ文脈とも相性がよい。

ここで、アクセストークンと ID トークンの違いを一度はっきりさせておく。混同しやすいからだ。

  • アクセストークン:何ができるかを表す(認可)。リソースサーバーに見せる「鍵」。
  • ID トークン:誰であるかを表す(認証)。RP に見せる「身分証」。

GitHub Actions → AWS の連携で主役になるのは ID トークンのほうだ。AWS は「GitHub Actions に何かを代行させたい」のではない。「この実行主体が確かに my-org/my-repo のワークフローだ」と確認したい。だから認可トークンではなく、身分を運ぶ ID トークンを使う。

ID トークンの中身(JWT のペイロード)はおおよそこうなる。

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:my-org/my-repo:ref:refs/heads/main",
  "aud": "sts.amazonaws.com",
  "ref": "refs/heads/main",
  "repository": "my-org/my-repo",
  "exp": 1735689600
}
  • iss(issuer):誰が発行したか。GitHub の OIDC エンドポイント。
  • sub(subject):誰についての身分証か。リポジトリとブランチまで含む。
  • aud(audience):誰宛てか。ここでは AWS STS。
  • exp(expiration):いつ切れるか。数分で失効する短命なトークン。

この JWT には、GitHub の秘密鍵による署名が付く。署名こそが「本物である証拠」だ。次章で、この署名がどこでどう検証されるのかを追う。


3. GitHub Actions → AWS の AssumeRole が成立する全手順

いよいよ核心だ。「ログイン情報の突き合わせ」の正体を、手順を追って解剖する。

結論を先に言う。突き合わせの実体は「GitHub が署名した ID トークンを、AWS STS が GitHub の公開鍵で検証する」ことである。 両者で共有するパスワードや長期キーは、どこにも存在しない。あるのは「署名」と「公開鍵による検証」だけだ。これは非対称鍵暗号の典型的な使い方である。

手書きのサインに似ている。GitHub は誰にも真似できない自分のサイン(秘密鍵による署名)を書類に書く。AWS は手元のサイン見本(公開鍵)と照合し、確かに GitHub 本人のサインだと確かめる。サインを書ける本人(秘密鍵)そのものを相手に渡す必要はない。

全体の流れはこうだ。

順に見ていく。

Step 1:ワークフローが ID トークンを要求する

ワークフロー側では、まず ID トークンを発行する権限を明示する。これがないと GitHub はトークンをくれない。

permissions:
  id-token: write   # OIDC トークンの発行を許可
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-1

aws-actions/configure-aws-credentials が内部でやっているのは、こうだ。ランナーに渡される環境変数(ACTIONS_ID_TOKEN_REQUEST_URLACTIONS_ID_TOKEN_REQUEST_TOKEN)を使い、GitHub の OIDC プロバイダに aud=sts.amazonaws.com で ID トークンを要求する。ブラックボックスに見える一行の正体は、この「身分証の取得」である。

Step 2:GitHub が署名付き JWT を返す

GitHub は、実行中のワークフローの文脈(リポジトリ、ブランチ、環境)を sub などの claim(JWT 内の属性項目)に詰める。そして自分の秘密鍵で署名した JWT を返す。これが前章で見た ID トークンだ。数分で失効する。

Step 3:STS に AssumeRoleWithWebIdentity で提示する

ランナーは受け取った JWT を、AssumeRoleWithWebIdentity という STS の API に渡す。「この身分証を持つ者として、このロールを引き受けたい」という申請だ。長期キーは一切登場しない。提示するのは短命な JWT だけである。

Step 4:STS が GitHub の公開鍵で署名を検証する

ここが突き合わせの本丸だ。STS は JWT の isshttps://token.actions.githubusercontent.com)を見る。そして、その発行者の OIDC discovery ドキュメント(/.well-known/openid-configuration)をたどる。そこに書かれた jwks_uri から GitHub の**公開鍵(JWKS=JSON Web Key Set)**を取得し、JWT の署名を検証する。

署名が GitHub の秘密鍵で作られたものなら、検証は通る。改ざんされていれば通らない。この瞬間に「GitHub にログインしている情報」と「AWS にログインしている情報」が初めて接続される。 ただし接続点はパスワードの照合ではなく、署名の数学的な検証だ。

なお、AWS 側で事前に「IAM OIDC アイデンティティプロバイダ」を一度登録しておく必要がある。これは「token.actions.githubusercontent.com を発行者として信頼する」という宣言で、STS が公開鍵をたどる入口になる。

Step 5:信頼ポリシーの条件を照合し、一時クレデンシャルを発行する

署名検証を通過してはじめて、第 1 章で見た信頼ポリシーの出番になる。STS は JWT の audsub を、ポリシーの Condition と突き合わせる。すべて一致すれば、STS は一時的なクレデンシャル(アクセスキー・シークレット・セッショントークン)を発行する。

この一時クレデンシャルには寿命がある(既定は 1 時間程度、設定で短縮も可)。ジョブが終われば捨てられる。盗まれても、すぐ無効になる。長期キーを Secrets に置く運用と比べて、攻撃可能な時間が桁違いに短い。これが OIDC 連携が好まれる最大の理由だ。


4. 信頼ポリシーの条件設計と、よくある落とし穴

仕組みが分かると、信頼ポリシーの Condition がいかに重要かが見えてくる。署名検証は「本物の GitHub トークンか」を保証するだけだ。「どのリポジトリ・どのブランチを信じるか」の線引きは、sub 条件が一手に担っている。

最も危険なのは、sub を緩く書きすぎることだ。たとえば次のように書くとどうなるか。

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:*"
}

これは「GitHub 上のどのリポジトリのワークフローでも、このロールを引き受けてよい」という意味になる。第三者が自分のリポジトリでワークフローを走らせれば、本物の GitHub 署名付きトークンを正規に取得できる。署名は通る。あとは緩い sub 条件をすり抜けるだけで、他人のロールを奪える。署名検証が堅牢でも、条件設計が緩ければ意味がない。

安全側に倒すなら、リポジトリとブランチ、あるいは環境まで絞る。

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}

aud も忘れてはいけない。aud=sts.amazonaws.com は「この身分証は AWS STS 宛てだ」という宛先の指定だ。これを条件に入れておくことで、別の宛先向けに発行されたトークンの流用を防ぐ。

STS 側の判定を流れで描くと、こうなる。

なぜ ARN やリポジトリ名がバレても破られないのか

ここで素朴な疑問が湧く。「ロール ARN(Amazon Resource Name、AWS リソースの一意な識別子)もリポジトリ名も、隠していない。これを知った第三者は、なりすませてしまうのでは?」

結論はノーだ。そして、その理由がこの仕組みの一番おもしろい急所である。ロール ARN もリポジトリ名も、そもそも秘密情報ではない。 信頼ポリシーやコードに普通に出てくるし、パブリックリポジトリなら名前は誰でも見える。守りはこれらの秘匿には一切依存していない

ロールを奪うには、STS に出す JWT が次を同時に満たす必要がある。

  1. GitHub の秘密鍵で署名されている(署名検証を通る)
  2. その subrepo:my-org/my-repo:*一致する(信頼ポリシーの条件を通る)

攻撃者は、この二つを同時には作れない。

肝は sub の出どころだ。subrepo:my-org/my-repo:... という値は、ワークフローが実際にどこで動いたかを GitHub が観測して埋める。攻撃者が好きに名乗れる自己申告欄ではない。

攻撃者が GitHub に正規のトークンを発行させること自体はできる。だが、それは自分が管理するリポジトリのワークフローからだけだ。すると GitHub が署名する sub は必ず repo:attacker-org/attacker-repo:... になり、あなたのリポジトリ名を騙れない。かといってトークンの sub 欄だけ書き換えれば、署名が壊れて検証で落ちる。署名が「sub の中身」と「本物であること」を不可分に縛っているのだ。だから ARN やリポジトリ名が露出していても、破られない。

これは、すぐ上で見た「sub: repo:* 事故」のちょうど裏返しでもある。条件を絞っていなければ、攻撃者が自分のリポジトリで正規に取得した本物の署名付きトークンが、そのまま条件を通ってしまう。署名検証が堅牢でも、最後の線引きは sub 条件が担っている。

落とし穴をまとめておく。

  • sub のワイルドカード事故repo:my-org/* のような広い指定は、組織内の任意リポジトリに門戸を開く。必要最小限に絞る。
  • aud の未指定:宛先チェックを省くと、トークン流用の余地が残る。
  • ブランチ・環境の無制限:本番デプロイ用ロールは environment:production などで絞り、PR からは触れないようにする。

長期アクセスキーの運用と比べてみる。長期キーは「一度漏れたら、気づいて無効化するまで使われ続ける」。OIDC の一時クレデンシャルは「数分〜1 時間で勝手に切れる」。さらに鍵を保管する場所そのものが存在しない。漏洩面(attack surface)が構造的に小さい。これが、新規プロジェクトで OIDC が標準解になった理由だ。


おわりに

GitHub Actions が鍵なしで AWS を触れる仕組みを、署名と検証のレイヤーまで降りて追ってきた。最後に二本柱を確認する。

  • 信頼ポリシーは入口の許可リストにすぎない。本人確認の実体は、GitHub が署名した OIDC ID トークンと、それを AWS STS が公開鍵で検証する工程にある。
  • SAML / OAuth2.0 / OIDC を一枚で整理すると、OIDC が「OAuth2.0 の上に本人確認を載せた軽量な層」だと分かる。その身分証と短期クレデンシャルの組み合わせが、長期キーを置き換えた。

突き合わせの正体は、パスワードの共有ではなかった。鍵を渡しているのではなく、素性を署名で証明させている。 次の一歩として、自分のリポジトリの trust policy を開き、subaud がどこまで絞られているかを読み返してほしい。長期キーがまだ Secrets に残っていれば、OIDC へ寄せる好機である。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?