Secure your Python dependencies

Walks through dependency pinning, audit workflows, and supply-chain hardening for teams using uv or pip to pull from PyPI without importing risk.

Best for: Engineering teams shipping Python who want to dodge common package-manager exploits.

Engineering / pipelines-dataatomicfor-engineersno-setupfrom-text

Skill file

Preview skill file
---
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](https://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)

```bash
# 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

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

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

### Security Tools

```bash
# 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:**
```bash
# 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:**
```bash
# 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:**
```toml
# pyproject.toml
[tool.uv]
exclude-newer = "7 days"  # Recommended for general use

# Or more aggressive for production
exclude-newer = "30 days"
```

```bash
# 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:**
```toml
# pyproject.toml - exempt security patches
[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { requests = "1 day" }
```

**With pip (v26.1+):**
```ini
# ~/.config/pip/pip.conf
[install]
uploaded-prior-to = P7D
```

```bash
# 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:**
```yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown: 7  # Wait 7 days after release
```

**Renovate cooldown:**
```json
{
  "packageRules": [
    {
      "matchDatasources": ["pypi"],
      "minimumReleaseAge": "7 days"
    }
  ]
}
```

### 3. Hash Verification

Always verify package integrity with cryptographic hashes:

**With uv (automatic in lockfile):**
```bash
# 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:**
```toml
[[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:**
```bash
# 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:**
```txt
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:**
```bash
# Create lockfile
uv lock

# Install exact versions from lockfile
uv sync

# Install without updating lockfile
uv sync --frozen
```

**With pip:**
```bash
# 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:**
```toml
# 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:**
```ini
# 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:**
```bash
# 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:**
```bash
# 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):**
```yaml
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:**
```bash
# 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:**
```bash
# 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:**
```bash
# 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):**
```bash
API_KEY=op://Development/API_KEY/password
DATABASE_URL=op://Development/DATABASE_URL/password
```

**With doppler:**
```bash
# 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:**
```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:**
```yaml
# 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

```bash
# 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:**
```yaml
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:

```yaml
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:**
```bash
# 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:**
```yaml
# .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:

```bash
# 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:**
```bash
# 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:**
```yaml
- 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:**
```python
# 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:**
```bash
# 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:**
```bash
# 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:**
```python
# 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

```toml
[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

```toml
# ~/.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:**
```bash
# 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:**
```bash
# 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:**
```bash
# 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:**
```toml
# 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:**
```yaml
# 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

- [uv Documentation](https://docs.astral.sh/uv/)
- [pip Documentation](https://pip.pypa.io/)
- [PyPI Security Advisories](https://pypi.org/security/)
- [OSV Vulnerability Database](https://osv.dev/)
- [Supply Chain Security Best Practices (SLSA)](https://slsa.dev/)

Source

Creator's repository · aradotso/security-skills

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk