Python開発での包括的サプライチェーン攻撃対策 (2025-2026年版)
東芝 Advent Calendar 2025 9日目の記事です。自己紹介は 3日目の記事 参照。
現時点(2025-12)で、比較的楽に実践できる Python 開発でのサプライチェーン攻撃対策について、備忘録的にまとめてみました。日々の開発、からのパッケージ公開、その後の継続的なメンテナンスを含む全工程で多層的に防御するプラクティスになっています。私がメンテする PySETO 等の OSS リポジトリにも適用済みの内容です。本稿では設定などを抜粋して紹介しますが、完全なものはリポジトリを参照してください。
はじめに
2025年は npm への大規模攻撃 (Great NPM Heist、Shai-Hulud 2.0) が相次ぎましたが、Python も無縁ではありません。2024年に攻撃された Ultralytics は Python ですし、2025年の tj-actions/changed-files の侵害は、GitHubのワークフローが攻撃対象で言語関係なく影響を受けるものでした。
本記事は、GitHub での開発から PyPI への公開、その後の継続的なアップデートを含む全工程 (1.開発、2.CI/CD、3.リリース・公開、 4.継続的メンテナンス) で、サプライチェーン攻撃を防ぐためのプラクティスをまとめたものになっています。
前提 - 開発環境・ツールセット
2025年、Python での開発には、uv、Ruff、mypy の3点セットが固い選択肢な印象ですが、この記事でも uv を前提として、Ruff、mypy を含む以下のツールセットを活用します。
- uv : Python 実行環境管理、パッケージ管理
- Task : タスクランナー
- Aikido Safe Chain : サプライチェーン防御
- pre-commit : コミットフック
- Trivy : 脆弱性スキャン
- pip-audit: 脆弱性スキャン
- Snyk: 脆弱性スキャン
- Ruff : リンタ・フォーマッタ (補助的)
- mypy : 型チェック (補助的)
- GitHub : コードリポジトリ他
- GitHub Actions : CI/CD
- zizmor : ワークフローセキュリティ
- pinact : SHA Pinning 補助ツール
- PyPI : パッケージレジストリ
- PyPI Trusted Publishers : パッケージ公開
- PyPI Attestations : デジタルアテステーション
(余談:mypy代替の高速型チェッカー ty にも注目していますが、まだ時期尚早かなと思ってます)
各ツールのインストール手順は公式リファレンスを参照してください。本記事では、これらのツールやサービスをどう使ってサプライチェーン攻撃対策とするか、ポイントを絞って具体的な設定を示しつつ書いていきます。
開発時の防御
開発最初期から、悪意のあるパッケージや脆弱なコードがプロジェクトに混入しないように気を付ける必要があります。最初の防御層ですね。
依存関係を固定する(uv + uv.lock)
ロックファイル (uv を使う場合には uv.lock) をリポジトリに入れて、全パッケージのバージョンとハッシュを固定し、改ざん検出可能な状況を維持します。CIでは、uv sync --frozen として、uv.lock 固定でインストールするようにします。
# npm ci と同じ。CI では必須。
uv sync --frozen
依存関係を最小にする (uv add --dev, etc.)
当たり前ですが一応。開発時に必要な依存関係は本体に混ぜないようにします。 uv add --dev pytest などして、[dependency-groups] で明確に分離します。
# 中略
dependencies = [
"requests>=2.32.0",
]
[dependency-groups]
dev = [
"pre-commit>=4.0.0",
"pip-audit>=2.7.0",
"ruff>=0.8.0",
"mypy>=1.13.0",
"pytest>=8.3.0",
]
# ...
ゼロデイ攻撃を緩和する(uv * --exclude-newer)
新し過ぎるリリースを排除してゼロデイ攻撃を緩和する対策をしておきます。乗っ取られてマルウェアが混入されたパッケージは数日程度で問題が発覚することを期待して「リリースから数日経ったパッケージのみ」インストールするようにします。タイポスクワッティング攻撃の防止にもなります。
例えば JavaScript系では、pnpm で minimumReleaseAge というオプションを使って、「リリースから数日経ったパッケージのみ」を取り込む設定ができます。uv でも add や lock 等の依存解決時に --exclude-newer を使うことで同様の効果が得られます。ただし、毎度指定するのも面倒なので、Task を使って工夫します(後述)。
なお、--exclude-newer は、乗っ取り直後の悪意あるリリースの混入をブロックできる一方、緊急CVE修正の取り込みまで遅れる副作用があります。言うまでもないですが。こういう場合に備えて、後述する Trivy、pip-audit、Snyk 等を用いた依存スキャンを CI に組み込み、Critical や High の脆弱性が見つかった際にはエラーが起こるようにしておき、例外対処するようにしましょう。組織でやるなら例外プロセスを定めておくとよいです。
setup.py 実行を回避する(uv * --no-build)
Source Distribution (sdist) の任意コード実行を防止します。Python の setup.py は npm の preinstall/postinstall より危険で、pip download でもコードが実行され得ます。
uv には --no-build オプションがあり、ソースから wheel をビルドする工程(= sdist 由来のビルド)を無効化できます。その結果、wheel が提供されていない依存関係が混ざるとインストールが失敗するため、結果として「sdist の任意コード実行(PEP 517 のビルドバックエンド等)を踏ませない」ようにできます。
ただし、ローカルキャッシュに「過去にビルド済みの wheel」が存在する場合は、それが再利用され得ますので、そこもケアしたい場合は、--no-cache を付けるのもありです。逆に、--no-build によってインストールが失敗することもありますので、問題ないことが確認できているものは、例外(特定パッケージのみ許可)を設ける運用もありだと思います。
# プロジェクト依存関係 (wheel のみ)
uv sync --no-build
uv add httpx --no-build
# グローバルツール (wheel のみ)
uv tool install ruff --no-build
uvx --no-build ruff check .
いちいち設定するのは面倒なので 以下のように環境変数で設定してしまうかー
# 全コマンドでビルドを禁止
export UV_NO_BUILD=1
# 特定パッケージのみ禁止
export UV_NO_BUILD_PACKAGE="pkg1 pkg2"
次節で述べるようにタスクランナーで予め指定してしまいます。
対策をタスク化・自動化する (Task)
Task は Go で書かれた YAML ベースのタスクランナーです。あまりツールを増やしたくはないのですが、uv には [tool.rye.scripts] のようなものが無いので。以下のようにすることで、uv sync も --frozen や --no-build を強制できますし、--exclude-newerも vars に日付設定しておき、全体で同じ設定で実行できるようにします。
# Taskfile.yml
version: '3'
vars:
# 2-day delay (security verification window)
EXCLUDE_NEWER_DAYS: 2
EXCLUDE_NEWER: "{{.EXCLUDE_NEWER_DAYS}} days"
tasks:
uv:add:
desc: "Add package (with {{.EXCLUDE_NEWER_DAYS}}-day delay)"
cmds:
- echo "Excluding packages newer than {{.EXCLUDE_NEWER}}"
- uv add --exclude-newer "{{.EXCLUDE_NEWER}}" --no-build --no-install-project {{.CLI_ARGS}}
- task: uv:export-requirements
uv:lock:
desc: "Lock dependencies (with {{.EXCLUDE_NEWER_DAYS}}-day delay)"
cmds:
- echo "Excluding packages newer than {{.EXCLUDE_NEWER}}"
- uv lock --exclude-newer "{{.EXCLUDE_NEWER}}" --no-build
- task: uv:export-requirements
uv:lock:upgrade:
desc: "Upgrade all dependencies (with {{.EXCLUDE_NEWER_DAYS}}-day delay)"
cmds:
- echo "Excluding packages newer than {{.EXCLUDE_NEWER}}"
- uv lock --upgrade --exclude-newer "{{.EXCLUDE_NEWER}}" --no-build
- task: uv:export-requirements
uv:export-requirements:
desc: "Export uv.lock to requirements-txt for scanners"
cmds:
- echo "Excluding packages newer than {{.EXCLUDE_NEWER}}"
- uv export --format requirements.txt --all-groups --locked --exclude-newer "{{.EXCLUDE_NEWER}}" -o requirements.txt
uv:sync:
desc: "Sync environment without building sdists"
cmds:
- uv sync --frozen --all-groups --no-build --no-install-project
uv:run:
desc: "Run command via uv with locked, time-delayed dependencies (e.g., task uv:run -- python app.py)"
cmds:
- uv run --no-sync {{.CLI_ARGS}}
uv:tool:install:
desc: "Install a uv tool without building sdists (e.g., task uv:tool:install -- ruff)"
cmds:
- uv tool install --no-build {{.CLI_ARGS}}
uv の主要コマンドはtask 経由で叩くようにすれば、対策が自動的に反映されるようになります。
task uv:add # x日間の遅延付き & wheel オンリーで依存ライブラリを追加
task uv:lock # x日間の遅延付き & wheel オンリーで依存関係を更新・ロック
task uv:lock:upgrade # x日間の遅延付き & wheel オンリーで依存関係をアップグレード
task uv:sync # uv.lock固定 (--frozen) & wheel オンリーでインストール
task uv:run # uv.lock固定 (--no-sync) & wheel オンリーで実行
なお、uv:add、uv:lock、uv:lock:upgrade では、uv.lockの更新と連動して、requirements.txt も生成し、これをリポジトリで管理するようにしています。これは、後述する Snyk での依存関係の脆弱性スキャンを有効化するためです。本来、(uv.lock と requirements.txt の) 二重管理はしたくないのですが、Synk が uv.lock に対応するまでと割り切って、このような対応を組み込んでいます。
リアルタイムマルウェア検知を使う(Aikido Safe Chain)
--exclude-newer 等をすり抜けられた場合に備え、開発時の第2層の防衛線として、パッケージのインストール時にマルウェアを検出してブロックしてくれるプロキシサーバ、Aikido Safe Chain を入れておくと良いかもしれません。メインターゲットは JavaScript 系 (npm, yarn, etc.)なのですが、ありがたいことに Python 系 (uv, poetry, pip, etc.) にも対応しています (最近 betaが取れたようです)。
ただし、現時点(2025年12月)では、Minimum Package Age (上記のminimumReleaseAgeに相当する「新しすぎるもの」を検知する機能) は npm のみでサポートされ、 python では使えません。
いずれにしても現状では以下のように、CI上で uv sync する前にバージョン固定でインストールしてみています。
- name: Install Aikido Safe Chain (Linux)
if: matrix.platform == 'ubuntu-latest' && matrix.python-version == env.PRIMARY_PYTHON_VERSION
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/1.3.3/install-safe-chain.sh | sh -s -- --ci
- name: Install Aikido Safe Chain (Windows)
if: matrix.platform == 'windows-latest' && matrix.python-version == env.PRIMARY_PYTHON_VERSION
run: iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/1.3.3/install-safe-chain.ps1' -UseBasicParsing) } -ci"
shell: pwsh
コミット前に必ず検証する(pre-commit)
pre-commit 使う派です。問題のあるコードがリポジトリに入る前にブロックする対策として、ライトなもの (リンタ・フォーマッタ系など) は入れておけばいいです。シークレット検知を含むSASTです。私は、pre-commit-hooks、ruff-pre-commit、mirrors-mypy だけ使っています。
以下、設定例。
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: detect-private-key # 秘密鍵の検出 (tests、docsは除く)
exclude: |
(?x)^(
tests/.*|
docs/.*|
README\.md
)$
# - id: detect-aws-credentials
# ...
# Ruff (リンティング、フォーマッティング)
# 設定は後述
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
# mypy (型チェック)
# 設定は後述
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
args: [--config-file=pyproject.toml]
CI/CD で包括的な脆弱性スキャンを行う (trivy, pip-audit)
コミットの後、PR時やデフォルトブランチへのマージ時には、包括的な脆弱性スキャンを行うようにします。Trivy や pip-audit を使います。
Trivy は uv.lock を直接サポートしており、uv.lock を解析して依存関係の脆弱性(CVE)を検出できます。“license” スキャナも持っているのですが、uv に関しては、ライセンスチェックはまだサポートされていません。したがって現時点では、vulnとsecretのみを有効にしています。
pip-audit は、Pythonに特化したスキャナとして、セカンドオピニオン的に設定しています。requirements.txt が必要なので、CI上で uv.lock から動的に生成し、スキャン対象データとして、artifact として残しておくようにしています。
# .github/workflows/security.yml
name: Security Scans
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch: {}
permissions: {}
jobs:
pip-audit:
name: pip-audit (PyPA Advisory)
runs-on: ubuntu-latest
env:
PRIMARY_PYTHON_VERSION: "3.13"
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
python-version: ${{ env.PRIMARY_PYTHON_VERSION }}
- name: Run pip-audit (PyPA Advisory)
run: uv run --with pip-audit pip-audit -r requirements.txt --vulnerability-service pypi
trivy:
name: Trivy Security Scan
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Run Trivy vulnerability scanner in fs mode (SARIF)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
scanners: 'vuln,secret'
exit-code: '0'
なお、サプライチェーン攻撃対策とは直接関係しないので割愛しますが、ライセンスチェックには、 pip-licenses を使っています。
PR 上の脆弱性チェックを見える化する (Snyk Pull Request Checks)
上では Trivy と pip-audit を使って、PR 時や main マージ時に依存関係の脆弱性(CVE等)をスキャンする例を示しました。これに加えて OSS では、Snyk と連携させて、PR 上にステータスチェック(Pull Request Checks)を出すのも有効です。というのも、公開リポジトリであれば、Snyk Open Source (依存ライブラリの脆弱性チェック)や Snyk Code (SAST)、および、これらをPR上でチェック・可視化してくれる Pull Request Checks が無償枠で使えるからです。
以下のような感じです。

