Pin every dependency to an exact version, record a hash for each, and install in hash-checking mode so pip refuses anything that does not match. Do the install in a build stage, then copy the result forward. This makes builds repeatable and blocks swapped packages.
What hash-checking mode enforces
The pip documentation describes the mode as verifying downloads against hashes stored in a requirements file, to guard against remote tampering and network problems. It sets strict rules.
| Rule | Meaning |
|---|---|
| Hashes on all requirements | One unhashed line would let a package slip in |
| Hashes on all dependencies | An unlisted dependency without a hash causes an error |
| Pinned versions | Use the double equals pin, a URL or a path |
| All or nothing | Adding a hash to one line turns the mode on for the file |
You can also force the mode on with the require hashes option. Generate the file with a lock tool rather than by hand, since transitive dependencies are included and the list is long.
A Dockerfile pattern
FROM python:3.13-slim AS build
WORKDIR /src
COPY requirements.lock .
RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install -r requirements.lock
FROM python:3.13-slim
# COPY with the from option set to build
COPY /opt/venv /opt/venv
COPY app /app
ENV PATH="/opt/venv/bin:$PATH"
USER 10001
CMD ["python", "/app/main.py"]
Put the require hashes setting in the lock file itself or in pip configuration, so the install cannot run unchecked. Confirm the Python tags you choose are still supported.
Install your own project without dependencies
The pip docs advise installing your own project with the no deps option, because running setup scripts directly can download dependencies that skip the hash check. Install dependencies from the lock file first, then your code with no dependency resolution.
Limits
Hashes prove the file you got is the file you recorded. They do not prove the file is safe. If the package you pinned was malicious or has a known flaw, a hash happily matches it. So combine this with scanning, review of new dependencies and a delay before adopting brand new releases.
Also expect friction. Updates now need a regenerated lock file, ideally by an automated pull request. Platform specific wheels can need separate hashes per platform, so build the lock on the platform you ship, or generate it for each.
Steps
- Generate a lock file with versions and hashes for all dependencies.
- Commit it and install only from it in the Dockerfile.
- Use a build stage and copy the virtual environment forward.
- Automate lock refreshes and scan every update.
- Rebuild the image on a schedule for base image fixes.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
Do hashes stop malicious packages?
They stop swaps after you pinned. They do not judge whether the pinned package was safe, so scan and review too.
Why does pip complain about a missing hash?
In hash-checking mode every requirement and dependency needs one. Regenerate the lock file so it includes them all.
Safeguard
Safeguard does SBOM and dependency scanning, and its policies and gates can run in CI so a bad image does not reach production. You can try a free scan before deciding anything.
Sources
Last reviewed: 19 October 2026.