2
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?

PAT と GITHUB_TOKEN は何が違うのか — Actions が 5 日間死んで調べ直した認証設計

2
Last updated at Posted at 2026-08-25

こんにちは、自分のワークフローが 5 日間死んでいたことに気づかなかったアーキテクトのやまぱん!です 😅

補足コメントや質問、いいね、拡散、ぜひお願いします 🥺!
間違っていたら 優しく 教えてください!

TL;DR

  • PAT(Personal Access Token)は パスワードの代替として生まれた「人の資格情報」GITHUB_TOKEN の実体は GitHub App のインストールアクセストークン です
  • 2 つが併存しているのは設計の出自が違うからです。GitHub Docs は「長期的なインテグレーションには GitHub App を使え」「Actions で PAT を使う前に GITHUB_TOKEN を検討しろ」と明示的に誘導しています
  • 「保管する秘密を減らす」流れは GitHub 固有ではありません。Microsoft Entra のワークロード ID フェデレーションも Azure Pipelines も、同じ問題意識から動いています
  • Actions で PAT を使っているなら、permissions: に 1 行足すだけで GITHUB_TOKEN へ移行できるケースがあります。私のリポジトリはそれでシークレットが 0 件になりました

PAT と GITHUB_TOKEN の比較図。PAT は人が手作業で作る 30 日で失効する資格情報、GITHUB_TOKEN はジョブ開始時に自動発行され終了時に失効する一時トークン

先に結論: 何がどう違うのか

GITHUB_TOKEN PAT
正体 GitHub App のインストールトークン 人間のユーザーの資格情報
発行 ジョブ開始時に GitHub が自動生成 人が手作業で作成
寿命 ジョブ終了で失効(最長 6 時間) 30 日 / 1 年 / 無期限
範囲 そのワークフローのあるリポジトリのみ classic は自分が触れる全範囲 / fine-grained は指定範囲
権限 permissions: で宣言的に指定 作成時に固定。コードから見えない
操作の帰属 github-actions[bot] その人本人
人が辞めたら 影響なし 無効になる
漏洩時 ジョブ終了で失効、1 リポジトリ限定 失効まで有効。classic は他リポにも波及

例えるなら、PAT は「人の合鍵を金庫に預けて使い回す」やり方です。GITHUB_TOKEN は「入館のたびに発行され、退館時に回収される、行ける階が明記された一時パス」ですね。

以下、この表の各行がどこから来ているのかを見ていきます。

この記事の実行日時は UTC 表記 です。対象のスケジュールは毎日 23:00 UTC(= 翌朝 08:00 JST)に動きます。

検証環境: Windows 11 / PowerShell 7 / gh v2 系(2026-08 時点)

きっかけ: 毎朝の Actions が 5 日間死んでいた

報告は「なんか最近更新されてない」だけでした。対象は Daily Dev Byte という、毎朝 Copilot CLI が IT 小ネタを 1 本書いて GitHub Pages へ流す個人リポジトリです。

git log を見ると、bot による docs/archive.json の更新が 5 日前で止まっていました。このファイルはパイプラインのいちばん下流なので、docs/ を掘っても何も出てきません。

パイプライン図。上流の Daily Dev Byte Publisher が 401 で failure になり、中間のアーカイブ公開が skipped になり、下流の GitHub Pages で「更新されない」という症状として見える

gh run list を叩くと、本体が 3 日連続 failure でした。更新が止まって 5 日なのに失敗が 3 回なのは、失敗が始まる前の 2 日ぶんはアーカイブ側に差分が出ていなかったからです。

落ちていたのは Copilot CLI を起動するステップで、3 回とも一字一句同じエラーです。

[copilot-harness] awf-reflect: models fetch returned 401 for http://api-proxy:10002/models
[copilot-harness] copilot-sdk driver mode: BYOK provider is required but could not be resolved — aborting

3 回とも完全に同一というのが手がかりでした。ネットワークの不調やタイミング依存なら、ここまで揃いません。

物証を並べます。

  • 外向き通信のプロキシログでは GitHub 宛のトンネル確立に成功。ネットワークとファイアウォールの線は薄い
  • コンテナは sha256: でピン留めされていて、実行環境は成功時と同一バイナリ
  • 最終成功から初回失敗までのコミットは bot のものだけ。こちら側は何も変えていない
  • gh secret list を見ると、Copilot 用シークレットの更新日が 初回失敗のちょうど 30 日前

