概要
- ある組織で管理している複数リポジトリの
GITHUB_TOKEN既定権限を「Read and write」から「Read」に切り替えた。 - そのうえで、各workflowのjobに、必要最小限の
permissions:を書き足した。 - あわせて
actions/checkoutにpersist-credentials: falseを指定し、後段のstepからtokenを持ち出しにくくした。 - ADRで方針を決め、3リポに展開した。
- 切替の順序を間違えるとrelease系workflowがまとめて落ちるので、そこだけは注意して回した。
前提: permissions の3階層
GitHub Actionsのjobが使う GITHUB_TOKEN の権限は、次の3階層で決まる。
- repository default: リポジトリ設定の
Settings > Actions > General > Workflow permissions - workflow level: workflowファイルの最上位に書く
permissions: - job level: 各job内に書く
permissions:
下の階層で permissions: を書くと、上の階層の指定を丸ごと上書きする。
job levelにscopeを1つでも書いた瞬間、そのjobは他階層の指定を一切継承しない1。
repository defaultがWriteのままだと、workflow / jobで permissions: を書き忘れた瞬間、そのjobは contents: write を含む広い権限で走る。
サードパーティactionがそのjobの実行中に GITHUB_TOKEN を触れば、リポジトリへの書き込みまで手が届く。
出発点
- 全リポジトリでrepository default = Read and write permissions
- workflowの8割以上で
permissions:が未指定。 - release用のworkflowはtag pushやrelease assetのuploadを実際に必要としており、書き込み権限が要る。
- サードパーティactionはSHA pin済みだが、pin先のコミット自体を検証しているわけではない。
workflow側は「動けばいい」で書かれており、権限付与は既定値まかせだった。
攻撃者側から見ると、read-onlyな用途のjobにまで書き込みトークンが降ってきており、supply chain攻撃の刺さる面積は広い状態だった。
変更1: repository defaultをReadに寄せる
最初にやるのはrepository defaultの切替だ。
GitHub UIから Settings > Actions > General > Workflow permissions で変えられるが、複数リポで一貫して当てたいのでTerraformで書いた。
resource "github_actions_repository_permissions" "this" {
repository = var.repository
default_workflow_permissions = "read"
can_approve_pull_request_reviews = false
}
この設定だけを先にmergeすると、permissions: を明示していないjobはほぼ全滅する。
書き込みを伴うstepが一斉に落ちる形で表面化する。
git pushを伴う自動コミット- release assetのupload
- PR / Issueへのコメントやlabel操作
なので後述の順序で回す必要がある。
変更2: job levelに最小権限を書く
repository defaultをReadに落とすので、書き込みが必要なjobだけ、必要なscopeに絞って permissions: を書き足す。
tag pushを伴うrelease jobなら、こうなる。
jobs:
release:
runs-on: ubuntu-24.04
permissions:
contents: write # tag と release 作成のため
# 他の scope は指定しない = Read すら降りてこない
steps:
- uses: actions/checkout@<sha>
- uses: softprops/action-gh-release@<sha>
permissions: をworkflow levelではなく job level に書く理由は2つある。
- job levelで書けば、そのjob以外はrepository defaultのまま影響を受けない。
- 同じworkflowに複数jobがあるとき、書き込みが要るjobだけに権限を閉じ込められる。
workflow levelに permissions: を書くと、そのworkflowの全jobに一律で継承される。
「lint jobにまでwriteが降ってくる」構図が復活してしまうので、書き先はjob levelに統一した。
変更3: actions/checkout に persist-credentials: false
actions/checkout は既定で、GITHUB_TOKEN をローカルの .git/config にextraheaderとして書き込む2。
これは後段のstepから git push するために必要な挙動だが、書き込みが不要なjobでは単にtoken漏洩の経路になる。
- uses: actions/checkout@<sha>
with:
persist-credentials: false
書き込みが必要なjob(release / bot系)だけ persist-credentials: true を明示し、それ以外はfalseに倒した。
job levelの permissions: contents: read と組み合わせると、GITHUB_TOKEN はcheckoutのHTTPS fetchにしか使われなくなる。
展開の順序
3リポで同じ変更が要ったので、次の順で回した。
- ADRで方針を書き、レビューを回す。
- 各リポのworkflow側PRで、job levelの
permissions:とpersist-credentials: falseを全jobに入れる。 - workflow側PRをすべてmergeした状態を確認する。
- Terraform側のPR(repository default = Read)をmergeする。
順序を逆にすると、Terraform mergeの直後に既存jobが全滅する。
「workflow側の穴埋めが終わった状態」を先に作ってから、defaultをReadに落とすのが安全側。
ハマったポイント
create-github-app-token は id-token: write を要求する
App tokenを発行するstepは contents: read だけでは動かず、id-token: write が要る。
これは permissions: のscope一覧を眺めても直感的に選びにくいので、失敗ログを読んで補うしかなかった。
似た例として、aws-actions/configure-aws-credentials のOIDC経路も id-token: write が要る。
workflow_dispatch のRe-runが突然落ちる
repository defaultをReadにしたあと、過去に走らせたrunの Re-run all jobs を押すと、run当時のworkflow定義に対して 現在の repository defaultが適用される。
「昔動いていた再実行」が理由不明で落ちるので、リリース担当は気付きにくい。
Importantrepository defaultをReadに切替えた直後は、過去runのRe-runはほぼ確実に壊れる。
切替直後の再実行はRe-runに頼らず、workflow_dispatchを新規に発火し直す運用に寄せた方が事故が少ない。
pull_request_target を使うworkflowの見直し
GITHUB_TOKEN の権限を絞ると、pull_request_target トリガのworkflowで、外部コントリビュータPRに対して発火する処理の権限も同時に狭まる。
既存のlabel付与botやauto-assign系が壊れたので、job levelに pull-requests: write を明示的に足して回した。
このタイミングで「そもそも pull_request_target を使う必要があるのか」も一緒に見直せると、後の作業が減る。
まとめ
- repository default = Readに落とす。
- 書き込みが要るjobだけに、
permissions:を job level で書く。 actions/checkoutは書き込みが要らないならpersist-credentials: false- 展開は「workflow側の穴埋め → repository default切替」の順で。
repository defaultがWriteのままだと、workflow側の書き忘れがそのまま「余計な書き込み権限を持つjob」になる。
defaultをReadに閉じてから、書き込みが要る場所だけ穴を開けていく方が、状態としては安全側に倒せる。
-
公式ドキュメント Controlling permissions for GITHUB_TOKEN。 ↩︎