GitHub Actions から AWS に入るのに、アクセスキーを配るのをやめた
GitHub Secrets に置いた鍵は、置きっぱなしになる
CI から AWS を触るとき、いちばん手っ取り早いのは IAM ユーザーを作ってアクセスキーを発行し、GitHub Secrets に登録することです。5分で終わります。
ただ、これをやると鍵が増えます。しかも、増えた鍵には次の性質があります。
- 失効しない。期限がないので、放っておくと何年でも有効なまま
- どこに配ったか追いにくい。Secrets に入れたのか、手元の
~/.aws/credentialsにも残っているのか、他の誰かに渡したのか - 漏れても気づけない。使われても正規の呼び出しと区別がつかない
3つ目が特に厄介です。パスワードと違って「ログイン通知」のようなものがないので、静かに使われても分かりません。
OIDC を使うと、この鍵そのものをなくせます。
OIDC にすると何が変わるか
仕組みはこうです。
GitHub Actions が実行される
↓ GitHub が「このリポジトリの、このブランチのワークフローです」という
署名付きのトークンを発行する
↓ そのトークンを AWS に渡す
↓ AWS が「確かに GitHub が発行したものだ」と検証し、
信頼ポリシーの条件と照合する
↓ 条件に合えば、短命の資格情報を返す
要点は、保管する鍵がなくなることです。実行のたびに一時的な資格情報が発行され、ジョブが終われば失効します。GitHub Secrets に AWS のキーを置く必要がありません。
漏れたときの被害も小さくなります。長期キーが漏れれば恒久的に使われますが、一時的な資格情報は時間が経てば無効になります。
設定は3つ
1. AWS 側に OIDC プロバイダを作る
「GitHub が発行したトークンを信じる」という宣言です。アカウントに1つあれば足ります。プロバイダの URL は token.actions.githubusercontent.com、対象者(audience)は sts.amazonaws.com です。
2. Assume されるロールを作る
CI が引き受けるロールを作り、信頼ポリシーでどのリポジトリからの Assume を許すかを書きます。ここが後述する要注意ポイントです。
3. ワークフローに2行足す
permissions:
id-token: write # OIDC トークンの発行に必要
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::<アカウントID>:role/<ロール名>
aws-region: ap-northeast-1
- name: Deploy
run: npm run deploy
permissions の id-token: write を忘れると、トークンが発行されず認証に失敗します。ここは最初にハマりやすいところです。
あとは以降のステップで、普通に AWS CLI や CDK を叩けます。キーの記述は一切ありません。
信頼ポリシーの絞り方が本題
ロールの信頼ポリシーは、だいたいこういう形になります。
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<アカウントID>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:<オーナー>/<リポジトリ>:ref:refs/heads/main"
}
}
}
sub(subject)が「誰からのトークンか」を表しています。ここをどこまで絞るかが、そのまま安全性になります。
| 書き方 | 何を許すか |
|---|---|
repo:オーナー/* | その組織の全リポジトリ・全ブランチ。危険 |
repo:オーナー/リポジトリ:* | そのリポジトリの全ブランチ・全イベント |
repo:オーナー/リポジトリ:ref:refs/heads/main | main ブランチからのみ |
repo:オーナー/リポジトリ:environment:production | 指定した Environment からのみ |
組織全体をワイルドカードで許すのが一番まずい形です。組織内のどれか1つのリポジトリが侵害されれば、そこから本番のAWSに手が届きます。テスト用に作った小さなリポジトリが踏み台になり得ます。
記事を書きながら、自分の設定が緩いことに気づいた
ここまで書いていて、自分の設定を確認したら sub がこうなっていました。
"repo:<オーナー>/<リポジトリ>:*"
末尾がワイルドカードです。そのリポジトリなら、どのブランチのどのワークフローからでも Assume できる状態でした。組織全体を許すよりはマシですが、適当なブランチを切ってワークフローを置けば通ってしまいます。
直しました。ワイルドカードをやめて、完全一致にします。
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": [
"repo:<オーナー>/<リポジトリ>:ref:refs/heads/main",
"repo:<オーナー>/<リポジトリ>:pull_request"
]
}
なぜ main だけでは足りないのか
最初は ref:refs/heads/main だけにしようとして、途中で気づきました。PR で動かしている CI が壊れます。
というのも、pull_request で実行されたときの sub は ref:refs/heads/... になりません。ブランチ名ではなく、こうなります。
repo:<オーナー>/<リポジトリ>:pull_request
PR のたびに cdk diff を出して差分を確認しているので、ここを許可しないと CI が AccessDenied で落ちます。
この挙動は、絞ってみて初めて分かる部類のものです。どのトリガーでどんな sub になるかを確認してから条件を書く、というのが実務上の注意点になります。
絞ったあとの確認
条件を変えたら、意図どおりに許可・拒否されるかを確かめておきます。
| ケース | 期待 |
|---|---|
| main への push | 許可 |
| main からの手動実行 | 許可 |
PR での cdk diff | 許可 |
| 適当なブランチからの実行 | 拒否 |
| タグからの実行 | 拒否 |
| 同一オーナーの別リポジトリ | 拒否 |
| 他人のリポジトリ | 拒否 |
「設定した」だけで通さず、拒否されるべきものが拒否されるかまで見ておくと安心です。
ロールの権限も絞る
信頼ポリシーで「誰が入れるか」を絞ったら、ロール自体の権限も最小にします。
今回の構成では、デプロイ用ロールに AWS リソースを直接触る権限を持たせていません。持たせているのは、CDK が作ったロールへ AssumeRole する権限だけです。
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<アカウントID>:role/cdk-*"
}
入口のロールを奪われても、そこから直接何かを壊せるわけではない、という形にしています。
あわせて、状態を持つスタック(DB や鍵)のデプロイは自動実行にせず、手動起動のワークフローに分けました。push のたびに走るのはアプリ側だけです。取り返しのつかない変更は、人が意図して叩いたときだけ動くようにしています。
注意しておく点
移してみて「ここは知っておかないと詰まる」と思った点をまとめます。OIDC そのものの欠点というより、運用でつまずく箇所です。
初期設定は長期キーより手間
アクセスキーなら5分ですが、OIDC はプロバイダとロールと信頼ポリシーが要ります。1回作れば以降は使い回せますが、最初の1回は確実に長期キーより時間がかかります。
sub の書式が変わることがある
GitHub は、2026年7月15日以降に作成されたリポジトリでは、所有者IDとリポジトリIDを含む形式を既定にするとしています。名前ベースで信頼ポリシーを書いていると、条件がマッチせず AccessDenied になる可能性があります。
リポジトリ名を変えたときも同じ問題が起きます。名前で縛っている以上、名前が変われば通らなくなります。
イベントの種類では絞りにくい
sub で区別できるのは主にブランチ・タグ・Environment・pull_request です。「push のときだけ」「特定のワークフローファイルからだけ」といった細かい制御は、sub だけでは表現しきれません。
セルフホストランナーだと読まれうる
GitHub のホステッドランナーではなく自前のマシンで走らせている場合、受け取ったトークンを環境変数経由で他のステップに渡していると、そのマシンを管理している人からトークンが見えます。OIDC にしたからといって、ランナー自体の安全性が保証されるわけではありません。
いちばん大きいリスクは、信頼ポリシーの外にある
これが本質的なところです。
信頼ポリシーをどれだけ厳密に書いても、リポジトリに書き込める人は、正規のワークフローに任意のコードを混ぜられます。攻撃する側から見れば、新しいブランチを作る必要も、条件をすり抜ける必要もありません。main のワークフローに1行足すだけで、正規の経路として実行されます。
つまり OIDC は「鍵の保管をなくす」ためのもので、「リポジトリへの書き込みを守る」ものではありません。ブランチ保護やレビュー必須の設定と組み合わせて、初めて意味を持ちます。
なお、Environment に承認者を設定して守るという手もありますが、プライベートリポジトリでこの保護ルールを使うには有料プランが必要です。無料の範囲では、ブランチ保護と sub の絞り込みで組み立てることになります。
移行しても、古い鍵は自動では消えない
最後に、移行のときに見落としやすい点を1つ。
OIDC に移しても、それまで使っていた IAM ユーザーとアクセスキーは残ったままです。 ワークフローから参照されなくなるだけで、キー自体は有効なので、放っておくと「誰も使っていないが有効な鍵」が残り続けます。
棚卸しは、アクセスキーの最終使用日で洗えます。長く使われていないものは、まず 無効化(Inactive) してから様子を見て、問題がなければ削除するのが安全です。無効化ならすぐ戻せるので、どこかで使われていた場合もエラーで気づけます。
まとめ
- OIDC の本質は「保管する鍵をなくす」こと。実行のたびに短命の資格情報が発行される
- 設定は3つ(プロバイダ・ロール・ワークフローの
id-token: write) - 信頼ポリシーの
subをどこまで絞るかが安全性そのもの。ワイルドカードは避ける - PR 実行時の
subはpull_requestになる。ブランチ名では絞れない - 入口のロールには直接の権限を持たせず、必要なロールへの AssumeRole だけ渡す
- OIDC は鍵の保管をなくすもので、リポジトリへの書き込みを守るものではない。ブランチ保護と組み合わせる
- 移行後、古い長期キーは自分で消す
書きながら自分の設定を見直したら、ワイルドカードのまま運用していたことに気づきました。設定した当時は「動いた」で終わっていたのだと思います。仕組みを説明しようとすると、自分の穴が見えるものですね。