401 / 403 / 429 の切り分け図。401 は資格情報そのものが無効、403 は資格情報は有効だが権限や枠が足りない、429(や 403)はレート制限

401 は「お前が誰か分からない」であって、403 の「権限や枠が足りない」とは別物です。401 は認証、403 は認可。 レート制限の 429(一部は 403 で返ります)はさらに別レイヤーで、gh-aw のログでは専用ラベルが立ちますが、今回は立っていませんでした。

確認できた範囲では外部要因も自分の変更も見当たらず、拒否されたのは認証層だけ。そこに 30 日の一致です。PAT の期限切れが濃厚でした。

そもそも PAT とは何なのか

GitHub Docs の Managing your personal access tokens はこう始まります。

Personal access tokens are an alternative to using passwords for authentication to GitHub when using the GitHub API or the command line.

(PAT は、GitHub API やコマンドラインを使うときに、パスワードの代わりに認証へ使うものです)

同じページには「アクセストークンはパスワードと同じように扱ってください」という警告も付いています。

用途についても書かれています。

Personal access tokens are intended to access GitHub resources on behalf of yourself. To access resources on behalf of an organization, or for long-lived integrations, you should use a GitHub App.

(PAT は 自分の代わりに GitHub のリソースへアクセスするためのものです。組織の代わりにアクセスする場合や、長期的なインテグレーション では GitHub App を使うべきです)

PAT は 人が手でやることの延長 として作られています。CI から毎晩自動で叩く用途は、想定の中心にありません。

classic と fine-grained

PAT には 2 種類あります。

classic fine-grained
権限の単位 スコープ(repo など粗い) リポジトリ単位 + 細かい権限
アクセス範囲 自分が触れる全リポジトリ 指定した 1 ユーザー / 1 組織のみ
組織側の統制 ポリシーで制限のみ 承認を必須にできる
作成上限 明示なし 50 個

classic は、付与したスコープとユーザー自身のアクセス範囲が重なります。Docs も「自分がアクセスできる組織の全リポジトリと、個人アカウントの全リポジトリへのアクセスを与えることになる」と書いています。

fine-grained はこれを大きく改善しましたが、制限も残っています。自分がメンバーでないパブリックリポジトリへのコントリビュート、複数組織への同時アクセス、Packages へのアクセスなどは、まだ classic でないとできません。Docs にはこれらが恒久的な制限ではなく、解消に取り組んでいる途中だと書かれています。

PAT が持つ、構造的な弱点

fine-grained にしても消えない性質が 2 つあります。

1 つは、生成したユーザーに紐づくこと。

Both fine-grained personal access tokens and personal access tokens (classic) are tied to the user who generated them and will become inactive if the user loses access to the resource.

(どちらの PAT も生成したユーザーに紐づいていて、そのユーザーがリソースへのアクセスを失うと無効になります)

PAT で回している自動化は、作った人が異動や退職でアクセスを失った瞬間に止まります。今回の私は「期限切れ」で済みましたが、組織で同じことをやっていたら、もっと厄介な形で表面化していたはずです。

もう 1 つは、作成上限があること。 fine-grained PAT は 1 アカウントで 50 個までです。Docs は上限の説明のあとに、「もっとトークンが必要な場合や、自動化を作っている場合 は、スケーラビリティと管理性のために GitHub App の利用を検討してください」と続けています。

そもそも GITHUB_TOKEN とは何なのか

When you enable GitHub Actions, GitHub installs a GitHub App on your repository. The GITHUB_TOKEN secret is a GitHub App installation access token.
GITHUB_TOKEN

(GitHub Actions を有効にすると、GitHub はリポジトリに GitHub App をインストールします。GITHUB_TOKEN はその GitHub App のインストールアクセストークン です)

GitHub App の認証は 3 種類ある