Snyk の Web GUI から有効・無効、エラーにする深刻度レベル (High 以上など) を設定できます。
- Integrations > GitHub Settings > Pull request status checks
ただし、注意点があります。Snyk の Python 依存スキャンは、2025-12 現在、uv.lock をサポートしていません。このため、requirements.txt を生成し、リポジトリ上で管理することで、Snyk が参照できるようにしておく必要があります。
対策をタスク化・自動化する (Task) で述べたとおり、私は、Taskfile.yml で、uv.lock 更新時に requirements.txt を同期させるようにして、Synk の uv.lock 対応を待っている状況です。
補助的防御(Ruff、mypy)
Ruff と mypy は直接的なサプライチェーン攻撃対策ではないですが、サプライチェーン攻撃で使われ得る問題のあるコードの検出ができるので設定しておきます。どちらもpyproject.tomlに設定を書くようにします。
Ruff (flake8-bandit, flake-bugbear)
少なくとも "S"ルール (flake8-bandit) は有効化しておきます。セキュリティに関係するものとして、"B"ルール (flake8-bugbear) も有効化しておくとよいかも。
- "S" ルール: セキュリティルール。動的コード実行に使われる
exec(),eval()、デシリアライゼーション攻撃で使われるpickle、marshal、その他インジェクション系の検出ができますし、ハードコードされたパスワードや、安全でないハッシュ関数利用なども見つけてくれます。 - "B" ルール: バグになりやすいパターン検知ルール。この中に、攻撃コードが利用しがちな難読化の検知、インポートしただけでコード実行される可能性がある実装 (デフォルト引数での関数呼び出し) の検知などが含まれています。
以下、設定例。
# pyproject.toml
[tool.ruff.lint]
select = [
"B", # flake8-bugbear: バグになりやすいパターン検知ルール
"E",
"F",
"W",
"I",
"S", # flake8-bandit: セキュリティルール
"T10",
"C4",
"UP",
"ARG",
"SIM",
]
ignore = ["E203", "E501"]
per-file-ignores = { "tests/**" = ["S101", "S106"] }
mypy
mypy も間接的な効用にすぎませんが、型の厳格なチェックによって予期しない動作や、インジェクション攻撃の余地を縮小することができます。
サプライチェーンセキュリティの観点だと、依存する外部ライブラリに型スタブが無ければエラーを返すことが出来るので (ignore_missing_imports = falseなど)、信頼性の低い外部ライブラリの検出が可能です。できる限り、strict=true にしてチェックを厳格化しておくのがよいです。
以下、設定例。
# pyproject.toml
[tool.mypy]
python_version = "3.10"
strict = true
# warn_return_any = true
# no_implicit_optional = true
# strict_equality = true
# warn_unused_ignores = true
disallow_any_unimported = true
warn_unreachable = true
ignore_missing_imports = false
pretty = true
CI/CD の防御
2025年3月の tj-actions/changed-files への攻撃で、CI/CD パイプライン防御の重要性に脚光が当たった感があります。一度侵害されると、全てのビルドとリリースに影響し、甚大な影響をもたらします。
ワークフローの脆弱性を検出する(zizmor)
まず重要なのが、以下の .github/workflows/workflow-security.yml を設定して、GitHub Actions ワークフロー自体の脆弱性を検出できるようにすることです。本章で扱う (リポジトリ設定を除く) 対策の欠如は怒ってくれます。
# .github/workflows/workflow-security.yml
name: Workflow Security Check
on:
pull_request:
paths:
- '.github/workflows/**'
push:
branches: [main]
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
# zizmor の結果を Code Scanning(Security タブ)へアップロード
security-events: write
# 以下、private リポジトリ + advanced-security のときのみ必要
# contents: read
# actions: read
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
with:
inputs: |
.github/workflows
アクションは SHA Pinning する
上記の tj-actions/changed-files への攻撃はタグの上書をするものでした。これを防止するためにアクションは SHA Pinning するようにします。これができていないと、上記の zizmor で error 扱いで怒られます。
# 悪い例: タグは上書き可能
- uses: actions/checkout@v6
# 良い例: SHA は不変
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
これを自動でやってくれる pinact という非常に便利なツールがあります。インストールして、該当リポジトリのディレクトリ下で以下を実行すると、上記の「悪い例」を「良い例」に自動で書き換えてくれます。
pinact run
一度 SHA Pinning 形式にしてしまえば、Dependabot や Renovate で自動更新できます (後述)。
権限を最小にする(permissions: {})
シークレットが含まれないようにするのと同様、侵害時の被害範囲を限定するために、付与する権限を最小にします。
# ワークフローレベルではデフォルトを無効化
permissions: {}
jobs:
# ...
package:
name: Build package
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
permissions:
# 個別ジョブで必要最小限の設定を行う
id-token: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
python-version: "3.13"
認証情報(トークンなど)を不用意に保存しない
GitHub Actions で最も多用する actions/checkout では、以下のように persist-credentials を false に設定します。以下、設定例。
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
このデフォルト値は true で、GITHUB_TOKEN をローカルの Git 設定 (.git/config) に保存します。このため、デフォルトのままだと後段のサードパーティアクションが乗っ取られた場合に、リポジトリに悪意のあるコードを push されてしまいます。
リポジトリのブランチを保護する(Branch Rulesets)
CI/CD パイプラインを守っても、リポジトリ自体が侵害されれば意味がありません。GitHub のブランチ保護ルールを設定して、悪意のあるコードがデフォルトブランチにマージされることを防ぎます。
私の場合は (OSS開発をする GitHub ユーザとしては) 個人開発者なこともあり、 Branch Rulesets ではなく、旧来の Branch Protection Rule で、"Require a pull request before merging" (+ "Require Approvals: 1") 及び "Dismiss stale pull request approvals when new commits are pushed" のみを有効化しています。
これはかなりライトな設定なので、組織管理するリポジトリであれば、signed commit を必須化したり、Branch Rulesets を使って柔軟な設定をする等、より高度な対策ができると思います。
- Settings > Rulesets > New ruleset > New branch ruleset
リポジトリのタグを保護する (Tag Rulesets)
tj-actions/changed-files のインシデントでは、Maintain ロールのアカウントが乗っ取られて、タグの更新がなされて甚大な被害となりました。タグに対しても保護ルールを適用しておきます。
私の場合、個人開発なので効果は限定的ですが、Tag Rulesets を使い、タグの更新や削除を禁止するルール (Restrict updates、Restrict deletions) を リリースタグ (v*) に適用しています。自分も更新・削除ができなくなるのですが。ブランチと同様、これについても柔軟かつ高度な設定ができるので、工夫してみる価値はあるかと思います。
- Settings > Rulesets > New ruleset > New tag ruleset
Zizmor - Audit Rules を読む
上にピックアップしたものは導入時に私が引っ掛かったエラー達ですが、他にもいろいろあります。以下を読んで、危険な設定と修正方法を学んでおくとよさそうです。
リリース・公開時の防御
PyPI への公開は、攻撃者にとって非常に価値の高いターゲットです。一度侵害されると、パッケージをインストールする全てのユーザーに影響しますので、アカウントの保護を含めてしっかり対策する必要があります。
多要素認証には TOTP を使わない (Passkey)
2024年に PyPIはMFAを必須化しましたが、MFAを有効にしたからと言って安全が担保されるわけではありません。2025年、MFAにTOTPを利用していた管理者がフィッシング攻撃被害にあう事例が発生しました (Incident Report: Phishing Attack) 。forward proxy としてユーザとPyPIの間に入って通信を中継する中間者攻撃で、TOTPも窃取できていました。
PyPIのMFAには、フィッシング耐性のある(=プロトコルレベルでのドメイン検証を行う) WebAuthn (Synced Passkey or Device-bound Passkey) を使うようにします。
当然ながら、これは GitHub のアカウントに対しても言えることですので、GitHub でも WebAuthn なMFAを使いましょう。
リリースに OIDC を使う (PyPI Trusted Publishers)
Trusted Publishers は2023年に導入されたパッケージ公開方式です。パッケージ公開を第三者の CI サービス (GitHub Actions など) に OIDC を使って移譲することができます (GitHub が IdP、GitHub Actions (ワークフロー) が RP)。
これにより、長期トークンを排除し、GitHub Secrets に PyPI 認証情報を保存する必要もなく、したがってシークレットのローテーションも不要な形でパッケージ公開が可能になります。
以下が具体的なリリースワークフローです。最後の2行を見ていただければわかりますが、トークンや認証情報を渡さずに済んでいます。
もう1つのポイントが、build と publish のワークフローを分割し、id_token: write 権限を publish フローにのみ限定するようにしている点です。これにより、uv に必要以上の権限を与えず、uv が万が一侵害された場合にも、P悪意あるコードを含んだパッケージリリースが行われないようにしています。
# .github/workflows/cd.yml
name: Python CD
on:
release:
types: [created]
permissions: {}
jobs:
build:
name: Build package
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
python-version: "3.13"
enable-cache: false
- name: Build the project
run: uv build
- name: Show result
run: ls -l dist
- name: Upload package distributions
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: dist
path: dist
deploy:
name: Publish package
runs-on: ubuntu-latest
needs: build
environment:
name: pypi
url: https://pypi.org/p/pyseto
permissions:
id-token: write
steps:
- name: Download package distributions
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: dist
path: dist
- name: Show result
run: ls -l dist
# シークレット無しでパッケージ公開
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
WebAuthnベースで強固に守られた PyPIアカウントを使い、PyPI の Trusted Publisher Manager 画面から RP を登録・管理できます。手順は、Configuring Trusted Publishing を参照。
出所証明できる形でリリースする (PyPI Attestations)
PyPI Attestationsは、2024年11月にPyPI 側で公式にアナウンスされた in-toto/Sigstore/Rekorを使ったデジタル・アテステーション機能 (PEP 740)です。「アテステーション」とは、ざっくり言うと「ある対象の真正性(本物であり、正常な状態であること)を第三者が検証可能な形で示し証明すること」です。
PyPI Attestations により、パッケージとソースコードに対する暗号学的に検証可能なリンクを利用者に提供でき、サプライチェーン攻撃に対する重要な防御層となります。具体的には、パッケージがどのリポジトリ・コミットから生成されたかの証明書が発行され、デジタル署名検証によって確認でき、かつ、透明性ログ(Rekor)に全署名イベントが記録されます。
PyPI 側の案内では、GitHub Actions から Trusted Publishing で公開し、pypa/gh-action-pypi-publish を使って publish している場合、アテステーションの生成・公開はデフォルトで有効になります。したがって、前節の設定例の通りにすれば (pypa/gh-action-pypi-publish@v1.13.0 なので)、OKです。
PyPI Attestations が有効になっていると、PyPI サイト上の リリースパッケージの File Details に、Provenance というセクションが設けられています。

