GitHub Actions OIDCで実現する鍵レス本番デプロイ:漏えい事故を減らす実装プレイブック

CI/CD の事故は、ビルドが失敗することより「漏えいしても気づけない鍵」が残り続けることのほうが深刻です。特に AWS_ACCESS_KEY_ID のような長期シークレットを GitHub Secrets に保存し続ける運用は、便利ですがリスクが高いです。

本記事では、GitHub Actions の OIDC(OpenID Connect)連携を使って、長期鍵を使わずに AWS へデプロイする実践手順をまとめます。単なる設定紹介ではなく、最小権限・ブランチ制限・監査ログ設計まで含めて、明日から本番投入できる形で説明します。

1. まず何が危険なのか:長期シークレット運用の限界

従来構成では、次のような問題が起きます。

  • Secret が漏れても検知が遅い(CIログ、誤コミット、権限の広いメンバー)
  • ローテーションが後回しになる
  • 1つの鍵で複数環境へアクセスできてしまう
  • 「誰のどの workflow 実行が何をしたか」が追いにくい

OIDC 連携では、GitHub が発行する短命トークンを信頼し、AWS 側で一時認証情報を払い出します。つまり、保管する鍵そのものを減らすのが最大の価値です。

2. 全体アーキテクチャ

基本フローは以下です。

  1. GitHub Actions ジョブが OIDC トークンを取得
  2. AWS IAM の OIDC プロバイダとロール信頼ポリシーで検証
  3. 条件に一致したジョブだけ AssumeRoleWithWebIdentity
  4. 一時クレデンシャルで S3/CloudFront/ECR/ECS へデプロイ

ポイントは「GitHub 側の workflow 制御」だけでなく、AWS 側で repo・branch・workflow を強制することです。

3. AWS 側の初期設定(OIDC Provider + IAM Role)

3.1 OIDC Provider を作成

CLI 例(すでに存在する場合はスキップ):

1
2
3
4
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

3.2 信頼ポリシーを厳密化する

以下のように subaud を必ず絞ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>: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:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

subrepo:org/repo:* のように広く取りすぎると、意図しない workflow からも引き受ける可能性があり危険です。

3.3 デプロイ権限ポリシーを分離する

「ロール1個に全部盛り」は避けます。

  • deploy-web-prod-role: S3同期 + CloudFront invalidation
  • deploy-api-prod-role: ECR push + ECS update
  • read-only-audit-role: CloudWatch Logs / Describe 系のみ

環境別(dev/stg/prod)にロールを分離すると、誤デプロイ時の被害半径が大きく減ります。

4. GitHub Actions workflow 実装

最小サンプル:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: deploy-web
on:
  push:
    branches: ["main"]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/deploy-web-prod-role
          aws-region: ap-northeast-1

      - name: Build
        run: |
          npm ci
          npm run build

      - name: Upload to S3
        run: aws s3 sync ./dist s3://example-prod-web --delete

      - name: Invalidate CloudFront
        run: aws cloudfront create-invalidation --distribution-id E123456 --paths "/*"

重要なのは permissions.id-token: write を明示する点です。これがないと OIDC トークンを取得できません。

5. 事故を防ぐための実務ルール

5.1 branch protection と environment protection を組み合わせる

  • main 直push禁止
  • 必ず PR + 1 approval
  • production environment には Required reviewers を設定
  • 夜間デプロイを禁止したい場合は手動承認ステップを入れる

5.2 workflow ファイル改変の監査

.github/workflows/*.yml の変更は CODEOWNERS で必ずレビュアー固定にします。

1
.github/workflows/*  @platform-team

5.3 self-hosted runner の扱い

OIDC を導入しても、runner 自体が侵害されると意味が薄れます。

  • runner をプロジェクト共有にしない(専用化)
  • ジョブ後にワークディレクトリをクリーン
  • egress 制限をかける
  • runner グループを環境ごとに分離

6. トラブルシューティング

症状1: Not authorized to perform sts:AssumeRoleWithWebIdentity

確認ポイント:

  1. audsts.amazonaws.com になっているか
  2. sub が実際の実行 ref と一致しているか(タグ実行でハマりやすい)
  3. permissions.id-token: write が設定されているか
  4. Role ARN の typo がないか

症状2: main 以外で偶発的にデプロイされた

  • workflow の on.push.branches 見直し
  • IAM 信頼ポリシー sub を main 固定へ
  • 環境保護ルール(Required reviewers)追加

症状3: デプロイは通るが操作が一部失敗

これは IAM 権限不足の可能性が高いです。CloudTrail の eventNameerrorCode を見て、必要最小限のアクションだけ追加します。闇雲に * を付けないこと。

7. 段階的な移行計画(既存運用からの切替)

実務では一気に切り替えず、以下の順が安全です。

  1. OIDC ロールを作成(既存シークレットは残す)
  2. staging workflow だけ OIDC に切替
  3. 1週間監視(失敗率・デプロイ時間・CloudTrail)
  4. production を OIDC 化
  5. 最後に長期シークレットを削除

削除前に「どの workflow がどの secret を参照しているか」を grep で確認しておくと事故が減ります。

1
git grep -n "AWS_ACCESS_KEY_ID\|AWS_SECRET_ACCESS_KEY" .github/workflows

8. 監査ログ設計:何を見れば安全性が上がるか

最低限、次をダッシュボード化すると運用しやすいです。

  • AssumeRoleWithWebIdentity 実行回数(日次)
  • 失敗イベント数(権限エラー/条件不一致)
  • production deploy 実行者(workflow + sha + actor)
  • 1回のデプロイで変更された主要リソース数

CloudTrail + CloudWatch Logs Insights での簡易クエリ例:

1
2
3
4
5
fields @timestamp, userIdentity.sessionContext.sessionIssuer.userName, eventName, errorCode
| filter eventSource = "sts.amazonaws.com"
| filter eventName = "AssumeRoleWithWebIdentity"
| sort @timestamp desc
| limit 50

まとめ

OIDC は「設定が新しいから導入する」ものではなく、長期鍵を削減して事故確率を下げるための運用設計です。導入時にやるべきことはシンプルで、次の3つに集約できます。

  1. IAM 信頼ポリシーを repo/branch 単位で厳密化する
  2. workflow 側で id-token 権限と環境保護を設定する
  3. CloudTrail で AssumeRole の監査を継続する

ここまで実施すれば、CI/CD のセキュリティは「頑張って守る」状態から、「漏えいしにくい仕組みで守る」状態へ進化します。まずは staging 1本から置き換えるのがおすすめです。