pypi-security-best-practices

v2026.09.24

Guide for implementing security best practices when using Python packages from PyPI with uv and pip

GitHub
Install command
npx skhub add reason-machines/pypi-security-best-practices
Markdown
SKILL.md

PyPI Security Best Practices

Skill by ara.so — Security Skills collection.

This skill provides comprehensive guidance on securing Python package installations from PyPI, covering supply chain attack mitigation, dependency verification, and secure development practices for both uv and pip package managers.

Overview

PyPI security best practices help protect against supply chain attacks like the LiteLLM/Telnyx incident (119k+ malicious downloads in under 3 hours) and other compromised package scenarios. This guide covers secure package installation, dependency management, and development environment hardening.

Key Security Principles:

  • Prefer binary-only installations to avoid arbitrary code execution
  • Implement dependency cooldowns to avoid newly-published malicious packages
  • Pin dependencies with cryptographic hash verification
  • Use deterministic installations and prevent dependency confusion
  • Scan for vulnerabilities and verify package health

Installation

uv (Recommended)

# Install uv (macOS/Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install uv (Windows)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# Verify installation
uv --version

pip

# pip is included with Python 3.4+
python -m pip --version

# Upgrade to latest pip
python -m pip install --upgrade pip

Security Tools

# Install pip-audit for vulnerability scanning
python -m pip install pip-audit

# Install uv-secure for lockfile scanning
uv tool install uv-secure

Core Security Practices

1. Binary-Only Installations

Source distributions can execute arbitrary code via setup.py. Enforce binary-only installs:

With uv:

# Command line
uv pip install --only-binary :all: requests

# In pyproject.toml
[tool.uv.pip]
only-binary = [":all:"]

# In uv.toml
[pip]
only-binary = [":all:"]

With pip:

# Command line
pip install --only-binary :all: requests

# Environment variable
export PIP_ONLY_BINARY=:all:
pip install requests

# In pip.conf (Linux/macOS: ~/.config/pip/pip.conf)
[install]
only-binary = :all:

2. Dependency Cooldowns

Avoid newly-published malicious packages by excluding recent releases:

With uv:

# pyproject.toml
[tool.uv]
exclude-newer = "7 days"  # Recommended for general use

# Or more aggressive for production
exclude-newer = "30 days"
# Command line usage
uv lock --exclude-newer "7 days"
uv sync --exclude-newer "7 days"

# Environment variable
export UV_EXCLUDE_NEWER="7 days"
uv sync

Per-package overrides:

# pyproject.toml - exempt security patches
[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { requests = "1 day" }

With pip (v26.1+):

# ~/.config/pip/pip.conf
[install]
uploaded-prior-to = P7D
# Command line (absolute date)
pip install --uploaded-prior-to=2026-06-01 requests

# Bypass cooldown for urgent patches
pip install --uploaded-prior-to=P0D requests==2.32.3

Dependabot cooldown:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown: 7  # Wait 7 days after release

Renovate cooldown:

{
  "packageRules": [
    {
      "matchDatasources": ["pypi"],
      "minimumReleaseAge": "7 days"
    }
  ]
}

3. Hash Verification

Always verify package integrity with cryptographic hashes:

With uv (automatic in lockfile):

# Generate lockfile with hashes
uv lock

# Install with hash verification (automatic)
uv sync

# For requirements.txt workflow
uv pip compile --generate-hashes requirements.in -o requirements.txt
uv pip install -r requirements.txt

Example lockfile entry:

[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "certifi" },
    { name = "charset-normalizer" },
]
wheels = [
    { url = "https://files.pythonhosted.org/packages/.../requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" },
]

With pip:

# Generate hashed requirements
pip-compile --generate-hashes requirements.in

# Install with hash verification
pip install --require-hashes -r requirements.txt

Example requirements.txt with hashes:

requests==2.32.3 \
    --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 \
    --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760
certifi==2024.2.2 \
    --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f