GitHub App は、リポジトリや Organization にインストールして使う統合の仕組みです。PAT と違って、アプリ自身の身元を持てます。Docs は認証を 3 つに分けています。

  • アプリ自身として: JWT を使う。インストールアクセストークンの生成やアプリ管理に使う
  • アプリのインストールとして: インストールアクセストークンを使う。活動はアプリに帰属する。GITHUB_TOKEN はこれ
  • ユーザーの代理として: ユーザーアクセストークンを使う。活動はそのユーザーに帰属する

3 つ目は PAT と役割が似ていますが別物です。PAT は本人が設定画面で作るのに対して、ユーザーアクセストークンはアプリがユーザーの認可を得て発行し、権限は「ユーザーが持つ権限」と「アプリに与えた権限」の両方で絞られます。期限付きが既定で、リフレッシュトークンも付きます。

Docs は、インストールとして認証する方式についてこう書いています。

Authenticating as an app installation is ideal for automation workflows that don't involve user input.

(アプリのインストールとして認証するのは、ユーザー入力を伴わない自動化ワークフロー に理想的です)

CI はまさにこれです。誰も見ていない時間に、誰の代理でもなく走る。PAT の説明が「自分の代わりに」だったのと、ちょうど裏返しになっています。

ライフサイクルと permissions

  • ジョブが始まる前に、GitHub がそのジョブ用のインストールアクセストークンを取得する
  • ジョブが終わると失効する。GitHub ホストランナーの最大実行時間が 6 時間なので、寿命も最長 6 時間
  • セルフホストランナーは最長 5 日実行できますが、トークンは 24 時間までしかリフレッシュできません。24 時間を超えるジョブでは PAT などが必要になります

権限の与え方も違います。PAT は作成時に権限が固定され、その中身は GitHub の設定画面にしかありません。GITHUB_TOKEN はワークフローファイルの中で宣言します。

permissions:
  issues: read
  copilot-requests: write

必要な権限が permissions: としてレビュー対象に残るので、何ができるトークンで動いているかを差分で追えます。

permissions: を書いた時点で、列挙していないスコープはすべて none になります。上の例をそのままコピーすると actions/checkout で転ぶので、必要な contents: read などを足してください。

なぜ 2 つあるのか

歴史的な経緯は公式には整理されていないので、ここからは Docs の記述から読み取れる範囲での私の解釈です。

PAT が生まれた場所は 人の作業 です。コマンドラインから git push する、API を手で叩く。そのときにパスワードを直接使わせないための仕組みでした。だから「自分の代わりに」という言い方になりますし、「パスワードのように扱え」という警告も付きます。

一方 Actions が必要としていたのは、誰の代理でもなく、そのリポジトリのそのジョブのためだけに存在して、終わったら消えるものです。PAT を CI へ置くと、次の 3 つを運用側で抱えることになります。

  • 寿命: 期限前の通知メールは私にも届いていたはずですが、通知の海に沈んでいました。人の受信箱を監視の一部にすると、こうなります
  • 範囲: classic を使うと、1 つのワークフローのために、その人が触れる全リポジトリへの鍵を置くことになります
  • 主体: ログ上の実行者がその人本人になるので、機械がやったのか人がやったのか区別できません

GitHub は fine-grained PAT で「範囲」を大きく改善しました。それでも「寿命」と「人に紐づく」性質は残ります。だから Docs は、自動化については一貫して GitHub App と GITHUB_TOKEN へ誘導しているのだと思います。

When using a personal access token in a GitHub Actions workflow, consider whether you can use the built-in GITHUB_TOKEN instead.
Managing your personal access tokens

(GitHub Actions のワークフローで PAT を使うときは、組み込みの GITHUB_TOKEN で代替できないか検討してください

私はこの 1 文を、今回の障害まで読んだことがありませんでした 😅

ただし GITHUB_TOKEN は万能ではない

次のケースでは PAT か GitHub App が必要です。

  • 他のリポジトリを触るGITHUB_TOKEN は 1 リポジトリに閉じています
  • ワークフローを連鎖させたいGITHUB_TOKEN による push などは、再帰暴走を防ぐため新しいワークフロー実行を作りません
  • 同じ理由で、GITHUB_TOKEN の push は GitHub Pages のビルドをトリガーしません(ブランチ公開方式の場合。Actions から Pages をデプロイする構成は該当しません)
  • 24 時間を超えるセルフホストランナーのジョブ。トークンのリフレッシュ上限に当たります

例外として、workflow_dispatchrepository_dispatch は常にワークフロー実行を作ります。pull_requestopened / synchronize / reopened は承認待ち状態で実行が作られます(GITHUB_TOKEN)。

今回のリポジトリは workflow_run で 2 つのワークフローを繋いでいます。workflow_run はトークンが起こした git イベントではなくワークフロー実行の完了を見るトリガーなので、上の制約とは別扱いになると理解しています。Docs の例外リストには明示されていませんが、実際に連鎖は動いています。なお workflow_run 自体には別の制約があり、ワークフローファイルがデフォルトブランチにある場合だけ発火し、3 階層までしか連鎖できませんEvents that trigger workflows)。

