Imported from reason-machines/security-skills (
skills/pypi-security-best-practices/SKILL.md). Install upstream withnpx skills add reason-machines/security-skills --skill pypi-security-best-practices. Copyright stays with the author.
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:
- Go to https://pypi.org/manage/account/publishing/
- Add GitHub repository
- 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