更新日: 2026-07-24
GitHub ActionsからAWSへdeployするために、AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYをGitHub Secretsへ保存している。
動いているし、Secretsなら画面にも出にくい。せやから、そのまま何年も置いてしまう。
でも正直に言いましょう。これは、期限のない合鍵をCIへ預けている状態なんです。漏えいしなければ問題は起きません。ただ、いつ誰が作り、どのworkflowが使い、まだ必要なのか分からなくなるほど、未来の自分が背負う確認コストは増えていきます。
結論はシンプルです。GitHub Actionsでは長期AWSキーを保存せず、OIDCでworkflow実行時だけ短期資格情報を受け取る形へ移します。
この記事では、安全に移行する順番を5ステップで説明します。
- 長期secretと使用箇所を棚卸しする
- AWSにGitHubのOIDC providerを登録する
- trust policyをrepo+branchまたはenvironmentへ絞る
- IAM roleの許可操作を最小化する
- caller identityだけ確認してから旧secretを無効化する
僕はこの3つの囲い方を 「短期認証境界」 と呼びます。CIが実行時だけ取得する短期資格情報を、repo、branchまたはenvironment、許可操作の3条件で囲う設計です。
OIDCを入れたら全部安全になる、という話ではありません。長期キーをなくすことと、強すぎる権限をなくすことは別です。そこも一緒に線を引きます。
最初に4つの用語だけ
OIDC(OpenID Connect) は、「誰が実行しているか」を別サービスへ証明するための仕組みです。GitHub Actionsは実行中のjobについてトークンを発行し、AWSはその情報を検証して一時資格情報を返せます。
JWTは、その証明情報を運ぶ署名付きの入れ物です。中には発行者、利用先、実行元などの情報が入ります。
claimは、JWTの中にある項目です。この記事で重要なのは、利用先を示すaudと、どのrepo・branch・environmentから来たかを表すsubです。
IAM roleは、AWSで「何をしてよいか」をまとめた役割です。長期キーを持つIAM userをCI専用に作る代わりに、GitHub Actionsが条件を満たす時だけroleを引き受けます。
たとえるなら、長期access keyは合鍵。OIDCは受付で毎回発行する短期の入館証。IAM roleは、その入館証で入れる部屋の範囲です。
入館証の期限が短くても、全フロアへ入れたら危ない。ここが大事です。
人間、AI、決定的ツールの役割を分ける
| 担当 | 任せること | 任せないこと |
|---|---|---|
| 人間 | 信頼するrepo・branch・environment、AWSで許可する操作、旧キー削除の承認 | JSON/YAMLの単純な草案 |
| AI | secret参照の棚卸し、policy草案、差分説明、危険候補の指摘 | 本番role作成、権限拡大、旧キーの無効化 |
| GitHub/AWS | JWT発行、claim検証、一時資格情報発行、監査ログ | その権限が業務上妥当かの判断 |
AIはpolicyを速く書けます。でも「このworkflowは本番S3の削除まで必要か」は決められません。必要な操作と許容できる損失を知っている人間が決めます。
5ステップの全体像
移行で一番避けたいのは、先にsecretを消してdeployを止め、慌てて権限を広げることです。
順番は次です。
棚卸し
↓
OIDC provider
↓
trust policy
↓
permission policy
↓
短期認証の確認
↓
旧secret無効化
新しい経路を狭く作り、読み取りだけで確認し、最後に古い合鍵を回収します。
ステップ1: 長期secretと使用箇所を棚卸しする
まず消しません。使っている場所を探します。
grep -RInE \
'AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|aws-access-key-id|aws-secret-access-key' \
.github/workflows .github/actions 2>/dev/null
ここで値そのものを表示する必要はありません。知りたいのは「どのworkflowが、どのsecret名を参照しているか」です。
一覧は次の形で十分です。
workflow: .github/workflows/deploy-staging.yml
trigger: push to staging
current_secret_refs: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
required_aws_actions: sts:GetCallerIdentity, s3:ListBucket
owner: platform-team
必要なAWS操作が分からない場合、いきなりAdministratorAccessで通さないでください。既存ログとコマンドから候補を出し、検証環境で不足分だけ追加します。
プロンプト1: 長期secretの参照を棚卸しする
.github/workflows配下の差分を読み、
長期クラウド資格情報を参照している箇所を一覧化してください。
出力:
- workflowファイル
- trigger
- secret名(値は絶対に出さない)
- 実行しているAWSコマンド
- 必要そうなIAM Actionの候補
- 人間が確認すべき不明点
ファイル編集、secret取得、AWS操作は実行しないでください。
IAM Actionは候補であり、確定と書かないでください。
ステップ2: AWSにGitHubのOIDC providerを登録する
AWS側は、GitHubのOIDC issuerを信頼できるidentity providerとして登録します。
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
このコマンドはAWSアカウントの設定を変更します。すでにproviderがあるか確認し、権限を持つ担当者が実行してください。記事を読みながら本番でそのまま打つコマンドではありません。
公式のaws-actions/configure-aws-credentialsは、通常のAWSパーティションでsts.amazonaws.comを既定audienceとして使います。中国リージョンなど別パーティションでは条件が変わるため、公式READMEを確認します。
ステップ3: trust policyをrepo+environmentへ絞る
OIDC providerを登録しただけでは、誰を信頼するかが広すぎます。IAM roleのtrust policyでaudとsubを制約します。
次はダミーのorganization、repo、AWSアカウントIDを使った例です。
{
"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",
"token.actions.githubusercontent.com:sub": "repo:example-org/example-app:environment:staging"
}
}
}
]
}
これで、少なくとも次の2条件を求めます。
- audienceがAWS STSである
-
example-org/example-appのstagingenvironmentから来た実行である
repo:example-org/*のようにorganization全体へ広げると、新しいrepoや侵害された別repoから同じroleを使える範囲が増えます。最初は一つのrepo、一つのenvironmentから始める方が確認しやすいです。
GitHub environmentを使うworkflowでは、subの形がbranch参照ではなくenvironment名を含む形になります。さらにGitHub公式OIDC referenceでは、2026年7月15日以降に作成されたrepo、またはopt-inしたrepoについてimmutable subject形式を案内しています。自分のrepoが出すclaim形式を公式referenceで確認し、想像でpolicyを書かないでください。
プロンプト2: trust policyをレビューする
次のAWS IAM trust policyを防御側としてレビューしてください。
確認観点:
- audが期待値へ固定されているか
- subがorganization全体ではなくrepo単位か
- branchまたはenvironmentが固定されているか
- wildcardが信頼範囲を広げていないか
- 実際のGitHub OIDC claim形式と照合が必要な箇所
出力は「事実」「危険候補」「人間が確認する点」「最小修正案」。
policyの適用やAWS操作は実行しないでください。
ステップ4: IAM roleの許可操作を最小化する
trust policyは「誰がroleを使えるか」を決めます。permission policyは「roleが何をできるか」を決めます。
この2つは別物です。
最初の疎通確認なら、deploy権限は要りません。sts:GetCallerIdentityで、どのidentityとして認証されたかを確認するだけにします。AWSのGetCallerIdentityは呼び出し元identityの情報を返します。
実際のdeploy roleを作る時は、workflowが使うresourceとactionを個別に絞ります。たとえばS3へ成果物を置くとしても、全bucketの全操作ではなく、対象bucketと必要な操作だけを候補にします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::example-deploy-bucket/staging/*"
]
}
]
}
これは構成例です。実際に必要なactionはdeploy方式で変わります。s3:*やResource: "*"へ広げて通すのではなく、失敗ログを確認して必要性を一つずつ判断します。
ステップ5: caller identityだけ確認してから旧secretを無効化する
GitHub Actions側ではjobへid-token: writeを許可します。GitHub公式によると、この設定はOIDC JWTの取得を可能にするもので、他のresourceへのwrite権限を直接与えるものではありません。
name: oidc-smoke-test
on:
workflow_dispatch:
permissions:
contents: read
jobs:
caller-identity:
runs-on: ubuntu-latest
environment: staging
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
with:
role-to-assume: arn:aws:iam::123456789012:role/github-oidc-staging
aws-region: ap-northeast-1
allowed-account-ids: "123456789012"
- name: Verify caller only
run: aws sts get-caller-identity
第三者actionは移動しない完全なcommit SHAへ固定し、コメントで確認したversionを残しています。allowed-account-idsは、想定外のAWSアカウントから資格情報を受け取った場合に失敗させるための追加確認です。
このworkflow例はYAMLとして構文確認しますが、実AWS環境では動作未確認です。role ARN、subject、environment保護ルールが環境ごとに違うためです。
疎通確認後も、すぐ旧secretを削除しません。
- OIDC経路で
GetCallerIdentityが成功 - 非本番の最小操作が成功
- CloudTrailなどの監査ログで想定roleを確認
- rollback手順を確認
- 旧access keyを無効化
- 一定期間問題がなければ削除
無効化を先に挟むと、見落としたworkflowがあっても戻せます。いきなり削除するより回収可能です。
10分でできる静的安全チェック
AWSへ接続する前に、trust policyとworkflowの危険候補をローカルで止めます。
import json
import pathlib
import re
import sys
def walk_values(value):
if isinstance(value, dict):
for key, child in value.items():
yield key, child
yield from walk_values(child)
elif isinstance(value, list):
for child in value:
yield from walk_values(child)
policy_path = pathlib.Path(sys.argv[1])
workflow_path = pathlib.Path(sys.argv[2])
policy = json.loads(policy_path.read_text())
workflow = workflow_path.read_text()
errors = []
pairs = list(walk_values(policy))
aud_values = [v for k, v in pairs if k == "token.actions.githubusercontent.com:aud"]
sub_values = [v for k, v in pairs if k == "token.actions.githubusercontent.com:sub"]
if aud_values != ["sts.amazonaws.com"]:
errors.append("audをsts.amazonaws.comへ1件だけ固定してください")
if len(sub_values) != 1 or not isinstance(sub_values[0], str):
errors.append("sub条件を1件だけ文字列で指定してください")
elif "*" in sub_values[0]:
errors.append("subにwildcardを使わないでください")
if re.search(r"AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)", workflow):
errors.append("workflowが長期AWS secretを参照しています")
if "id-token: write" not in workflow:
errors.append("OIDC token取得権限がありません")
unpinned = []
for action_ref in re.findall(r"^\s*uses:\s*([^#\s]+)", workflow, re.MULTILINE):
if action_ref.startswith("./") or action_ref.startswith("docker://"):
continue
revision = action_ref.rsplit("@", 1)[-1]
if not re.fullmatch(r"[0-9a-f]{40}", revision):
unpinned.append(action_ref)
if unpinned:
errors.append(f"完全なcommit SHAへ未固定: {', '.join(unpinned)}")
if errors:
print("\n".join(f"STOP: {item}" for item in errors))
raise SystemExit(1)
print("OK: minimum static checks passed")
この検査コードはローカルで、正常例は終了コード0、subへwildcardを入れた異常例は終了コード1になることを確認しました。ただし、万能なpolicy analyzerではありません。ダミーrepo名を自分の許可repoへ置き換えた上で、チームのルールに合わせて検査します。
AIにpolicyの意味を判定させる前に、audがない、subがない、長期secretが残っている、という決定的な条件をコードで止める。文章生成が得意なAIと、同じ入力なら同じ判定を返す検査を分ける感じです。
プロンプト3: 移行差分を公開前レビューする
OIDC移行のPR差分をレビューしてください。
必須確認:
1. 長期AWS secret参照が残っていないか
2. id-token: writeが必要なjobだけにあるか
3. trust policyのaud/subが狭いか
4. IAM roleのAction/Resourceが過大でないか
5. third-party actionが完全なcommit SHAへ固定されているか
6. triggerがpull_request_target等の危険な文脈を含まないか
7. 旧access keyの無効化にrollback手順があるか
各指摘に根拠となるファイルと行を付け、
確定できないものは「人間確認」と明記してください。
権限変更、secret削除、workflow実行はしないでください。
よくある失敗5つ
1. subを広くしすぎる
OIDC providerを登録しただけ、またはorganization全体のwildcardで許可すると、信頼範囲が意図より広がります。repoとbranchまたはenvironmentまで狭めます。
2. roleへ管理者権限を付ける
短期資格情報でも、実行中に奪われた時の影響はrole権限ぶん残ります。認証の寿命と認可の広さを別々にレビューします。
3. すべてのjobへid-token: writeを付ける
OIDCが必要なdeploy jobだけへjob-levelで付けます。lintやunit testまでトークン取得可能にする理由はありません。
4. actionをversion tagだけで参照する
tagは人間には読みやすい一方、参照先が動く余地があります。完全なcommit SHAへ固定し、更新はレビュー付きの依存更新で行います。
5. OIDC成功前に旧secretを消す
移行は認証方式の入れ替えです。新経路をcaller identity、非本番最小操作、監査ログの順で確認し、旧キーは無効化を経て削除します。
この方法が効かない条件
OIDCは、長期AWSキーをGitHubへ保存しないための強い選択肢です。でも次の問題は単独では解決しません。
- IAM roleのpermissionが広すぎる
- 信頼するrepoやbranchが広すぎる
- workflow triggerが危険
- 実行する第三者actionが侵害される
- deploy内容そのものが間違っている
つまり、OIDCは「合鍵を置かない」仕組みであって、「入館後に何をしても安全」にする仕組みではありません。
小さな個人検証でAWSへ接続しないworkflowなら、そもそもOIDCは不要です。逆に複数クラウド、複数account、複数environmentを一気に移行すると、subjectとroleの組み合わせが増えて確認しづらくなります。まず非本番の一つのworkflowから始めるのが回避策です。
今日10分でやること
今日は本番deployを変えなくて大丈夫です。
-
.github/workflowsで長期AWS secret参照を検索する - 対象workflowを一つ選ぶ
- 信頼するrepoとbranchまたはenvironmentを1行で書く
- 許可がある検証環境だけで、
aws sts get-caller-identityまでの移行PRを作る
「どのCIが、どの合鍵を持っているか分かった」だけでも最初の成功です。そこから短期認証へ移し、最後に古い合鍵を回収する。
未来の自分が、出どころの分からないaccess keyを見つけて青ざめなくて済む。こういう小さな回収可能性が、明日の自分への「あざっす」になる気がします。
参考リンク(一次情報・2026-07-24確認)
- GitHub Docs - Configuring OpenID Connect in Amazon Web Services
- GitHub Docs - OpenID Connect reference
- AWS IAM - OIDC federation
- AWS IAM - Create a role for OIDC federation
- aws-actions/configure-aws-credentials
生成AI活用エンジニア&3児のパパ。AI×開発の実践知を毎日発信しています → X