
こんにちは、プロダクト開発本部でエンジニアをしている秋山です。
みなさんは GitHub の Personal Access Token、まだ classic を使っていますか? kickflowでも、以前は使用していました。正確には「使わざるを得ない場所が一箇所だけ残っていた」という状態でした。
先日、社内のセキュリティ対策の一環として、組織の設定で Personal Access Token (classic) の利用を制限しました。「権限の範囲が広すぎるので、漏洩したときの影響を抑えるために fine-grained PAT へ移行してください」というアナウンスがあり、大半の用途はこれで済みます。
ところが、ローカルの開発環境から GitHub Packages の private な npm パッケージを取得する用途だけは、fine-grained PAT では置き換えられません。この記事ではその穴をどう埋めたか、埋めた結果として何が良くなり、何が変わらなかったのかを紹介したいと思います。
- 一箇所だけ残っていた classic PAT
- GitHub Packages に届くトークンの種類
- GitHub CLI が保存する OAuth App のトークン
- 「スコープが広い」問題の考え方
- gh のトークンを pnpm に渡す
- ユーザーの .npmrc でしか効かない tokenHelper
- セットアップを 1 コマンドにする
- 移行で良くなった点と変わらない点
- 残る課題
- まとめ
- We are hiring!
一箇所だけ残っていた classic PAT
kickflow のフロントエンドは Nuxt 4 で構築されています。そして一部の共通ライブラリを GitHub Packages に private な npm パッケージとして公開して、kickflow 本体から依存関係として取得する構成になっています。
CI では GitHub Actions の GITHUB_TOKEN でパッケージを参照できるのでトークンの管理は必要ありませんが、問題はローカル環境です。各開発者が read:packages スコープの classic PAT を発行し、環境変数に入れて ~/.npmrc から参照する手順になっていました。classic PAT が制限された時点で、この手順では pnpm install が 401 で止まることになり、放置はできません。
「まあ fine-grained PAT に差し替えればいいか」で済まないのが今回の話です。
GitHub Packages に届くトークンの種類
GitHub Packages の npm レジストリに対して、どの種類のトークンで認証できるのかを整理します。
| トークンの種類 | GitHub Packages (npm) | 備考 |
|---|---|---|
| Personal Access Token (classic) | 使える | 公式ドキュメントが挙げている唯一の個人用トークン |
| Fine-grained PAT | 使えない | Packages 用の権限そのものが存在しない |
GITHUB_TOKEN |
使える | GitHub Actions の中だけ |
| GitHub App のトークン | 公式には非対応 | installation token は Packages 非対応と明記 |
| OAuth App のトークン | 使える | read:packages スコープが必要 |
公式ドキュメント には 2026 年 11 月の時点でも「GitHub Packages では personal access token (classic) を使用した認証のみがサポートされています」と書かれています。fine-grained PAT の権限一覧には Packages の項目がなく、付与しようがありません。
fine-grained PAT が GA になったのは 2025 年 3 月ですが、Packages 対応は 2022 年の公開ロードマップに一度「完了」として載ったあと取り消され、2024 年 11 月に公開ロードマップから外されたまま、今も open です。コミュニティのスレッドでは「fine-grained でも npm は動く」という書き込みが時々現れますが、同じスレッドの中で再現できないと否定されており、自分の環境でも 401 でした。
結局、個人の端末から GitHub Packages に届くのは classic PAT と OAuth App のトークンの 2 つで、classic PAT が禁止された以上、OAuth App トークンで解決するほかありません。
GitHub CLI が保存する OAuth App のトークン
OAuth App のトークンと言われてもピンと来ないかもしれませんが、gh auth login でブラウザが開いて認可したときに発行される、gho_ で始まるトークンのことです。GitHub CLI は GitHub 自身が所有する OAuth App で、ログインするとその App のユーザートークンが端末に保存されます。
gh は環境変数 GH_TOKEN 経由で PAT も受け付けますし、fine-grained PAT も 2023 年 5 月から gh で動くようになっています。ただし gh のマニュアルでは、fine-grained PAT は対象リソースの絞り込みがあるため他のリソースを操作したときに挙動が紛らわしくなることから、使用する場合は環境変数 GH_TOKEN で渡すよう注意喚起されています。また、環境変数方式は自動化などの headless な利用に向く、とも書かれています。日常的に gh pr create や gh issue view を叩いている開発者の端末には、 gho_ トークンが 1 本ある状態です。
つまり今回やることは「GitHub Packages 用に新しい種類のトークンを増やす」ことではなく、「すでに端末にある gh のトークンに read:packages を 1 つ足して、pnpm からも使う」ことになります。
「スコープが広い」問題の考え方
classic PAT を禁止した理由は「権限の範囲が広すぎる」でした。では gh のトークンはどうなのか、という疑問が当然出ます。正直なところ、こちらも広いです。gh は最低でも repo read:org gist のスコープを要求し、gh auth login --scopes で足すことはできても減らすことはできません。read:packages だけに絞った classic PAT と比べれば、むしろ広くなります。
それでも今回はこの方式でいくことにしました。理由は、論点が「トークン 1 本のスコープの幅」ではなく「開発者の端末にどの経路のトークンを置くことを認めるか」だからです。
gh を業務で使っている以上、repo スコープのトークンは端末にすでにあります。そこに read:packages 付きの classic PAT が別に 1 本あった状態と、gh のトークンに read:packages を足して PAT を 0 本にした状態を比べると、管理するべきトークンの総量は後者のほうが少なくなります。
GitHub のドキュメントも、コマンドラインから GitHub にアクセスするなら PAT を作らず GitHub CLI か Git Credential Manager を使うように、と案内しています。
セキュリティの厳しい組織では gh 自体が禁止されているのでは、とも思いましたが、調べた範囲ではそうではありませんでした。OAuth App のアクセス制限を有効にして GitHub CLI を個別に許可する、SAML SSO を強制する、端末管理や IP 制限で囲う、という組み合わせが通例で、gh は明示的に許可される側です。gh を止めると他の CLI ツールも道連れになるので、禁止する組織はそう多くなさそうです。
今回の制限は classic PAT が対象で、GitHub CLI のログイントークンは対象外でした。ただし「対象外」は「お墨付き」ではないと自分は受け取っています。スコープが広い状態なのは事実なので、後述する運用条件を付けて使うことにしました。
gh のトークンを pnpm に渡す
やることは 3 つです。
まず、gh のトークンに read:packages スコープを足します。ブラウザでの承認は最初の 1 回だけです。
gh auth refresh --hostname github.com --scopes read:packages gh auth status # Token scopes に 'read:packages' が入っていれば OK
次に、gh auth token を実行するだけのヘルパースクリプトを置きます。
#!/bin/sh exec gh auth token --hostname github.com
最後に、ユーザーの ~/.npmrc に pnpm の tokenHelper を書きます。パスは絶対パスで、引数は付けられません。
//npm.pkg.github.com/:tokenHelper=/path/to/gh-token-helper.sh
tokenHelper は、pnpm が該当レジストリにアクセスするたびにスクリプトを実行し、標準出力の 1 行目をトークンとして使う仕組みです。
もっと手軽な方法として、シェルの設定に export NPM_TOKEN="$(gh auth token)" と書いて ~/.npmrc から ${NPM_TOKEN} で参照する手もあり、実際これでも動きます。ただ、環境変数に入れるとそのシェルから起動するすべてのプロセス(pnpm install の途中で動く依存パッケージのスクリプトも含めて)がトークンを読めてしまいます。前述のとおり gh のトークンには repo スコープが付いているので避けたいところです。
そこで tokenHelper です。これなら pnpm が GitHub Packages に接続する瞬間だけトークンを取得し、子プロセスには渡りません。
ユーザーの .npmrc でしか効かない tokenHelper
設定を各開発者に手で入れてもらうのは避けたかったので、最初はリポジトリの .npmrc に tokenHelper を書いて済ませようとしましたが、これは動きませんでした。pnpm は tokenHelper をユーザーの ~/.npmrc からしか読まない仕様のためです。任意のコマンドを実行する設定なので、リポジトリを clone しただけで実行されては困る、という理由と思われます。
ユーザーの ~/.npmrc にしか書けないのは不便でしたが、書いたあとの優先順位は都合のよい方に転びました。~/.npmrc に旧手順の ${環境変数} の行が失効したトークンのまま残っていても、pnpm の認証ファイルに古いトークンが直書きされていても、tokenHelper の行さえあれば pnpm install は通るため(pnpm 12 系で確認)、移行期間中に古い設定と新しい設定が混在しても動きます。
セットアップを 1 コマンドにする
ユーザーの ~/.npmrc に書く以上、各開発者の手元で何かを実行してもらう必要はありますが、なるべく開発者の作業の手間を減らしたいので、設定スクリプトを GitHub CLI の拡張として配布することにしました。
Organization にリポジトリを作成して、シェルスクリプトを置くだけです。GitHub CLI は既にインストール済みであることが前提なので、配布経路も同じ GitHub CLI に乗せてしまえば、開発者が追加で入れるものはありません。各メンバーが以下コマンドを打つだけでお手軽に設定スクリプトを共有できます。
gh extension install kickflow/gh-pnpm-auth gh pnpm-auth
この拡張がやることは次のとおりです。
ghに github.com でログイン済みか確認する- トークンのスコープに
read:packagesがなければgh auth refreshで足す(ブラウザでの承認はここで 1 回だけ) - ヘルパースクリプトをリポジトリの外の固定パス(
~/.config配下)に置く ~/.npmrcにtokenHelperの行がなければ追記する- 旧手順の設定(環境変数、
~/.npmrcの旧行、pnpm の認証ファイルに直書きしたトークン)が残っていれば、削除と classic PAT の失効を案内する - ヘルパーを pnpm と同じ形(引数なしの絶対パス)で 1 回実行してトークンが返ることを確かめ、GitHub API でパッケージを読み取れることを確認する
何度実行しても同じ結果になるようにしてあるので、pnpm install が 401 になったらまず拡張を再実行する、という運用にできます。トークンの値そのものは拡張の中で表示も保存もせず、gh に任せています。
利用側のリポジトリに置くものは、.npmrc のレジストリ指定(@kickflow:registry=https://npm.pkg.github.com)の 1 行だけです。パッケージを使うリポジトリが複数あっても、設定スクリプトをそれぞれに複製せずに済みます。
ヘルパースクリプトをリポジトリの中ではなく外に置くのは、worktree を消すと ~/.npmrc が指しているパスが消えて pnpm install が通らなくなるからです。普段から git worktree を多用していて、最初にリポジトリ内のパスで試して自分でこの問題を踏み抜きました。
移行で良くなった点と変わらない点
移行前後の比較です。比べる単位は「開発者 1 人が GitHub 向けに持っているトークン」にしています。
| 移行前 | 移行後 | |
|---|---|---|
| 1. GitHub 向けのトークンの本数 | gh のトークン 1 本 + classic PAT 1 本以上 | gh のトークン 1 本 |
| 2. 保管場所 | 環境変数、シェルの設定ファイル、~/.npmrc に平文 |
OS のキーチェーン(gh が管理) |
| 3. トークンを読める範囲 | シェルから起動した全プロセス | pnpm がレジストリに接続する瞬間だけ |
| 4. スコープ | PAT は read:packages のみ |
repo read:org gist workflow + read:packages |
| 5. 有効期限 | PAT は設定次第(無期限も可) | なし(1 年未使用で自動失効) |
| 6. 個別の失効 | PAT ごとに可能 | GitHub CLI アプリの認可を取り消すと全端末分が無効になる |
良くなったのは 1-3 です。平文で置かれるトークンがなくなり、読める範囲が狭くなり、本数が減りました。
変わらない、あるいは悪くなったのが 4-6 です。スコープは classic PAT より広く、縮める手段がなく、無期限でトークン単位の失効もできません。そのうえで、次の条件を付けて運用しています。
gh auth refreshで足すスコープはread:packagesだけにする。設定スクリプトでもそれ以外は足さない- トークンはファイルや環境変数に書き出さない。
gh auth tokenの出力を手でどこかに貼ることもしない gh auth logoutはローカルの保存を消すだけでトークンを失効させない。端末を廃棄するときや漏洩を疑うときは、GitHub の Settings → Applications から GitHub CLI の認可を取り消す
残る課題
GitHub Packages への依存だけが、無期限で広いスコープのトークンに乗り続けています。根本的には 2 つの方向があります。
1 つは GitHub 側の対応を待つことです。fine-grained PAT の Packages 対応が出れば、read:packages 相当の権限だけを持つ有期限トークンに戻せます。ただ 4 年経過しても動きがないのでこちらはあまり期待していません。
もう 1 つはパッケージの配布先を GitHub Packages から別のレジストリへ移すことです。たとえば Google Artifact Registry なら gcloud の 1 時間有効なトークンで取得でき、CI は Workload Identity で認証できるので、個人の長期トークンそのものをなくせます。
ちょうど kickflow は Heroku から Google Cloud に基盤を移行したこともあり、有力な選択肢になります。今回の gh 方式はそこへ移るまでのつなぎと位置づけています。
まとめ
GitHub Packages の npm レジストリは 2026 年 11 月の時点でも fine-grained PAT に対応しておらず、classic PAT が使えなくなると、個人の端末から届く手段は実質 GitHub CLI のログイントークンだけになります。kickflow ではこのトークンに read:packages を足して pnpm の tokenHelper から gh auth token を呼ぶ形にし、セットアップは GitHub CLI 拡張として配布しました。
これでトークンの本数は減り、平文で置く場所もなくなりましたが、スコープが広く無期限という点は変わっていません。そこは運用条件で補いつつ、いずれは Artifact Registry に引っ越して個人の長期トークンそのものをなくしたいと考えています。
We are hiring!
kickflow(キックフロー)は、運用・メンテナンスの課題を解決する「圧倒的に使いやすい」クラウドワークフローです。
サービスを開発・運用する仲間を募集しています。株式会社kickflowはソフトウェアエンジニアリングの力で社会の課題をどんどん解決していく会社です。こうした仕事に楽しさとやりがいを感じるという方は、カジュアル面談・ご応募お待ちしています!