🐧

ghtkn でローカル開発用に GitHub Access Token をセキュアに生成して PAT から卒業する

に公開

ローカル開発のために GitHub App からセキュアに User Access Token を生成する CLI である ghtkn を紹介します。

https://github.com/suzuki-shunsuke/ghtkn

https://github.com/suzuki-shunsuke/ghtkn-go-sdk

まとめ

  • ghtkn は Device Flow を用いて GitHub App から User Access Token を生成する CLI
  • セキュアなローカル開発を実現
    • No More Personal Access Token
    • Personal Access Token と違い短命 (8時間) な access token
    • GitHub App とユーザー両方が持ってる権限しか持たず、両方がアクセスできるリポジトリにしかアクセスできない
    • OS Keyring を用いて access token を管理
      • プレインテキストや shell の環境変数に access token を露出しないため、流出リスクが低い
      • ghtkn v0.2.5 からは OS Keyring 以外にも対応
  • 複数の GitHub App を使い分けることが可能
  • Git Credential Helper としても利用可能
  • access token の自動更新も可能
  • Go SDK によって Go Application にも組み込み可能

ghtkn とは

ghtkn は Device Flow を用いて GitHub App から User Access Token を生成する CLI です。

User Access Token では Installation Access Token と違い、 access token がユーザーに帰属するため、ユーザーとして操作を行うことができます。
ユーザーは ghtkn で生成した access token を用いて任意の処理を行うことができます。
ghtkn によって設定ファイルを用いて複数の GitHub App を管理したり、 Windows Credential Manager や macOS KeyChain, GNOME Keyring のような OS Keyring を用いて access token を管理することができます。
ghtkn v0.2.5 からは OS Keyring 以外にも対応したため、 OS Keyring が使えないコンテナなどの環境でも使えるようになりました。

なぜ ghtkn が必要か

ghtkn を使うことで、ローカル開発における access token の流出リスクを最小化し、セキュリティを高めることができます。
GitHub CLI の OAuth App や Personal Access Token のような有効期限の長い access token を ローカルに保持していると流出時のリスクが大きいです。
ghatkn では Personal Access Token や GitHub App の Private Key や Client Secret のような機密性の高い secret は必要ありません。
必要なのは GitHub App の Client ID だけですし User Access Token の有効期限も 8 時間だけなので相対的に見て安全です。

Getting Started

  1. ghtkn をインストール
  2. GitHub App を作成
  • Enable Device Flow にチェックをつける
  • Webhook のチェックは外す
  • Homepage URL: https://github.com/suzuki-shunsuke/ghtkn (なんでもよい)
  • Only on this account
  • Permissions: なし (デフォルトのまま)
  • Repositories: なし (デフォルトのまま)
  1. ghtkn init で設定ファイルを生成し、修正
  • Windows: %APPDATA%\ghtkn\ghtkn.yaml
  • macOS, Linux: ${XDG_CONFIG_HOME:-${HOME}/.config}/ghtkn/ghtkn.yaml
ghtkn.yaml
apps:
  - name: suzuki-shunsuke/none # GitHub App を識別する名前。ユーザーが自由につけられる
    client_id: xxx # 必須。 GitHub App の Client ID

この状態で ghtkn auth を実行して access token を生成します。

ghtkn auth

するとブラウザで https://github.com/login/device?skip_account_picker=true が開くので、ターミナルに表示されたコードを入力し、承認します。

すると ghu_ で始まる access token が生成され、保存されます。
開いたタブは閉じて大丈夫です。

このようにブラウザを介した認可フローが必要なため、 CI のようなインタラクティブでない環境では ghtkn は使えません。
ghtkn は主にローカルでの開発を想定しています。

ghtkn get を実行すると access token が出力されます。

ghtkn get

この access token を使ってあなたが管理する適当な public repository に gh issue create を実行してみましょう。