透明性ログのエントリは以下。リリースされたパッケージの証明書がばっちり記録されていますね。
とはいえ、未だ、パッケージを利用する際に pip や uv の標準インストールが自動でこの証明書を検証してくれる、という状態には至っていません。ただ、現時点 (2025-12) でも pypi/pypi-attestations という CLI で検証することはできますし、徐々に利用側がアテステーションを検証することが一般的になっていくだろうと思っています。
継続的メンテナンス
リリース後も、継続的なメンテナンスによって、サプライチェーン攻撃対策に気を配っていく必要があります。幸い、メンテコストを低減してくれるソリューションがいろいろありますので、積極的に使っていきます。
自動更新時にもクールダウン期間を設ける (dependabot - cooldown)
Dependabot や Renovateを使った自動更新は、当たり前のようにやっていると思いますが、サプライチェーン攻撃対策として重要なのは、--exclude-newer (前述) に相当する cooldown を使うことです (Renovate では minimumReleaseAge)。これにより、新しすぎるリリースをブロックして、ゼロデイ攻撃を受ける可能性を緩和できます。
以下、設定例。私は、uv、github-actions、pre-commit を dependabotの更新対象として、すべてに、cooldown を設定しています。
version: 2
updates:
# Python dependencies (uv)
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "daily"
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"
include: "scope"
labels:
- "dependencies"
- "python"
# Wait 2 days after release for supply chain security
cooldown:
default-days: 2
# GitHub Actions dependencies
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "ci"
include: "scope"
labels:
- "dependencies"
- "ci/cd"
- "github-actions"
# Wait 2 days after release for supply chain security
cooldown:
default-days: 2
# Group minor and patch updates together for actions
groups:
github-actions-minor-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
# pre-commit hooks
- package-ecosystem: "pre-commit"
directory: "/"
schedule:
interval: "daily"
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "deps"
include: "scope"
labels:
- "dependencies"
- "pre-commit"
# Wait 2 days after release for supply chain security
cooldown:
default-days: 2
# Group minor and patch updates together for hooks
groups:
pre-commit-minor-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
脆弱性発見時に影響範囲を特定できるようにしておく (Trivy SBOM)
いざという時 (過去のリリースで使っていた依存ライブラリに深刻な脆弱性が見つかった時など)に、楽に影響範囲を特定できるよう、 CI で SBOM を生成し、artifact として残しておくと (将来的に) 便利だろうと考えています。サプライチェーン攻撃対策の観点では、被害調査・対応のアジリティを高める観点です。
SBOM は現時点では完全なものではないですが、日々改善されていくでしょうし、Trivy で常に複数種類のフォーマットで吐き出し、artifact として残しておくのはやっておけばよいと考えています。大したコストではないですし。
以下、設定例。CycloneDX、SPDX 両形式で出力し、artifact としてアップロードします。
trivy:
name: Trivy Security Scan
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
# ...
- name: Generate Trivy SBOM (CycloneDX)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
scan-type: 'fs'
scan-ref: '.'
format: 'cyclonedx'
output: 'trivy-sbom.cdx.json'
severity: 'CRITICAL,HIGH,MEDIUM'
scanners: 'vuln,secret'
exit-code: '0'
- name: Generate Trivy SBOM (SPDX)
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
with:
scan-type: 'fs'
scan-ref: '.'
format: 'spdx-json'
output: 'trivy-sbom.spdx.json'
severity: 'CRITICAL,HIGH,MEDIUM'
scanners: 'vuln,secret'
exit-code: '0'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Upload Trivy SBOM artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
if: always()
with:
name: trivy-sbom
path: |
trivy-sbom.cdx.json
trivy-sbom.spdx.json
楽にツール更新できるようにしておく
前述した --exclude-newer や --no-build を練りこんだ Task で安全かつ一括で依存関係を更新できるようにしておき、これを使って定期的にアップデートします。
# Task で依存関係をアップグレード実行
task lock:upgrade
# pre-commit フックの更新
task uv:run -- pre-commit autoupdate
Dependabot や Renovate に任せてもよいですが、個別に PR が来るのも面倒なので、一息で実行できるユーティリティを用意しておくことはメンテナンスコストの低減にもつながります。
東芝のサイバーセキュリティへの取り組み
さて、社会インフラを支える様々な事業に取り組む当社は、当然ながらサプライチェーン防御を含むサイバーセキュリティに対する取り組みを推進しています。今年6月には、2024年度の活動をまとめた「東芝グループ サイバーセキュリティ報告書2025」を発行しています ↓↓↓
以下のサイトでは、当社グループが実践するサイバーセキュリティ強化の取り組みを紹介しています。
おわりに
一通り、Python開発者向けの包括的なサプライチェーン攻撃対策を挙げてみました。とりあえずやってみた程度のところもありますので、他案・不備・間違いあれば是非教えてください。
なお、冒頭でも言及したとおり、本稿で挙げた対策はすべて、私がメンテしているOSS (PySETO等) で実践しているものです。完全な事例は以下を参照してください。
PySETO は地味に Dependents (Used By) が増えていて (2025年12月時点でもうすぐ8000)、Alpine の community リポジトリ にも登録されているようなので、サプライチェーン攻撃対策しっかりしないと・・と思ったところが執筆背景でした。
記事の末尾に付けるリファレンス一覧を作成します。
参考リンク
ツール公式ドキュメント
パッケージ管理・開発環境
- uv 公式ドキュメント
- uv Resolution (exclude-newer)
- Ruff 公式ドキュメント
- mypy 公式ドキュメント
- pre-commit 公式サイト
- Task (Taskfile) 公式サイト
セキュリティツール
- Aikido Safe Chain - GitHub
- Trivy 公式ドキュメント
- pip-audit - GitHub
- zizmor 公式ドキュメント
- zizmor Audit Rules
- pinact - GitHub
PyPI・パッケージ公開
- PyPI Trusted Publishers
- PyPI Attestations
- Configuring Trusted Publishing (PyPA)
- pypa/gh-action-pypi-publish - GitHub
GitHub
セキュリティインシデント関連
Python / PyPI
- Supply-chain attack analysis: Ultralytics - PyPI Blog
- Incident Report: Phishing Attack - PyPI Blog
- Preventing Domain Resurrection Attacks - PyPI Blog
- Python Package Installation Attacks
GitHub Actions
- tj-actions/changed-files Incident (CVE-2025-30066)
- How to detect vulnerable GitHub Actions at scale with Zizmor - Grafana Labs
npm (参考)
- Shai-Hulud 2.0: Guidance for detecting, investigating, and defending against the supply chain attack
ベストプラクティス・解説記事
- We should all be using dependency cooldowns
- Python Supply Chain Security Made Easy - Michael Kennedy
- Securing GitHub Actions with William Woodruff - Open Source Security
- Socket Now Supports uv.lock Files
仕様・標準
- PEP 740 – Index support for digital attestations
- PEP 751 – A file format to list Python dependencies for installation reproducibility
- in-toto - A framework for securing software supply chains
- Sigstore
- Rekor - Sigstore Transparency Log
Discussion