So far we've secured the code WE wrote ourselves. But task-tracker-api also depends on dozens of third-party packages (FastAPI, SQLModel, bcrypt, etc.) and a base Docker image we didn't write ourselves. Vulnerabilities there are just as dangerous — often more so, because we're not always aware we're relying on them. This is called supply chain security.
Why a Dependency Can Become a Gap
Every pip install pulls someone else's code into our application, with the same level of trust as code we wrote ourselves. If one of our dependencies (or a dependency of a dependency, called a transitive dependency) has a publicly known vulnerability (recorded in a CVE database), our application automatically becomes vulnerable too — even though we never wrote the problematic line of code ourselves.
Scanning Dependencies With pip-audit
pip-audit is an official tool from the Python Packaging Authority that checks requirements.txt against a database of known vulnerabilities:
pip install pip-audit
pip-audit -r requirements.txt
No known vulnerabilities found
Clean at this point in time — but this is NOT a one-and-done check. New vulnerabilities keep getting discovered in existing packages (sometimes in packages considered safe for years). The right habit: run pip-audit regularly — ideally automatically in every CI pipeline (exactly the concept covered in CI/CD for Beginners, just add it as one new step in the test job).
If pip-audit finds a vulnerability, the fix is usually as simple as updating to the version that patches it:
pip-audit -r requirements.txt --fix
But always re-run the test suite after updating — a new version sometimes brings an API change that breaks existing code (a breaking change).
Container Hardening — Never Run as Root
By default, a Docker container runs as the root user inside it — if someone manages to exploit a flaw in our application to execute arbitrary code, root inside that container has far more leeway (writing to the entire container filesystem, and in some misconfiguration scenarios, even "escaping" the container).
Our Dockerfile explicitly creates and switches to a regular user:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
# Never run as root
RUN useradd --create-home --shell /usr/sbin/nologin appuser
USER appuser
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Prove it directly:
docker build -t security-demo-app .
docker run --rm security-demo-app whoami
appuser
Not root. This is the least privilege principle — any process should run with the MINIMUM access it needs to do its job, not the most leeway "just in case, to avoid permission errors." Our application never needs to install new software or modify system files at runtime — so there's no reason it needs to run as root.
Other Good Habits for Docker Images
A few additional practices already — or that should be — applied:
python:3.11-slim, not plain python:3.11) — the less software installed in an image, the smaller its attack surface. Every extra tool the application doesn't use is an unnecessary potential gap.Dockerfile only does COPY app ./app, not the entire project directory. Files like .env, tests/, or .git/ never make it into the production image.python:3.11-slim should ideally be pinned to a more specific tag (e.g. python:3.11.9-slim) in a production environment, so builds are always deterministic and don't silently change the moment a new release ships that might introduce a problem.Summary
pip-audit checks dependencies against a database of known vulnerabilities — run it regularly, ideally automated in CI.