REPO=suzuki-shunsuke/ghtkn # あなたの public リポジトリに要修正
env GH_TOKEN=$(ghtkn get) gh issue create -R "$REPO" --title "Hello, ghtkn" --body "This is created by ghtkn"

すると権限不足で失敗します。

GraphQL: Resource not accessible by integration (createIssue)

GitHub App に issues:write 権限を付与して再び実行しましょう。また失敗するはずです。
GitHub App を対象のリポジトリに install して再び実行すると今度は成功するはずです。
Issue の作成者は App ではなくあなたになります。

User Access Token の権限 (Permissions と Repositories) は認可したユーザー (つまりあなた) と GitHub App が両方持つ権限になります。
そのため、上記のように GitHub App が許可されてない操作は出来ませんし、逆にユーザーが権限のない操作も出来ません。

access token の更新を自動化する

v0.3.4 から access token の更新を自動化する機能が追加されました。
これを有効化すると 8 時間毎の認証が不要になります。
詳細はドキュメントを読んでください

ghtkn exec でコマンドを実行する

先ほどは ghtkn get を使って access token を取得して後続のコマンドに渡していましたが、 ghtkn exec を使うと coding agent による access token の漏洩リスクを下げることが出来ます。

ghtkn exec -- gh issue create -R "$REPO" --title "Hello, ghtkn" --body "This is created by ghtkn"

例えば ghtkn get が失敗した場合、 coding agent は調査のために ghtkn get を実行し出力してしまう場合があります。
こういった場合、直ぐに ghtkn revoke コマンドで revoke する必要があります。
ghtkn exec は access token を標準出力はしないので同様の事故が起こりにくくなります。
そのため、コマンドに環境変数として access token を渡したいなら ghtkn get ではなく ghtkn exec がおすすめです。
詳細はドキュメントを読んでください。

ghtkn を使って任意のコマンドをラップする

ghtkn を使って access token を必要とする任意のコマンドの簡単なラッパー (シェル関数) を書けます。

gh コマンドのラッパー
gh() {
    ghtkn exec -- gh "$@"
}

こうして普通に gh コマンドを実行すれば、 access token が勝手に gh コマンドに渡されるようになります。

個人的にはシェル関数よりもシェルスクリプトのほうがオススメです。
PATH 配下にコマンドと同じ名前でスクリプトを作って実行権限を付与します。

gh コマンドのラッパースクリプト
#!/usr/bin/env bash

set -eu

