Clear the security scan findings: patched Starlette/cryptography, and a runtime image without the compiler (#195) #200

Merged
claude-bot merged 1 commit from fix/security-scan-195 into main 2026-10-13 14:52:08 +00:00
Collaborator

Addresses #195 (249 CVEs reported against the production-pinned weatherbot:1.8.1 image). The findings fall into three groups with three different fixes.

What changes

Group Findings Fix
Build tooling shipped in the runtime image (linux-libc-dev, libpq-dev, libssl-dev) ~213 Two-stage Dockerfile: gcc and the -dev headers stay in the build stage; the runtime stage gets a copied venv plus gosu
Python packages 10 starlette 0.52.1 → 1.8.0, cryptography 49.0.0 → 50.0.2; pyjwt and urllib3 pinned explicitly (2.15.1 / 2.8.0)
Debian base packages with a fix available (perl-base, libpcre2, libsqlite3, gzip, bsdutils) ~17 apt-get upgrade in the runtime stage

Roughly 9 findings have no upstream fix yet (libncursesw6, libacl1, libsystemd0, part of bsdutils, one perl-base) and will remain until Debian ships one.

PyJWT and urllib3 were never pinned. They arrive through twilio and requests, so each image carried whatever was current on its build day — that is how 1.8.1 ended up with a PyJWT carrying a critical advisory. A plain rebuild of main would already pick up fixed versions, but only by accident; pinning makes it deliberate and puts them under Renovate.

Nothing at runtime needs the compiler or the headers. asyncpg speaks the Postgres wire protocol itself (no libpq) and Pillow's wheel bundles its codecs. Verified by installing with --only-binary=:all: in a bare python:3.14-slim and running the whole suite there.

CI changes

  • pip-audit is now blocking (--strict, no continue-on-error). It was advisory only because of the backlog this PR clears; the step's own comment said to gate once that was done.
  • CI now builds the image and imports the native dependencies inside it, and asserts gcc is absent. Until now the Dockerfile was only built by release.yml at tag time. A two-stage build adds a new way to break it (a package compiled against a library the runtime stage lacks), so this PR's own CI run is the first real test of the new Dockerfile.

Verification

Run on docker-test in a throwaway python:3.14-slim container with a throwaway Postgres 16:

  • ruff: clean
  • alembic upgrade head against fresh Postgres: 0001 → 0037 clean
  • bulk tier: 1002 passed
  • Postgres tier: 14 passed
  • pip-audit on the resolved set: no known vulnerabilities (the only hit was the container's own stock pip, which CI upgrades and the Dockerfile now upgrades in the venv)
  • import app.main under Starlette 1.8.0: OK. All 21 TemplateResponse calls already use the request-first signature Starlette 1.x requires.

Not verified before push: the image build itself. This session has no local Docker, and the dev host runs the deployed stack, so the build is left to this PR's CI run.

Not in scope

  • The open Renovate PRs (#178 minor/patch group including SQLAlchemy 2.1, #199 Python 3.15, #193 timezonefinder 9, #197 signal-cli). None is needed to clear the scan; #179 (cryptography 50) is superseded by this PR.
  • Production stays on 1.8.1 until a release containing this is tagged and the iac-repo pin is bumped.

🤖 Generated with Claude Code

Addresses #195 (249 CVEs reported against the production-pinned `weatherbot:1.8.1` image). The findings fall into three groups with three different fixes. ## What changes | Group | Findings | Fix | |---|---:|---| | Build tooling shipped in the runtime image (`linux-libc-dev`, `libpq-dev`, `libssl-dev`) | ~213 | Two-stage Dockerfile: gcc and the `-dev` headers stay in the build stage; the runtime stage gets a copied venv plus `gosu` | | Python packages | 10 | `starlette` 0.52.1 → 1.8.0, `cryptography` 49.0.0 → 50.0.2; `pyjwt` and `urllib3` pinned explicitly (2.15.1 / 2.8.0) | | Debian base packages with a fix available (`perl-base`, `libpcre2`, `libsqlite3`, `gzip`, `bsdutils`) | ~17 | `apt-get upgrade` in the runtime stage | Roughly 9 findings have no upstream fix yet (`libncursesw6`, `libacl1`, `libsystemd0`, part of `bsdutils`, one `perl-base`) and will remain until Debian ships one. **PyJWT and urllib3 were never pinned.** They arrive through `twilio` and `requests`, so each image carried whatever was current on its build day — that is how 1.8.1 ended up with a PyJWT carrying a critical advisory. A plain rebuild of `main` would already pick up fixed versions, but only by accident; pinning makes it deliberate and puts them under Renovate. **Nothing at runtime needs the compiler or the headers.** `asyncpg` speaks the Postgres wire protocol itself (no libpq) and Pillow's wheel bundles its codecs. Verified by installing with `--only-binary=:all:` in a bare `python:3.14-slim` and running the whole suite there. ## CI changes - **`pip-audit` is now blocking** (`--strict`, no `continue-on-error`). It was advisory only because of the backlog this PR clears; the step's own comment said to gate once that was done. - **CI now builds the image and imports the native dependencies inside it**, and asserts `gcc` is absent. Until now the Dockerfile was only built by `release.yml` at tag time. A two-stage build adds a new way to break it (a package compiled against a library the runtime stage lacks), so this PR's own CI run is the first real test of the new Dockerfile. ## Verification Run on `docker-test` in a throwaway `python:3.14-slim` container with a throwaway Postgres 16: - ruff: clean - `alembic upgrade head` against fresh Postgres: 0001 → 0037 clean - bulk tier: **1002 passed** - Postgres tier: **14 passed** - `pip-audit` on the resolved set: no known vulnerabilities (the only hit was the container's own stock `pip`, which CI upgrades and the Dockerfile now upgrades in the venv) - `import app.main` under Starlette 1.8.0: OK. All 21 `TemplateResponse` calls already use the request-first signature Starlette 1.x requires. **Not verified before push:** the image build itself. This session has no local Docker, and the dev host runs the deployed stack, so the build is left to this PR's CI run. ## Not in scope - The open Renovate PRs (#178 minor/patch group including SQLAlchemy 2.1, #199 Python 3.15, #193 timezonefinder 9, #197 signal-cli). None is needed to clear the scan; #179 (cryptography 50) is superseded by this PR. - Production stays on 1.8.1 until a release containing this is tagged and the iac-repo pin is bumped. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The fleet scanner reports 249 CVEs against the 1.8.1 image. They fall into
three groups with three different fixes.

Python packages. starlette 0.52.1 -> 1.8.0 and cryptography 49.0.0 -> 50.0.2
are the two pins that were actually vulnerable. PyJWT and urllib3 were never
pinned at all: they arrive through twilio and requests, so an image carried
whatever was current on its build day, which is how 1.8.1 ended up with a
PyJWT that has a critical advisory. They are now pinned explicitly so a
rebuild cannot drift, and so Renovate tracks them.

Build tooling in the runtime image. gcc and the -dev headers were installed
in the image that ships, which pulled in linux-libc-dev (kernel headers) and
libpq-dev. Those two packages account for about 210 of the 249 findings and
nothing at runtime uses either: asyncpg speaks the wire protocol itself and
Pillow's wheel bundles its codecs. The Dockerfile is now two stages, with the
compiler confined to the build stage and a venv copied across.

Debian base packages (perl-base, pcre2, sqlite, gzip, util-linux, openssl).
Fixed upstream but newer than the base tag's last rebuild, so the runtime
stage now runs apt-get upgrade.

CI changes that follow from this:
- pip-audit is now blocking. It was advisory only because of the backlog this
  commit clears, and its own comment said to gate once that was done.
- CI builds the image and imports the native dependencies inside it. Until
  now the Dockerfile was only ever built by release.yml, at tag time, and a
  two-stage build adds a new way to break it: a package compiled against a
  library the runtime stage lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/WeatherBot!200
No description provided.