| name | pypi-security-best-practices |
| description | Guide for implementing security best practices when using Python packages from PyPI with uv and pip |
| triggers | ["how do I secure my Python package installations","show me PyPI security best practices","how to prevent supply chain attacks in Python","configure uv with security hardening","implement dependency cooldown for PyPI packages","verify package hashes with pip and uv","secure my Python development environment","protect against malicious PyPI packages"] |
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)
curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
uv --version
pip
python -m pip --version
python -m pip install --upgrade pip
Security Tools
python -m pip install pip-audit
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:
uv pip install --only-binary :all: requests
[tool.uv.pip]
only-binary = [":all:"]
[pip]
only-binary = [":all:"]
With pip:
pip install --only-binary :all: requests
export PIP_ONLY_BINARY=:all:
pip install requests
[install]
only-binary = :all:
2. Dependency Cooldowns
Avoid newly-published malicious packages by excluding recent releases:
With uv:
[tool.uv]
exclude-newer = "7 days"
exclude-newer = "30 days"
uv lock --exclude-newer "7 days"
uv sync --exclude-newer "7 days"
export UV_EXCLUDE_NEWER="7 days"
uv sync
Per-package overrides:
[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { requests = "1 day" }
With pip (v26.1+):
[install]
uploaded-prior-to = P7D
pip install --uploaded-prior-to=2026-06-01 requests
pip install --uploaded-prior-to=P0D requests==2.32.3
Dependabot cooldown:
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
cooldown: 7
Renovate cooldown:
{
"packageRules": [
{
"matchDatasources": ["pypi"],
"minimumReleaseAge": "7 days"
}
]
}
3. Hash Verification
Always verify package integrity with cryptographic hashes:
With uv (automatic in lockfile):
uv lock
uv sync
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:
pip-compile --generate-hashes requirements.in
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:
uv lock
uv sync
uv sync --frozen
With pip:
pip freeze > requirements.txt
pip-compile requirements.in -o requirements.txt
pip install -r requirements.txt
5. Prevent Dependency Confusion
Configure package sources to prevent private/public namespace collisions:
With uv:
[[tool.uv.index]]
name = "company-internal"
url = "https://pypi.company.com/simple"
explicit = true
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true
With pip:
[global]
index-url = https://pypi.org/simple
extra-index-url =
https://pypi.company.com/simple
[install]
trusted-host = pypi.company.com
6. Vulnerability Scanning
Regularly scan dependencies for known vulnerabilities:
With pip-audit:
pip-audit
pip-audit -r requirements.txt
pip-audit --format json -o audit.json
pip-audit --fix
pip-audit --ignore-vuln PYSEC-2024-1234
With uv-secure:
uv-secure scan
uv-secure scan --exit-code
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:
npm install -g @socketsecurity/cli
socket python scan requirements.txt
socket ci
Phylum for supply chain analysis:
curl -sSL https://sh.phylum.io/ | sh
phylum analyze requirements.txt
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:
op item create --category=password \
--title "API_KEY" \
--vault "Development" \
password="${API_KEY_VALUE}"
eval $(op inject -i .env.template -o .env)
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:
brew install dopplerhq/cli/doppler
doppler login
doppler setup
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:
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
11. Publish with Trusted Publishing (OIDC)
GitHub Actions workflow:
name: Publish to PyPI
on:
release:
types: [published]
permissions:
id-token: write
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:
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:
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:
name: Secure Release
on:
push:
tags:
- 'v*'
jobs:
security-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify signatures
run: |
git verify-commit HEAD || exit 1
- name: Vulnerability scan
run: |
uv tool install uv-secure
uv-secure scan --exit-code
- 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
14. Reduce Package Dependency Tree
Minimize dependencies to reduce attack surface:
uv tree
uv pip list --format json | jq '.[] | select(.required_by == [])'
[project.optional-dependencies]
dev = ["pytest", "ruff"]
docs = ["sphinx", "mkdocs"]
Package Health Practices
15. Generate and Track SBOMs
Generate SBOM with uv:
uv export --format requirements-txt | \
cyclonedx-py requirements -r -i - -o sbom.json
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:
import pypistats
stats = pypistats.recent("requests")
print(f"Recent downloads: {stats}")
import requests
response = requests.get("https://pypi.org/pypi/requests/json")
data = response.json()
releases = data["releases"]
print(f"Total releases: {len(releases)}")
urls = data["info"]["project_urls"]
repo = urls.get("Source")
print(f"Repository: {repo}")
Query OSV database:
osv-scanner --lockfile uv.lock
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:
pip download --no-deps requests==2.32.3
unzip -l requests-2.32.3-py3-none-any.whl
unzip -p requests-2.32.3-py3-none-any.whl | grep -E '\.exe$|\.dll$|setup\.py'
Use quarantine tools:
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)
for root, dirs, files in os.walk(tmpdir):
for file in files:
path = os.path.join(root, file)
print(f"File: {path}")
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]
exclude-newer = "7 days"
[tool.uv.pip]
only-binary = [":all:"]
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true
[tool.ruff]
select = ["E", "F", "S"]
ignore = ["S101"]
[tool.pytest.ini_options]
testpaths = ["tests"]
Complete uv.toml for Global Configuration
exclude-newer = "7 days"
[pip]
only-binary = [":all:"]
[cache]
dir = "~/.cache/uv"
[install]
reinstall = false
Troubleshooting
Cooldown Blocks Required Package
Problem: exclude-newer filters out a necessary package version
Solution:
uv sync --exclude-newer P0D
[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:
uv pip install --only-binary :all: --no-binary problematic-package requests
[tool.uv.pip]
only-binary = [":all:"]
no-binary = ["problematic-package"]
Hash Verification Fails
Problem: Hash mismatch during installation
Solution:
uv lock --upgrade-package package-name
uv pip compile --generate-hashes --upgrade requirements.in
pip download --no-deps package-name==version
sha256sum package-name-*.whl
Dependency Confusion Attack
Problem: Wrong package version from wrong index
Solution:
[[tool.uv.index]]
name = "internal"
url = "https://pypi.internal.com/simple"
explicit = true
dependencies = [
"internal-package @ https://pypi.internal.com/simple/internal-package",
]
CI/CD Pipeline Fails After Security Hardening
Problem: Pipeline breaks with security settings
Solution:
- name: Security scan
run: uv-secure scan || true
- name: Security scan (enforced)
run: uv-secure scan --exit-code
- 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