迷った点: 個人リポでも GITHUB_TOKEN でいいのか

移行の直前でいちばん迷ったのがここでした。先に答えを書くと、個人リポでも GITHUB_TOKEN 経路は成立します。私のリポジトリは個人所有で組織にも属していませんが、切り替えたら普通に動きました。動くかどうかで差は付かず、変わるのは運用のほうです。PAT を選べば、有効期限を監視して更新し続ける仕事が付いてきます。

迷ったのは、gh-aw のドキュメントが PAT を使う場面の例として「個人リポジトリ」を名指ししているからです(Authentication | GitHub Agentic Workflows)。ここだけ読むと、個人リポは GITHUB_TOKEN 方式の対象外に見えます。

ところが GitHub Docs 本体には、個人リポの課金先が普通に書かれていました。

In a personally-owned repository, usage is billed to the repository owner's Copilot seat.
About using Copilot CLI in GitHub Actions

個人所有リポジトリ では、利用分はリポジトリ所有者の Copilot シートに課金されます)

課金先が定義されているということは、経路として想定されているということです。

これは 2026-08 時点で私の個人リポジトリが動いたという話です。組織のポリシーや Copilot のプラン次第では挙動が変わる可能性があります。移行するときは自分の環境で 1 回手動実行して確かめてください。

実際にやった移行

やったことは、frontmatter に 1 行足しただけでした。

permissions:
  issues: read
  copilot-requests: write # ← これ

copilot-requests: write は、Copilot CLI の推論を GITHUB_TOKEN で認証するための権限です。2026-07-02 の GitHub Changelog で入りました。あとは gh aw compile で lock ファイルを再生成します。

生成物の変化は 3 つでした。

  • 各ジョブに copilot-requests: write が追加された
  • COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}${{ github.token }} に変わった
  • PAT を検証するステップが丸ごと消えた。検証すべき PAT が無くなったので当然です

2 つ目が紛らわしいのですが、COPILOT_GITHUB_TOKENGITHUB_TOKEN の別名ではありません。GITHUB_TOKEN は Actions が発行するトークンそのもの、COPILOT_GITHUB_TOKEN は gh-aw が Copilot CLI の認証に読みにいくシークレット名(入れ物)です。中身は fine-grained PAT 限定で、GitHub App や OAuth トークンは受け付けません。

copilot-requests: write を付けると、gh-aw は推論に ${{ github.token }} を使い、secrets.COPILOT_GITHUB_TOKEN に入れた PAT は推論に渡されなくなります。環境変数名が残ったまま中身だけ入れ替わっているのは、そのためです。

移行後、定期実行は復旧し、リポジトリのシークレットは 0 件 になりました。期限切れという読みも、これで裏づけが取れています。ちなみに、同じ構成のリポジトリが他にもないか棚卸ししたら 1 件見つかったので、そちらも先回りで移行しています。

ここまでに出てきたトークンの関係を 1 枚に整理すると、こうなります。

PAT / GITHUB_TOKEN / COPILOT_GITHUB_TOKEN の相関図。実体のレイヤーでは PAT が人に、GITHUB_TOKEN が GitHub App のインストールアクセストークンに紐づく。ワークフローに書く名前のレイヤーでは、COPILOT_GITHUB_TOKEN が PAT を入れるシークレット名、copilot-requests: write が github.token を使う指定になる

