はじめに
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_URL と ACTIONS_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 の iss(https://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 の aud と sub を、ポリシーの 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 が次を同時に満たす必要がある。
- GitHub の秘密鍵で署名されている(署名検証を通る)
- その
subがrepo:my-org/my-repo:*に一致する(信頼ポリシーの条件を通る)
攻撃者は、この二つを同時には作れない。
肝は sub の出どころだ。sub の repo: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 を開き、sub と aud がどこまで絞られているかを読み返してほしい。長期キーがまだ Secrets に残っていれば、OIDC へ寄せる好機である。