# If GH_TOKEN or GITHUB_TOKEN is set, use it.
if [ -n "${GH_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then
  # echo "[WARN] skip ghtkn because GH_TOKEN or GITHUB_TOKEN is set" >&2
  exec /opt/homebrew/bin/gh "$@" # Specify the absolute path to avoid infinite loop
fi

exec ghtkn exec -e GH_TOKEN -- /opt/homebrew/bin/gh "$@"

aqua でインストールしている場合、絶対パスの代わりに aqua exec でも良いでしょう。

exec ghtkn exec -e GH_TOKEN -- aqua exec -- gh "$@"

Git Credential Helper の設定

ghtkn を使って Git Credential Helper を定義します。
こうすると git push や pull の際に ghtkn の access token で認証するようになります。

  1. ghtkn helper を使用するように Git を設定
~/.gitconfig
[credential]
	helper =
	helper = !ghtkn git-credential

GitHub リポジトリに応じて GitHub App を切り替えたい場合:

  1. 設定ファイルで git_owner を設定

例えば以下のような設定をすると、 suzuki-shunsuke のリポジトリに push/pull する際に suzuki-shunsuke/write の GitHub App が使用されるようになります。

apps:
  - name: suzuki-shunsuke/write
    client_id: xxx
    git_owner: suzuki-shunsuke 

これは GHTKN_APP の設定より優先されます。
ただし、 git_owner は Git Credential Helper のための設定であり、 ghtkn get コマンドはこれを無視します。
git_owner はユニークでなければなりません。

同じ Enterprise GitHub App を複数の GitHub Organization で使う場合、 git_owner の代わりに git_owners を使います。

apps:
  - name: enterprise-app/write
    client_id: xxx
    git_owners:
      - <organization 1>
      - <organization 2>
  1. Git を設定

Git Credential Helper にリポジトリの情報が渡るようにこの設定をします。

git config credential.useHttpPath true

GitHub App を使い分ける

設定ファイルの apps に複数の GitHub App を設定し、 App の Owner (Organization や User) や権限ごとに App を作成して使い分けることができます。
デフォルトでは apps の最初の App が使われます。

コマンドの引数で App を指定できます。

ghtkn get suzuki-shunsuke/write

指定するのは設定ファイルで定義した App の name です。
name は自由に定義できますが、個人的には <app の owner>/<read|write|none> というふうにすると分かりやすくて良いのではないかと思っています。

代わりに環境変数 GHTKN_APP でも指定できます。
例えば direnv を使って directory ごとに GHTKN_APP を切り替えると便利です。
自分の場合、 ~/repos/src/github.com/suzuki-shunsuke という directory 配下に https://github.com/suzuki-shunsuke のリポジトリを checkout してあるので、 ~/repos/src/github.com/suzuki-shunsuke に .envrc を配置します。

source_up

export GHTKN_APP=suzuki-shunsuke/write

同様に ~/repos/src/github.com/aquaproj にも .envrc を配置します。

source_up

export GHTKN_APP=aquaproj/write

デフォルトでは何の権限も持たない App が使われるようにしておきます。
何の権限も持たない access token に意味はないと思う人もいるかもしれませんが、 public repository を read するのには使えますし、 access token なしの場合と比べて API rate limit に引っかかりにくくなるので十分意味はあります。

apps:
  - name: suzuki-shunsuke/none
    client_id: xxx

これで作業ディレクトリによって access token を透過的に切り替えられます。
.envrc に書くのは access token ではなく GHTKN_APP であり、これは secret でもなんでもないので安全です。

会社の Organization で ghtkn を使いたい場合

ghtkn では GitHub App が必要ですが、会社の GitHub Organization にユーザーごとに GitHub App を作るのは一定以上の規模の開発組織では非現実的です。
そこで共有の GitHub App を作り、 Client ID を社内で共有することになると思います。
User Access Token はそのユーザーが持っている権限以上の access token は生成できず、また他人になりすますことも出来ません。
API の rate limit もユーザーごとです。
そのため、社内という限られた空間で共有してもリスクは低いのではないかと思っています。

企業からすると企業の Organization にアクセス可能な開発者の PAT や GitHub CLI の OAuth App の access token が流出するのを防ぐことができます。
万が一 Client ID が社外に流出しても企業の Organization に直接アクセスできるわけではないですし、 access token が流出しても有効期限が短いのでリスクを最小限にできます。

SDK による Go Application への組み込み

ghtkn の Go SDK を使うことで Access Token を必要とする Application で ghtkn を用いて access token を取得できるようになります。
SDK なので ghtkn CLI には依存しませんが、 backend や GitHub App, ghtkn の設定ファイルは当然必要です。
oauth2.TokenSource の実装も提供しているので、非常に簡単に組み込めます。
aqua への組み込みも既にやってますが非常に簡単でした。
aqua の他に pinact, ghir, ghaperf などにも組み込まれています。
最新版を使えば ghtkn の設定ファイルがあるだけでデフォルトで integration が有効になります。

さいごに

以上、ローカル開発のために GitHub App からセキュアに User Access Token を生成する CLI である ghtkn を紹介しました。

https://github.com/suzuki-shunsuke/ghtkn

https://github.com/suzuki-shunsuke/ghtkn-go-sdk

ghtkn を使って、流出時のリスクが高い PAT や GitHub CLI の OAuth access token の利用、煩雑な PAT の管理をやめてみませんか?

Discussion