移行するときに踏みそうな罠が 3 つあります。

  • CI テストが旧方式を強制していないか先に確認する。ワークフローを移行しても、テスト側が PAT の存在を要求していると、そこで止まります
  • schedule はデフォルトブランチからしか動かない。マージするまで修正は効きません
  • gh-aw を使っている場合は .md.lock.yml をセットでコミットする。frontmatter だけ直して lock を再生成し忘れると、何も変わりません

6 日後の追記

修正後の定期実行は 5 回連続で成功しました。ところが 8/22 と 8/23 はまた失敗しています。落ちたステップはさっき直したのと同じ Copilot CLI の実行で、正直「え、また?」と思いました。

ログを見たら別物でした。

[copilot-harness] attempt 1 failed: exitCode=1
  failureClass=permission_denied
  isAuthError=false
  permissionDeniedCount=11

isAuthError=false で、エージェントのツール実行が 11 回拒否されてアボートしていました。今回潰した PAT まわりとは別のレイヤーです。認証エラーのほうは再発していません。

業界はどこへ向かっているか

保管する秘密を減らす方向へ動いているのは、GitHub だけではありません。

Microsoft Entra の ワークロード ID フェデレーションの概念 には、今回の私の障害をそのまま説明したような一文があります。

これらの資格情報はセキュリティ上のリスクをもたらすため、安全に保管し、定期的にローテーションする必要があります。また、資格情報の有効期限が切れると、サービスのダウンタイムのリスクも発生します。

長期の資格情報が持つ問題を、漏洩リスク有効期限切れによるダウンタイム の 2 つとして並べています。前者はよく語られますが、後者を明記しているのが良いですね。私が踏んだのも後者でした。

Azure Pipelines も同じ道を通っています。2023 年のワークロード ID フェデレーションのプレビュー告知は、「Azure サービス接続へのシークレットと証明書の格納を停止しますか?」「有効期限が切れるたびにこれらのシークレットのローテーションについて心配するのをやめたいですか?」という問いかけから始まっていました(Sprint 227 リリースノート)。Microsoft の ゼロトラストを開発者ワークフローに組み込む というガイダンスも、GitHub OIDC と Entra のフェデレーションの組み合わせを推奨しています。

整理すると、こういう構図になります。

レイヤー 長期の資格情報 短命の資格情報
GitHub 内の操作 PAT GITHUB_TOKEN(GitHub App のインストールトークン)
GitHub から Azure サービスプリンシパルのシークレット GitHub OIDC + Entra ワークロード ID フェデレーション
Azure Pipelines から Azure サービス接続に保管したシークレット ワークロード ID フェデレーション
Kubernetes から Azure Secret に置いた資格情報 ワークロード ID

短命トークンにも期限はありますが、人が保管して更新する対象ではなくなります。私が今回変えたのは左上の 1 マスだけです。

付録: 切り分けに使ったコマンド

今回使ったコマンドです。すべて PowerShell 7 で実行しています。

# 原因は上流に出る。まず実行履歴を見る
gh run list --limit 20

# 失敗ステップのログだけ
gh run view <run-id> --log-failed

# 失敗したステップだけをジョブ名つきで抽出する。原因の場所が一撃で分かる
gh run view <run-id> --json 'jobs' `
  --jq '.jobs[] | .name as $j | .steps[] | select(.conclusion=="failure") | "\($j) :: \(.name)"'

# シークレットの更新日を確認
gh secret list

PowerShell から gh を呼ぶとき、--jq に渡す式は 必ずシングルクォートで囲んでください。ダブルクォートだと $j のような jq の変数を PowerShell が先に展開してしまい、式が壊れます。私はこれで 1 回転びました 😅

まとめ

調べ直して腑に落ちたのは、PAT と GITHUB_TOKEN が同じトークンの安全版・危険版ではなく、別の目的で作られたもの だということでした。PAT は人の作業を助けるために、GITHUB_TOKEN は機械の自動化のために生まれています。

選び方はこう整理できます。

  • 同じリポジトリの中で完結する自動化GITHUB_TOKEN
  • 複数リポジトリや組織をまたぐ自動化 → GitHub App
  • 自分が手で操作する延長 → PAT

私のワークフローでは PAT を削除できました。次は、5 日間も失敗に気づけなかった通知のほうを直します。手元に PAT で動いている Actions があるなら、permissions: 1 行で足りないか一度見てみてください 💪

参考 / 出典

2
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
2
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?