🐍

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/CD3.リリース・公開4.継続的メンテナンス) で、サプライチェーン攻撃を防ぐためのプラクティスをまとめたものになっています。

前提 - 開発環境・ツールセット

2025年、Python での開発には、uvRuffmypy の3点セットが固い選択肢な印象ですが、この記事でも uv を前提として、Ruff、mypy を含む以下のツールセットを活用します。

(余談: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:adduv:lockuv: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-hooksruff-pre-commitmirrors-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時やデフォルトブランチへのマージ時には、包括的な脆弱性スキャンを行うようにします。Trivypip-audit を使います。

Trivy は uv.lock を直接サポートしており、uv.lock を解析して依存関係の脆弱性(CVE)を検出できます。“license” スキャナも持っているのですが、uv に関しては、ライセンスチェックはまだサポートされていません。したがって現時点では、vulnsecretのみを有効にしています。

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 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()、デシリアライゼーション攻撃で使われる picklemarshal、その他インジェクション系の検出ができますし、ハードコードされたパスワードや、安全でないハッシュ関数利用なども見つけてくれます。
  • "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 形式にしてしまえば、DependabotRenovate で自動更新できます (後述)。

権限を最小にする(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 updatesRestrict deletions) を リリースタグ (v*) に適用しています。自分も更新・削除ができなくなるのですが。ブランチと同様、これについても柔軟かつ高度な設定ができるので、工夫してみる価値はあるかと思います。

  • Settings > Rulesets > New ruleset > New tag ruleset

Zizmor - Audit Rules を読む

上にピックアップしたものは導入時に私が引っ掛かったエラー達ですが、他にもいろいろあります。以下を読んで、危険な設定と修正方法を学んでおくとよさそうです。

https://docs.zizmor.sh/audits/

リリース・公開時の防御

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 というセクションが設けられています。

Provenance

透明性ログのエントリは以下。リリースされたパッケージの証明書がばっちり記録されていますね。

https://search.sigstore.dev/?logIndex=779063002

とはいえ、未だ、パッケージを利用する際に pipuv の標準インストールが自動でこの証明書を検証してくれる、という状態には至っていません。ただ、現時点 (2025-12) でも pypi/pypi-attestations という CLI で検証することはできますし、徐々に利用側がアテステーションを検証することが一般的になっていくだろうと思っています。

継続的メンテナンス

リリース後も、継続的なメンテナンスによって、サプライチェーン攻撃対策に気を配っていく必要があります。幸い、メンテコストを低減してくれるソリューションがいろいろありますので、積極的に使っていきます。

自動更新時にもクールダウン期間を設ける (dependabot - cooldown)

DependabotRenovateを使った自動更新は、当たり前のようにやっていると思いますが、サプライチェーン攻撃対策として重要なのは、--exclude-newer (前述) に相当する cooldown を使うことです (Renovate では minimumReleaseAge)。これにより、新しすぎるリリースをブロックして、ゼロデイ攻撃を受ける可能性を緩和できます。

以下、設定例。私は、uvgithub-actionspre-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 として残しておくのはやっておけばよいと考えています。大したコストではないですし。

以下、設定例。CycloneDXSPDX 両形式で出力し、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」を発行しています ↓↓↓

https://www.global.toshiba/jp/news/corporate/2025/06/news-20250618-01.html

以下のサイトでは、当社グループが実践するサイバーセキュリティ強化の取り組みを紹介しています。

https://www.global.toshiba/jp/cybersecurity/corporate.html

おわりに

一通り、Python開発者向けの包括的なサプライチェーン攻撃対策を挙げてみました。とりあえずやってみた程度のところもありますので、他案・不備・間違いあれば是非教えてください。

なお、冒頭でも言及したとおり、本稿で挙げた対策はすべて、私がメンテしているOSS (PySETO等) で実践しているものです。完全な事例は以下を参照してください。

https://github.com/dajiaji/pyseto

PySETO は地味に Dependents (Used By) が増えていて (2025年12月時点でもうすぐ8000)、Alpine の community リポジトリ にも登録されているようなので、サプライチェーン攻撃対策しっかりしないと・・と思ったところが執筆背景でした。

記事の末尾に付けるリファレンス一覧を作成します。


参考リンク

ツール公式ドキュメント

パッケージ管理・開発環境

セキュリティツール

PyPI・パッケージ公開

GitHub

セキュリティインシデント関連

Python / PyPI

GitHub Actions

npm (参考)

ベストプラクティス・解説記事

仕様・標準

本記事の実践例

Discussion