PyPI Index Configuration: Architecture, Trust Boundaries, and Operational Safeguards
Guide to replacing PyPI with a custom index using pip's --index-url, covering trust boundaries, hash verification, operational checks, and failure modes.
04 Aug 2026, 01:42 UTC

Requirements
Organizations often need to replace the public PyPI endpoint with a private mirror, an air-gapped archive, or a vetted index that enforces policy (e.g., license scanning, vulnerability blocking). The requirement is a single source of truth for package metadata and artifacts that can be audited, cached, and served over TLS.
Smallest Suitable Design
pip's --index-url flag swaps the default https://pypi.org/simple/ for any PEP 503-compatible repository. The minimal configuration is:
pip install --index-url https://pkg.example.com/simple/ mypackage==1.2.3
No changes to pip.conf or environment variables are required; the flag works per-invocation, which keeps the change explicit and auditable.
Adding a Fallback
When the custom index may miss a dependency, --extra-index-url appends a secondary source without replacing the primary:
pip install \\
--index-url https://pkg.example.com/simple/ \\
--extra-index-url https://pypi.org/simple/ \\
-r requirements.txt
Order matters: pip queries the primary index first, then the extra index.
Trust and Data Boundaries
- Transport security – Non-HTTPS endpoints transmit credentials and package hashes in clear text. Always use
https://URLs; self-signed certificates must be added to the system trust store or supplied via--cert. - Integrity verification – PyPI availability does not guarantee that a downloaded wheel matches the publisher's artifact. Use
--require-hashestogether with arequirements.txtthat includes--hash=sha256:…entries. This forces pip to abort on any mismatch. - Index authenticity – A private index should sign its metadata (PEP 691) or serve a
simple/directory with immutable filenames. Without signatures, a compromised index can serve malicious wheels that still pass hash checks if the hashes were poisoned at upload time.
Operational Checks
Verify Index Reachability and Version Set
Run on a machine that has network access to the custom index (CI runner, developer workstation, or bastion host). No elevated privileges are needed beyond read access to the index.
pip index versions requests --index-url https://pkg.example.com/simple/
Expected result: a list of version strings (e.g., 2.31.0, 2.30.0) confirming the package exists. If the command returns ERROR: Could not find a version that satisfies the requirement, the index is missing the package or the URL is wrong.
Validate Metadata via the Public JSON API
For cross-checking, fetch the public metadata directly:
curl -s https://pypi.org/pypi/requests/json | jq '.info.version'
Compare the version with the one reported by your custom index. Discrepancies indicate an out-of-date mirror.
Hash Audit in CI
Add a pipeline step that runs pip install --dry-run --require-hashes -r requirements.txt against the custom index. A non-zero exit code flags missing or mismatched hashes before deployment.
Failure Modes
| Symptom | Root Cause | Mitigation |
|---|---|---|
WARNING: Retrying (Retry(total=4, ...)) then ERROR: Could not find a version | Network partition or TLS handshake failure to the custom index. | Ensure the index URL uses a valid certificate; configure --trusted-host only as a last resort and document the exception. |
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE | Wheel on the index differs from the hash recorded in requirements.txt. | Regenerate hashes with pip hash after confirming the wheel's provenance; update the lock file. |
Installation succeeds but runtime fails with ImportError | Incompatible wheel tag (e.g., cp311-cp311-manylinux_2_17_x86_64 on a musl-based container). | Enforce platform constraints via --platform or build from source (--no-binary :all:) for non-standard environments. |
Conditions That Would Change the Design
- Adoption of PEP 708 (Repository-level yank) – If the index supports yank metadata, pip can automatically avoid yanked releases without extra tooling.
- Shift to a content-addressable store (e.g., Nix, Bazel) – The
--index-urlmodel would be replaced by a lockfile that references immutable store paths. - Regulatory requirement for signed metadata – Migration to an index that publishes
.sigfiles for eachsimple/page, with pip configured to verify via--index-urlplus a custom transport plugin.
Limitations and Practical Verification
The --index-url approach does not provide:
- Automatic mirror synchronization – you must schedule
bandersnatchorpypimirrorruns. - Built-in vulnerability scanning – integrate a scanner (e.g.,
pip-audit) as a separate CI step.
To verify the whole chain after a change, run:
pip install --index-url https://pkg.example.com/simple/ \\
--require-hashes -r requirements.txt --target ./verify-env
Inspect ./verify-env for the expected package directories. Absence of errors and presence of the correct wheel files confirm the configuration works.
Rollback Considerations
Changing --index-url is a runtime flag; it does not mutate global state. If a deployment uses a wrapper script that injects the flag, revert the script to the previous URL. No package uninstallation is required unless the new index installed different wheel versions, in which case re-create the virtual environment from the lock file.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.