4. Deterministic Installations

Use lockfiles for reproducible builds:

With uv:

# Create lockfile
uv lock

# Install exact versions from lockfile
uv sync

# Install without updating lockfile
uv sync --frozen

With pip:

# Generate pinned requirements
pip freeze > requirements.txt

# Or use pip-tools
pip-compile requirements.in -o requirements.txt

# Install exact versions
pip install -r requirements.txt

5. Prevent Dependency Confusion

Configure package sources to prevent private/public namespace collisions:

With uv:

# pyproject.toml
[[tool.uv.index]]
name = "company-internal"
url = "https://pypi.company.com/simple"
explicit = true  # Only use for explicitly specified packages

[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true

With pip:

# pip.conf
[global]
index-url = https://pypi.org/simple
extra-index-url = 
    https://pypi.company.com/simple

[install]
# Require that private packages come from internal index
trusted-host = pypi.company.com

6. Vulnerability Scanning

Regularly scan dependencies for known vulnerabilities:

With pip-audit:

# Scan installed packages
pip-audit

# Scan requirements file
pip-audit -r requirements.txt

# Output as JSON
pip-audit --format json -o audit.json

# Fix vulnerabilities automatically
pip-audit --fix

# Ignore specific vulnerabilities
pip-audit --ignore-vuln PYSEC-2024-1234

With uv-secure:

# Scan uv lockfile
uv-secure scan

# Fail CI on vulnerabilities
uv-secure scan --exit-code

# Generate SARIF for GitHub
uv-secure scan --format sarif -o results.sarif

In CI/CD (GitHub Actions):

name: Security Scan
on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install uv
        uses: astral-sh/setup-uv@v3
      
      - name: Scan for vulnerabilities
        run: |
          uv tool install uv-secure
          uv-secure scan --exit-code

7. Harden Package Installs with Security Tools

Socket.dev for real-time protection:

# Install Socket CLI
npm install -g @socketsecurity/cli

# Scan Python dependencies
socket python scan requirements.txt

# Monitor CI/CD
socket ci

Phylum for supply chain analysis:

# Install Phylum
curl -sSL https://sh.phylum.io/ | sh

# Analyze dependencies
phylum analyze requirements.txt

# Block malicious packages in CI
phylum check requirements.txt --fail-on-critical

Secure Local Development

8. No Plaintext Secrets in .env Files

Use secret management instead of plaintext .env files:

With 1Password:

# Store secret
op item create --category=password \
  --title "API_KEY" \
  --vault "Development" \
  password="${API_KEY_VALUE}"

# Load secrets into environment
eval $(op inject -i .env.template -o .env)

# Run with secrets
op run -- python app.py

.env.template (commit this):

API_KEY=op://Development/API_KEY/password
DATABASE_URL=op://Development/DATABASE_URL/password

With doppler:

# Install doppler
brew install dopplerhq/cli/doppler  # macOS
# or curl -Ls https://cli.doppler.com/install.sh | sh

# Login and setup
doppler login
doppler setup

# Run with secrets
doppler run -- python app.py

9. Work in Dev Containers

Isolate development environments with containers:

devcontainer.json:

{
  "name": "Python Development",
  "image": "mcr.microsoft.com/devcontainers/python:3.12",
  "features": {
    "ghcr.io/devcontainers/features/uv:1": {}
  },
  "postCreateCommand": "uv sync",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "charliermarsh.ruff"
      ]
    }
  },
  "remoteEnv": {
    "UV_EXCLUDE_NEWER": "7 days"
  }
}

Docker Compose for local development:

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    volumes:
      - .:/workspace
      - uv-cache:/root/.cache/uv
    environment:
      - UV_EXCLUDE_NEWER=7 days
      - UV_NO_SYNC=1
    command: uv run python app.py

volumes:
  uv-cache:

Maintainer Security Practices

10. Enable 2FA for PyPI Accounts

# PyPI requires 2FA for all accounts
# Visit https://pypi.org/manage/account/two-factor/
# Use TOTP app or security key (recommended)

11. Publish with Trusted Publishing (OIDC)

GitHub Actions workflow:

name: Publish to PyPI

on:
  release:
    types: [published]

permissions:
  id-token: write  # Required for trusted publishing

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: astral-sh/setup-uv@v3
      
      - name: Build package
        run: uv build
      
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          # No API token needed - uses OIDC
          skip-existing: true

Configure on PyPI:

  1. Go to https://pypi.org/manage/account/publishing/
  2. Add GitHub repository
  3. Specify workflow name and environment

12. Publish with Package Attestations

Generate provenance attestations:

name: Publish with Attestations

on:
  release:
    types: [published]

permissions:
  id-token: write
  contents: read
  attestations: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: astral-sh/setup-uv@v3
      
      - name: Build
        run: uv build
      
      - name: Generate attestations
        uses: actions/attest-build-provenance@v1
        with:
          subject-path: dist/*
      
      - name: Publish
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          attestations: true

Verify attestations:

# Download and verify package attestations
pip download --no-deps requests==2.32.3
gh attestation verify requests-2.32.3-py3-none-any.whl \
  --owner psf

13. Secure CI/CD Release Pipeline

Branch protection and signed commits:

# .github/workflows/release.yml
name: Secure Release

on:
  push:
    tags:
      - 'v*'

jobs:
  security-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Verify signed commits
      - name: Verify signatures
        run: |
          git verify-commit HEAD || exit 1
      
      # Scan dependencies
      - name: Vulnerability scan
        run: |
          uv tool install uv-secure
          uv-secure scan --exit-code
      
      # SBOM generation
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          format: cyclonedx-json
          output-file: sbom.json
      
      - name: Upload SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.json

14. Reduce Package Dependency Tree

Minimize dependencies to reduce attack surface:

# Analyze dependency tree
uv tree

# Check for unnecessary dependencies
uv pip list --format json | jq '.[] | select(.required_by == [])'

# Use extras for optional dependencies
# pyproject.toml
[project.optional-dependencies]
dev = ["pytest", "ruff"]
docs = ["sphinx", "mkdocs"]

Package Health Practices

15. Generate and Track SBOMs

Generate SBOM with uv:

# Export to CycloneDX format
uv export --format requirements-txt | \
  cyclonedx-py requirements -r -i - -o sbom.json

# With syft
syft packages dir:. -o cyclonedx-json > sbom.json

Track SBOMs in CI:

- name: Generate SBOM
  run: |
    uv export --format requirements-txt > requirements.txt
    syft packages file:requirements.txt -o cyclonedx-json=sbom.json

- name: Upload to Dependency-Track
  env:
    API_KEY: ${{ secrets.DEPENDENCY_TRACK_KEY }}
  run: |
    curl -X POST https://dtrack.company.com/api/v1/bom \
      -H "X-Api-Key: $API_KEY" \
      -F "project=my-project" \
      -F "bom=@sbom.json"

16. Consult Vulnerability Databases

Check package health signals:

# Using pypistats
import pypistats

# Download statistics
stats = pypistats.recent("requests")
print(f"Recent downloads: {stats}")

# Using PyPI JSON API
import requests

response = requests.get("https://pypi.org/pypi/requests/json")
data = response.json()

# Check release cadence
releases = data["releases"]
print(f"Total releases: {len(releases)}")

# Check project URLs
urls = data["info"]["project_urls"]
repo = urls.get("Source")
print(f"Repository: {repo}")

Query OSV database:

# Using osv-scanner
osv-scanner --lockfile uv.lock

# Query specific package
curl -X POST https://api.osv.dev/v1/query \
  -H "Content-Type: application/json" \
  -d '{
    "package": {"name": "requests", "ecosystem": "PyPI"},
    "version": "2.31.0"
  }'

17. Verify Published Package Contents

Inspect package before installing:

# Download without installing
pip download --no-deps requests==2.32.3

# Unzip and inspect
unzip -l requests-2.32.3-py3-none-any.whl

# Check for suspicious files
unzip -p requests-2.32.3-py3-none-any.whl | grep -E '\.exe$|\.dll$|setup\.py'

Use quarantine tools:

# Inspect in isolated environment
import zipfile
import tempfile
import os

def inspect_wheel(wheel_path):
    with tempfile.TemporaryDirectory() as tmpdir:
        with zipfile.ZipFile(wheel_path, 'r') as zip_ref:
            zip_ref.extractall(tmpdir)
        
        # List all files
        for root, dirs, files in os.walk(tmpdir):
            for file in files:
                path = os.path.join(root, file)
                print(f"File: {path}")
                
                # Check for executable permissions
                if os.access(path, os.X_OK):
                    print(f"  WARNING: Executable file")

inspect_wheel("requests-2.32.3-py3-none-any.whl")

Configuration Examples

Complete pyproject.toml with Security Hardening

[project]
name = "my-secure-app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "requests>=2.32.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "ruff>=0.3.0",
    "pip-audit>=2.7.0",
]

[tool.uv]
# Dependency cooldown
exclude-newer = "7 days"

# Binary-only installations
[tool.uv.pip]
only-binary = [":all:"]

# Dependency sources
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true

[tool.ruff]
select = ["E", "F", "S"]  # Include security checks
ignore = ["S101"]  # Allow assert in tests

[tool.pytest.ini_options]
testpaths = ["tests"]

Complete uv.toml for Global Configuration

# ~/.config/uv/uv.toml (macOS/Linux)
# %APPDATA%\uv\uv.toml (Windows)

# Dependency cooldown
exclude-newer = "7 days"

# Binary-only installations
[pip]
only-binary = [":all:"]

# Cache configuration
[cache]
dir = "~/.cache/uv"

# Install configuration
[install]
reinstall = false

Troubleshooting

Cooldown Blocks Required Package

Problem: exclude-newer filters out a necessary package version

Solution:

# Temporarily disable cooldown
uv sync --exclude-newer P0D

# Or add per-package override
[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { my-package = "1 day" }

Binary Not Available for Platform

Problem: --only-binary :all: fails because no wheel exists

Solution:

# Allow source build for specific package
uv pip install --only-binary :all: --no-binary problematic-package requests

# Or in config
[tool.uv.pip]
only-binary = [":all:"]
no-binary = ["problematic-package"]

Hash Verification Fails

Problem: Hash mismatch during installation

Solution:

# Regenerate lockfile
uv lock --upgrade-package package-name

# Or regenerate requirements
uv pip compile --generate-hashes --upgrade requirements.in

# Verify package wasn't tampered with
pip download --no-deps package-name==version
sha256sum package-name-*.whl

Dependency Confusion Attack

Problem: Wrong package version from wrong index

Solution:

# Use explicit indexes
[[tool.uv.index]]
name = "internal"
url = "https://pypi.internal.com/simple"
explicit = true  # Only use when explicitly specified

# Then specify in dependencies
dependencies = [
    "internal-package @ https://pypi.internal.com/simple/internal-package",
]

CI/CD Pipeline Fails After Security Hardening

Problem: Pipeline breaks with security settings

Solution:

# Gradual rollout - start with warnings
- name: Security scan
  run: uv-secure scan || true

# Then enforce
- name: Security scan (enforced)
  run: uv-secure scan --exit-code
  
# Allow cooldown bypass for security patches
- name: Install with conditional cooldown
  run: |
    if [ "${{ github.event_name }}" == "dependabot" ]; then
      uv sync --exclude-newer P0D
    else
      uv sync --exclude-newer 7d
    fi

References

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

NOASSERTION

Source path

skills/pypi-security-best-practices

Default branch

main

Latest commit

304c245

Tree SHA

010f47d