npm Trusted PublishingでOIDCを使ってトークンレスでCIからnpmパッケージを公開する
npm Trusted Publishingが2025年7月31日に一般公開されました。 これにより、OpenID Connect (OIDC)を使ってnpmトークンなしでCI/CDからnpmパッケージを公開できるようになりました。
- npm trusted publishing with OIDC is generally available
- Trusted publishing for npm packages | npm Docs
この記事では、npm Trusted Publishingの仕組みや設定方法、実際のリリースフローについて紹介します。
npm Trusted Publishingとは
npm Trusted Publishingは、npmレジストリとCI/CD環境(GitHub ActionsやGitLab CI/CD)の間でOIDCベースの信頼関係を確立する仕組みです。これにより、npmトークンを使わずにパッケージを公開できます。
これまでのCIからnpmパッケージを公開する際には、長期間有効なnpmトークンをCI/CDの環境変数(Secrets)に保存する必要がありました。しかし、このアプローチにはいくつかのセキュリティリスクがあります。
- npmトークンがCIのログや設定ファイルに誤って露出する可能性
- トークンが侵害された場合、無効化するまで悪用される可能性
- トークンの手動ローテーションが必要
- 必要以上に広い権限を持つことが多い
- granular access tokens で scope を制限できるが、monorepo では設定/維持が難しく org 単位になりがち
npm Trusted Publishingは、これらの問題を解決します。 短時間だけ有効で、かつ特定ワークフローに限定された署名付きトークンを使うため、流出しても再利用されにくい構造になっています。
対応CI/CD環境
現在、npm Trusted Publishingは次のCI/CD環境をサポートしています。
- GitHub Actions(GitHub-hosted runners)
- GitLab CI/CD(GitLab.com shared runners)
設定方法
npm Trusted Publishingの設定は大きく2つのステップで構成されます。
1. npmjs.comでTrusted Publisherを設定
まず、npmjs.comのパッケージ設定でTrusted Publisherを設定します。
- npmjs.comのパッケージ設定ページを開く
- “Trusted Publisher”セクションを見つける
- “Select your publisher”でCI/CDプロバイダーを選択
GitHub Actionsの場合は次のような設定をします。
- Organization or user (必須): GitHubのユーザー名または組織名
- Repository (必須): リポジトリ名
-
Workflow filename (必須): ワークフローのファイル名(例:
publish.yml
)- ファイル名のみを入力(フルパスではない)
-
.yml
または.yaml
拡張子を含める必要がある - ワークフローファイルは
.github/workflows/
内に存在する必要がある
- Environment name (オプション): GitHub environmentsを使用する場合
2. CI/CDワークフローの設定
GitHub Actionsの設定例
ワークフローにOIDC権限 id-token: write
を追加します。例えば、次のようなワークフローを設定すると、タグをプッシュしたときにnpmパッケージを公開できます。
name: Publish Package
on:
push:
tags:
- 'v*'
permissions:
id-token: write # OIDCに必要
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# npm 11.5.1以降が必要
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
- run: npm run build --if-present
- run: npm test
- run: npm publish
重要なのはid-token: write
権限とnpm 11.5.1以降を使用することです。これにより、GitHub Actionsがnpmに対して一時的な署名付きトークンを発行できるようになります。
pnpmの
pnpm publish
コマンドはnpm publish
に処理を移譲しているため、pnpmを使う場合もnpm
11.5.1以降が必要です。
実際のリリースフロー
自分が実際に運用している単独パッケージとmonorepoパッケージでのリリースフローを紹介します。
単独パッケージのパターン
新規パッケージのOIDCの準備
新規パッケージでは、OIDCはパッケージが一度公開されていないと設定できません。 そこで、この問題を解決するためにsetup-npm-trusted-publishというツールを作成しました。
READMEのみを含む初期パッケージを一度公開し、OIDC設定をするための下準備を自動化するツールです。 次のコマンドでパッケージを公開できるので、作成したパッケージにTrusted Publisherを手動で行います。
npx setup-npm-trusted-publish <パッケージ名>
実際のパッケージ例。
- setup-npm-trusted-publish 新規パッケージ用
- simple-oidc-example-package 最小構成のOIDC公開例(READMEとpublish用Workflow)
初めての公開後に、実際にCIからのPublishができるようになります。
既存パッケージのOIDCの準備
すでに一度でも公開済みであれば、そのパッケージのnpmjs.com設定画面から直接Trusted Publisherを追加できます。初期公開用の空パッケージを別途用意する手順は不要です。
CIからのPublishの実装
実運用では次の2つのWorkflowに分けています。
-
create-release-pr.yml
: リリースPRを作るだけ(publishしない) -
release.yml
: リリースPRがマージされたら publish して GitHub Release を作る。npm 側の Trusted Publisher にはこのrelease.yml
だけを登録する。
「PRでリリースノートを確認/編集 → マージで公開」というシンプルな二段構成です。
npmjs.comのTrusted Publisher設定では release.yml
のみを指定します。create-release-pr.yml
は公開権限を持たないリリースPRを作るだけのワークフローです。
そのため、user/example-pkg
リポジトリで @user/example-pkg
のTrusted Publisher設定は次のようになります。
- 設定場所:
https://www.npmjs.com/package/@user/example-pkg/settings/access
- Organization or userには
user
を設定 - Repositoryには
example-pkg
を設定 - Workflow filenameには
release.yml
を設定 - Environment name (オプション): なし
1Passwordを使った設定の効率化
Trusted Publisherの設定はnpmjs.comでの手動の作業が必要です。 手動設定でのミスを防ぐため、1Passwordのautofill機能を活用しています。
- 1Passwordで先にTrusted Publisherの設定項目を作成(リポジトリの情報を元に
op
コマンドで生成しておく) - npmの設定画面で1Passwordのautofillを使用して自動入力
Trusted Publisherの設定は項目が多く間違いやすく、設定ミスを減らすことが目的です。 手入力をなくすことで、設定ミスを減らせます。
OIDC連携するためのnpm trusted publishの設定の作業の様子です。 pic.twitter.com/2kRXto3Z45
— azu (@azu_re) September 6, 2025
1. create-release-pr.yml の流れ
create-release-pr.yml
は手動でリリースPRを作成するワークフローです。
実行時に、アップデートするバージョンの種類(patch / minor / major)を選択します。
次のようなステップで動作します。
- パッケージマネージャ検出(
pnpm-lock.yaml
などの存在確認で npm / pnpm / yarn を選択) - 依存関係インストールなしでバージョンだけ上げる(
npm version 1.2.3 --no-git-tag-version
相当) - 直前のリリースタグを取得
- ある:
releases/generate-notes
API で差分ベースのリリースノートを生成 - ない: 初回はテンプレ最小文言
- ある:
-
release/vX.Y.Z
ブランチを作成しpackage.json (+ lockfile)
をコミット - DraftなPRを作成(ラベル:
Type: Release
) - PR本文に自動生成ノートを詰め、編集可能な状態で人間レビュー
create-release-pr.yml
は次のようなことを考慮しています。
- GITHUB_TOKEN 権限でPRを作るため、リポジトリ設定で「Actions can create and approve PRs」をONにしておく
- バージョン更新時に install しないので高速・副作用少なめ
- GitHub Release Notes API で自動的に前回のリリースからの差分を取得
create-release-pr.yml
は次のような内容です。
name: Create Release PR
on:
workflow_dispatch:
inputs:
version:
description: 'Version type'
required: true
type: choice
options:
- patch
- minor
- major
jobs:
create-release-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 'lts/*'
# No need to install dependencies - npm version works without them
- name: Version bump
id: version
run: |
npm version "$VERSION_TYPE" --no-git-tag-version
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
env:
VERSION_TYPE: $
- name: Get release notes
id: release-notes
run: |
# Get the default branch
DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.default_branch')
# Get the latest release tag using GitHub API
# Use the exit code to determine if a release exists
if LAST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null); then
echo "Previous release found: $LAST_TAG"
else
LAST_TAG=""
echo "No previous releases found - this will be the first release"
fi
# Generate release notes - only include previous_tag_name if we have a valid previous tag
echo "Generating release notes for tag: v$VERSION"
if [ -n "$LAST_TAG" ]; then
echo "Using previous tag: $LAST_TAG"
RELEASE_NOTES=$(gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f "tag_name=v$VERSION" \
-f "target_commitish=$DEFAULT_BRANCH" \
-f "previous_tag_name=$LAST_TAG" \
--jq '.body')
else
echo "Generating notes from all commits"
RELEASE_NOTES=$(gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
-f "tag_name=v$VERSION" \
-f "target_commitish=$DEFAULT_BRANCH" \
--jq '.body')
fi
# Set release notes as environment variable
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
env:
GH_TOKEN: $
VERSION: $
GITHUB_REPOSITORY: $
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
branch: release/v$
delete-branch: true
title: "Release v$"
body: |
$
commit-message: "chore: release v$"
labels: |
Type: Release
assignees: $
draft: true
2. release.yml
の流れ
release.yml
はリリースPRがマージされたら自動でnpm publishを実行するワークフローです。
release.yml
は次のようなステップで動作します。
- PRがマージされたか &
Type: Release
ラベルがあるかを確認 -
package.json
の version からタグ名vX.Y.Z
を決定 - そのタグが既に存在したら(再実行 / リトライ)何もしないで終了 → 冪等性確保
- Nodeセットアップ + (必要なら)依存 install / build / test
-
npm publish --provenance
(npm 11.5.1+ が必要) - GitHub Release 作成
- 成功/失敗を元の PR にコメント
release.yml
は次のような内容です。
name: Release
on:
pull_request:
branches:
- master
- main
types:
- closed
jobs:
release:
if: |
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'Type: Release')
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # OIDC
pull-requests: write # PR comment
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Get package info
id: package
run: |
VERSION=$(jq -r '.version' package.json)
PACKAGE_NAME=$(jq -r '.name' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
- name: Check if tag exists
id: tag-check
run: |
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
env:
VERSION: $
- name: Setup Node.js
if: steps.tag-check.outputs.exists == 'false'
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
- name: Install latest npm
if: steps.tag-check.outputs.exists == 'false'
run: |
echo "Current npm version: $(npm -v)"
npm install -g npm@latest
echo "Updated npm version: $(npm -v)"
- name: Install dependencies
if: steps.tag-check.outputs.exists == 'false'
run: npm ci
- name: Publish to npm with provenance
if: steps.tag-check.outputs.exists == 'false'
run: npm publish --provenance --access public
- name: Create GitHub Release with tag
id: create-release
if: steps.tag-check.outputs.exists == 'false'
run: |
RELEASE_URL=$(gh release create "v$VERSION" \
--title "v$VERSION" \
--target "$SHA" \
--notes "$PR_BODY")
echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT
env:
GH_TOKEN: $
VERSION: $
SHA: $
PR_BODY: $
- name: Comment on PR - Success
if: |
always() &&
github.event_name == 'pull_request' &&
steps.tag-check.outputs.exists == 'false' &&
success()
run: |
gh pr comment "$PR_NUMBER" \
--body "✅ **Release v$VERSION completed successfully!**
- 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION
- 🏷️ GitHub Release: $RELEASE_URL
- 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
env:
GH_TOKEN: $
PR_NUMBER: $
VERSION: $
PACKAGE_NAME: $
RELEASE_URL: $
SERVER_URL: $
REPOSITORY: $
RUN_ID: $
- name: Comment on PR - Failure
if: |
always() &&
github.event_name == 'pull_request' &&
steps.tag-check.outputs.exists == 'false' &&
failure()
run: |
gh pr comment "$PR_NUMBER" \
--body "❌ **Release v$VERSION failed**
Please check the workflow logs for details.
🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
env:
GH_TOKEN: $
PR_NUMBER: $
VERSION: $
SERVER_URL: $
REPOSITORY: $
RUN_ID: $
リリースを2分割にしている理由
create-release-pr.yml
とrelease.yml
を分けている理由は次のとおりです。
-
create-release-pr.yml
でバージョンを間違ってもやり直しやすい(リリースPRを閉じて再度作成できる) - リリースPRで、リリースノートを確認/編集できる
- リリースPRで、Contributorにmentionが飛ぶのでどのリリースで変更が入るかがわかりやすい
- create-release-prとreleaseで分けることで、
release.yml
をリトライできる
基本的にはどちらもブラウザ上で完結するので、ブラウザだけでリリースができるようになっています。
リリース手順
実際のリリース手順は次のとおりです。
- GitHub ActionsでPRを作成できるようにする
- リポジトリ設定で”Allow GitHub Actions to create and approve pull requests”を有効化する
- リポジトリのラベルを設定
-
Type: Release
ラベルを用意する - @azu/github-label-setupでラベルを自動設定する
- Create Release PRでリリースPRを作成
- ワークフローが自動でリリースPRを作成する
- リリースPRをレビューしてリリースノートを書く
- PR Bodyがそのままリリースノートになる
- デフォルトで前回PRからの差分CHANGELOGが含まれる
- リリースPRをマージ
- マージ後にCIが自動でnpm publishを実行する
- 自動リリース完了
- npmパッケージを公開する
- PRにリリース結果をコメントする
実際のサンプルは次のURLです。
- リポジトリ: https://github.com/azu/simple-oidc-example-package
- npm: https://www.npmjs.com/package/simple-oidc-example-package
- リリースPR: https://github.com/azu/simple-oidc-example-package/pull/3
単体パッケージのパターンまとめ
OIDC連携の設定は結構面倒ですが、ここまでの設定は自動化したスクリプトを書いています。
setup-release-pr-with-oidc.ts
では次のようなことをやっています。
- パッケージマネージャ検出:
package.json
のpackageManager
フィールド、なければ lockfile を見て npm / pnpm / yarn を判定する - ビルド有無の確認:
package.json
のscripts.build
の存在を確認する - ワークフローディレクトリ生成:
.github/workflows/
ディレクトリを作成する - Release PR 作成用Workflow生成:
create-release-pr.yml
をテンプレートから生成する - Publish 用Workflow生成:
release.yml
をテンプレートから生成し OIDC (id-token: write
) と provenance 付き publish を含める - GitHub Actionsバージョン更新:
pinact run --update
で pinned された action の最新版取得を試行する(失敗時は警告表示) - 変更のコミット:
git status
で差分を確認し workflow ファイルをコミットする(既存ならスキップ) - リポジトリ情報取得:
git remote get-url origin
から owner/name を抽出する - 手動必要設定の案内: GitHub Actions の PR 作成権限設定を有効化するよう警告を表示する
- npm Trusted Publisher 設定ガイド表示: npm アクセス設定ページURLと入力すべき項目(owner / repo / workflow名)を整形して出力する
- 1Password 連携: 1Password CLI が利用可能なら Trusted Publisher 用の項目を検索し、なければフィールド付きで作成する
- 対話メニュー起動: Enter で npm 設定ページをブラウザで開く / S でサマリ表示 / C で初回リリースPR作成手順案内 / Q で終了する
- 初回リリース手順リマインド: NPM 設定完了後に
gh workflow run create-release-pr
を案内する
一部の設定(npm 側の Trusted Publisher 設定、GitHub Actions の権限変更、初回ワークフロー実行など)は手動作業です。しかし、基本的にそのままの流れ作業で、手入力はなしでできるようにしています。
monorepoのパターン
monorepoの場合はsecretlintで実装している方法を使用しています。
基本的な流れは単独パッケージと同じです。
monorepoでも1つのバージョンを使うFixed modeを使ってるので、コマンドがちょっと違うだけで流れは全く同じです。
-
npm version {patch|minor|major}
は、monorepoだとpnpm version {patch|minor|major} --no-git-tag-version && pnpm --recursive exec pnpm pkg set version=\"$(node -p \"JSON.parse(fs.readFileSync('package.json', 'utf8')).version\")\"
で代用 -
npm publish --provenance
は、monorepoだとpnpm -r publish --no-git-checks --access public --provenance
で代用
📝 lernaはOIDCをサポートしていないため、pnpm -r publish --no-git-checks
で各パッケージを npm publish
しています(pnpm publish
はnpm
コマンドを内部的に使うので、npm 11.5.1以上が必要です)。
これに追加でmonorepo用のチェックjobを実行しています。
- provenanceを確認するワークフロー: 新規/既存パッケージでprovenance未設定なら通知する
- pnpmによるpublish: lernaはOIDC未対応なため、内部でnpm publishを呼ぶpnpmを使用する
OIDC未連携のパッケージが残っている場合はコメントで通知します。
monorepoだと、後からパッケージが増えることがあるので、その度にOIDC設定をする必要があります。 そのため、未設定のパッケージを検出して通知するワークフローを追加しています。
それ以外は、単独のパッケージとmonorepoでは大きな違いはありません。
npm Trusted Publishing + OIDCの効果
npmトークンをSecretsに保存しなくてよくなる
Trusted Publisherを設定してOIDC連携をすると、トークンレスでnpmパッケージをCIから公開できます。 これによって、GitHub ActionsのSecretsにnpmトークンを保存する必要がなくなります。
npmトークンの漏洩リスクがなくなり、手動でやるトークンのローテーションも不要になります。
npmトークンによる公開を禁止できる
公開後はパッケージ設定で”Require two-factor authentication and disallow tokens”を有効化しています。 この設定を有効にすると公開経路を対話操作に限定できます。
これによりパケージの公開に関して次のような制約が課されます。
- メンテナーは2要素認証が必要
- 自動化トークンや汎用アクセストークンでの公開が禁止される
そのため、汎用のnpmトークンが漏洩してもパッケージレベルでの乗っ取りを防げるようになります。
Require two-factor authentication and disallow tokens With this option, a maintainer must have two-factor authentication enabled for their account, and they must publish interactively. Maintainers will be required to enter 2FA credentials when they perform the publish. Automation tokens and granular access tokens cannot be used to publish packages.
Requiring 2FA for package publishing and settings modification | npm Docs
Provenance生成できる
Trusted Publishingを使用すると、npmは自動でprovenance attestationsを生成・公開します。 これにより、パッケージがどこでどのようにビルドされたかを示せます。 利用者は生成元とビルド経路を機械的に検証しやすくなります。
Provenanceは、次のサイトで確認できます。
運用面の違い
- ブラウザだけでnpmパッケージの公開とリリースノート作成が可能
- リリースフローの標準化でメンテナーのバスファクターを軽減
- 手動でのnpmトークン管理が不要になる
- Protection RulesetやEnvironmentのProtection Rulesなどを使わないと、GitHubの権限を渡すことがnpmの権限を渡すことと同義になる
現在の制約事項
現時点での主な制約は次のとおりです。
- セルフホストランナーは未対応(現状はクラウドホスト型のみ)
- 1つのパッケージに設定できるTrusted Publisherは1つだけ
- プライベートリポジトリからはprovenanceを生成できない
- npm CLIはv11.5.1以上が必要
不満点
実際に運用してみて感じる不満点もあります。
- npmの最新版がNode.jsにビルトインされていないため、
npm i -g npm
が必要 - GITHUB_TOKENで作成したPRではCIが即時には走らない。そのため、merge-gatekeeper等の必須チェックがあるとリリースPRがProtect Rulesにかかることがある
- GitHub App Tokenを使うと解決するけどそこまでやるのは面倒
- Protection RulesetやEnvironmentのProtection Rulesなどを使わないと、GitHubの権限を渡すことがnpmの権限を渡すことと同義になる
これが難しくて、ベストな方法がよくわかっていません。
改善ポイント
この変更によって、GitHub Actionsからnpmへのパッケージ公開においては、長期間有効なnpmトークンを使うよりはOIDCを使用することで確実によくなっています。 GitHub Actionsのsecretsにnpm tokenがなくなります。そのため、OIDCのtokenが漏れても短時間で無効になるため、攻撃難易度はあがります。
一方で、ローカルからnpmへpublishするのに比べて、GitHub Actionsからnpmにpublishすると若干リスクは変わる部分がある。 具体的には、従来はGitHubアカウントの乗っ取りがあっても、npmにpublishされることはありませんでした。 これは、GitHubとnpmが別々のアカウントであり、npmトークンが漏れない限りはnpmにpublishされることはなかったためです。
OIDC化によって、GitHubアカウントの乗っ取りにより、GitHub Actionsのワークフローを改変されてnpmにpublishされるリスクがあります。
具体的には次の2つのリスクが残っています。
-
ワークフローファイルの改変リスク:
release.yaml
を直接変更されると、改変後のフローで不正なパッケージを公開される可能性がある- Permission的には
contents: write
とworkflow: write
があれば可能
- Permission的には
-
正規フローを装った攻撃: 悪意あるコンテンツを含む変更をPull Requestで通すことで、npmにマルウェアを公開することがまだ可能である
- Permission的には
contents: write
とPRにラベルを付与できる権限と、PRをマージできる権限があれば可能
- Permission的には
npmのTrusted PublisherのドキュメントにあるようにAdditional security measuresでこれらのリスクはもう少し軽減できる。
自分の場合は、次の2つの対策を入れているけど、もっと楽でもっと良い方法があれば教えて欲しいです。
Environment Protection Ruleの設定
GitHub ActionsのEnvironmentのProtection Rulesを設定することで、release.yml
が動くときにもう一段階の認証を挟んでいます。
具体的には次のような設定をしています。
-
npm
というEnvironmentを作成 - そのEnvironmentにRequired reviewers(オーナー)を設定
- 必ず手動レビューが通らないと実際にはリリースできないようにチェックを設定
- 注意: npm 側の OIDC 設定で Environment を
npm
に設定する必要がある
# .github/workflows/release.yml
jobs:
publish:
runs-on: ubuntu-latest
environment: npm # 作成したEnvironment名 = npmのODC設定にもEnvironment名を設定する必要がある
steps:
# ...
この設定をすると、Actionが走る時に手動でレビューを通るような形にできます。(Required reviewersを入れなければ自動で通る)
ConfirmするとActionが実行される。
チーム内でデプロイは誰かにチェック必須にしたい場合などに、この機能が利用できます。
CODEOWNERSによるワークフロー保護
release.yml
自体が変更されることを防ぐため、次の対策を実装している。
- CODEOWNERSファイルの設定: ワークフローファイルの変更にはオーナーのレビューが必須
# CODEOWNERS
.github/CODEOWNERS @your-username
.github/workflows/ @your-username
- Branch Protection Rules: メインブランチへの直接プッシュを禁止し、必ず Pull Request を経由するルールを設定する。併せて “Require review from Code Owners” を有効化する。
これらの対策により、攻撃者がプロテクションルールを回避するにはPull Requestを通して変更する必要があります。 また、Code Ownersのレビューが必須になるため、攻撃者はオーナーのアカウントを乗っ取るか、Code Ownersに入っている人を乗っ取る必要があります。
Pull Request は公開で行われるため攻撃は可視化され、攻撃者にとって実行しづらくなります。
将来的な改善点
Protection RulesetのRestrict file paths(Enterprise)を使用することで、ファイルパスに対するより厳格なプロテクションルールを設定できるようになります。
これを活用すると、release.yml
に対する変更ルールをさらに厳しく設定できます。ただし、パブリックリポジトリではまだこの機能が利用できない状況です。
また、Protection RulesetやEnvironment Protection RuleがAPIで削除できるようになっています。そのため、アカウントが持っているGitHub Tokenが漏洩すると、これらのルールを削除して攻撃することも可能になってしまう。GitHub Actionsの secrets.GITHUB_TOKEN
の漏洩への対策にはなっているが、アカウントのGitHub Tokenの漏洩への対策にはなっていないという問題があります。
release.yml
に関する処理を改ざんするには、必ずMFAを使わないといけないみたいな設定方法があればいいのですが、その方法が現状だとわかっていないです。
対策まとめ
現在のOIDC化とEnvironment + Protection Ruleset(CODEOWNERS)の組み合わせにより、次の効果が得られています。
- 攻撃者は攻撃に公開Pull Requestを経由する必要がある
- 水面下でのワークフロー改変やGitHub Actionsのトークン(
secrets.GITHUB_TOKEN
)の悪用が難しくなった - ローカルのnpm tokenが漏洩しても、既存のnpmパッケージへのマルウェア版の上書きを防げるようになった
- require two-factor authentication and disallow tokensでMFAが必須になるため
- リリースプロセスの透明性が向上した
完全な防御は不可能ですが、攻撃コストは結構上がる状態が作られている形です。
Require two-factor authentication and disallow tokensにより、ローカルのnpmトークンが漏洩した場合の対策が可能になっています。パッケージレベルでnpmトークンのみでのpublishができなくなるためです。 そのため、影響が大きいパッケージは”Require two-factor authentication and disallow tokens”が有効になっている状態が良いです。
“Require two-factor authentication and disallow tokens” は OIDC 化とは別に設定できます。しかし monorepo では手動認証を都度挟むのは数と時間の都合で難しく、結果として未設定パッケージが多い状態でした。
まとめ
Trusted Publishingを導入することで公開フローの属人性を下げつつ、長期間有効なnpm tokenなしにnpmパッケージを安全に公開できるようになります。また、Provenanceも自動生成されるため、サプライチェーンセキュリティに対する信頼性も向上します。
また、リリースフローをCI/CDにまとめることで、メンテナンスの負担軽減とセキュリティ向上を両立できます。新規パッケージや既存パッケージ、monorepoにも対応可能なため、ぜひ導入を検討してみてください。
実はこのリリースフローは3世代目です。 今まではmonorepoでしかやってなかった(npm token管理が大変なのでやれなかった)のですが、ブラウザだけで、リリースノート書けるし、Contirbutorにどのリリースで入るかが通知できて、パッケージ公開できる仕組みになっています。
- 1世代目: lernaでのmonorepoにおけるリリースフロー(Fixed/Independent) | Web Scratch
- 2世代目: GitHubのリリースノートを自動化する仕組み | Web Scratch
参考資料
- npm trusted publishing with OIDC is generally available
- Trusted publishing for npm packages | npm Docs
- setup-npm-trusted-publish - 新規パッケージ用のセットアップツール
-
setup-release-pr-with-oidc.ts
- 単独パッケージ用のセットアップスクリプト - monorepo-npm-oidc-releases - monorepo用のベーステンプレート
- Nx の攻撃から学べること #s1ngularity | blog.jxck.io - Nxのサプライチェーン攻撃事例
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。