なぜ検査したか
このサイトは Python バックエンド一つがログイン・ブログ・語学学習・演習の採点をすべて担います。依存関係は requirements.lock にハッシュまで固定してあるので「何が入っているか」は明確ですが、「その中に既知の脆弱性があるか」は一度も測ったことがありませんでした。Trivy で二か所を検査しました。
- リポジトリ —
requirements.txtの Python 依存、Kubernetes マニフェスト、秘密文字列 - 本番イメージ — 実際に動いているコンテナのファイルシステム(OS パッケージ + インストール済み Python パッケージ)
二つ目が肝心です。リポジトリだけ見ると OS パッケージが抜け、イメージだけ見るとどこを直せばよいかが見えません。
本番イメージの中で検査する方法
イメージレジストリ(Harbor)は認証が必要で、Pod には pull secret がありません(ノードが資格情報を持っています)。そこでレジストリからイメージを取り出す代わりに、本番イメージそのものをコンテナとして起動し、init コンテナで Trivy のバイナリを入れて、その中からルートファイルシステムを検査しました。
initContainers:
- name: get-trivy
image: docker.io/aquasec/trivy:0.58.0
command: ["cp", "/usr/local/bin/trivy", "/tools/trivy"]
volumeMounts: [{ name: tools, mountPath: /tools }]
containers:
- name: scan
image: 192.168.219.202/labhub/backend:build-634 # 本番と同じイメージ
command: ["sh", "-c", "/tools/trivy rootfs --scanners vuln --format json /"]
資格情報なしで本番イメージを検査でき、ノードがイメージをすでに持っているので速い。最初は runAsUser: 0 を入れて CronJob テンプレートの runAsNonRoot ポリシーに引っかかり、Pod が Pending で止まりました。外したら動きました。ルートファイルシステムは一般ユーザーでも読めます。
最初の結果:935 件 — そのうち 167 件は検査ツール自身
{'LOW': 207, 'HIGH': 348, 'MEDIUM': 340, 'CRITICAL': 12, 'UNKNOWN': 28}
対象別: debian 13.6 → 720 · Python → 48 · tools/trivy → 167
三行目を見てください。tools/trivy 167 件 — 私が init コンテナで入れた Trivy バイナリ(Go プログラム)の依存関係です。検査ツールが検査対象の中にあるので、自分自身を数えました。CRITICAL 12 件のうち 4 件(go-git、x/crypto、grpc、Go の標準ライブラリ)がそれでした。除いて数え直すとこうです。
検査ツールを除く: {'LOW': 197, 'HIGH': 275, 'MEDIUM': 264, 'CRITICAL': 8, 'UNKNOWN': 24} = 768 件
そのうち修正版があるもの: {'CRITICAL': 1, 'HIGH': 53, 'MEDIUM': 55, 'LOW': 32} = 141 件
768 という数字は怖いですが、対処が決まるのは 「修正版があるか」 で分けたときです。141 件が手を入れられるもので、残りの 627 件は Debian にまだ修正版がないもの(その中に CRITICAL 7 件 — glib、mbedtls、libxml2、perl-base)です。
直せる 141 件は結局三つのかたまり
| 深刻度 | どこ | 何 | 現在 → 修正版 |
|---|---|---|---|
| CRITICAL | Python | authlib | 1.4.0 → 1.6.12(10 件) |
| HIGH | Python | Pillow | 11.3.0 → 12.3.0(18 件) |
| HIGH | Python | python-multipart | 0.0.20 → 0.0.31(7 件) |
| HIGH | Python | starlette | 0.41.3 → 0.49.1 以上(7 件、fastapi が引き込む) |
| HIGH | OS | util-linux 系 9 個 | 2.41-5 → 2.41.5-0+deb13u1(13 件) |
| HIGH | OS | openssl 系 3 個 | 3.5.6 → 3.5.7(10 件) |
| MEDIUM | Python | pip | 25.0.1 → 26.x(6 件) |
authlib — ログインの署名検証バイパス
まず見るべきは CRITICAL の一件です。CVE-2026-27962、JWS の JWK ヘッダ注入。key=None でトークンを検証すると、ライブラリは トークンの中にある jwk ヘッダの鍵 で署名を確認します。攻撃者が自分の鍵で署名し、その鍵をヘッダに入れれば通ります。1.6.9 で修正されました。
このサイトがその経路を通るか確認しました。authlib を呼ぶ場所は一か所です。
token = await oauth.google.authorize_access_token(request)
starlette クライアントは Google の JWKS から取得した鍵を明示して ID トークンを検証するので、key=None の経路を直接通りはしません。それでもそのライブラリをその版のままにしておく理由はないので上げました。
OS パッケージ — ダイジェスト固定の裏面
util-linux と openssl は Debian に修正版があるのに、なぜイメージになかったのか。Dockerfile がこう始まっていたからです。
FROM python:3.12-slim@sha256:2c941e86… # ダイジェストで固定
RUN apt-get update && apt-get install -y --no-install-recommends fonts-nanum ffmpeg
ダイジェストで固定するとビルドは再現できますが、OS のセキュリティ修正は永遠に入ってきません。 apt-get install は新しいパッケージを入れるだけで、すでにあるものを上げません。二行を変えました — ダイジェストを現在のものに上げ、apt-get upgrade -y を入れて、ビルドのたびにその時点の修正を受け取るようにしました。
直したあとにもう一度測る
同じ Dockerfile でローカルにビルドし、同じ方法で検査し直しました。
| 修正前 | 修正後 | |
|---|---|---|
| 全体(検査ツール除く) | 768 | 640 |
| 修正版があるもの | 141 | 13 |
| うち CRITICAL | 1 | 0 |
| うち HIGH | 53 | 3 |
| util-linux | 2.41-5 | 2.41.5-0+deb13u1 |
| openssl | 3.5.6-1~deb13u2 | 3.5.7-1~deb13u2 |
残る HIGH 3 件は starlette 1.x でしか直らないもので、fastapi のアップグレードと一緒に別途扱います。残る CRITICAL 7 件は Debian にまだ修正がなく、このイメージでは届きません — それは「直した」ではなく「待っている」と書いておくべきものです。
アップグレードが無害だとどうして分かるか
三つのパッケージを上げて lock を作り直すと、他のものも動くことがあります。本番ビルドと同じ python:3.12 + pip-tools 7.5.1 で lock を作り直したところ、変わったのはその三つだけでした。
その lock を --require-hashes でインストールしたコンテナで、全 2,014 件の試験を回しました。4 件が失敗しました。ここで止まれば「アップグレードが何かを壊した」と読めてしまいます。そこで 同じコンテナに元の lock をインストールし、同じ 4 件をもう一度回しました — まったく同じように失敗します。ローカルのスタック(動作中のサービス、フック、採点ツール)が必要な試験でした。対照群がなければ「無関係」という言葉は推測です。
Pillow はメジャー版を一つ飛ばすので使用箇所を別に見ました。Image.open、ImageDraw、ImageFont.truetype、ImageCms、ImageOps — すべて 12.x にそのままあります。
秘密検出:12 件、実際の漏えいは 0 件
Trivy の秘密検出は 12 件を出しました。一つずつ開いて見ました。
| 検出 | 実体 |
|---|---|
| RSA 秘密鍵 2 個 | 演習教材 — gen-tokens.py が生成するデモ鍵(demo-key-id-001) |
| JWT トークン 3 個 | 演習本文の例示トークン |
| Slack Webhook 1 個 | ツール内の例示文字列(curl ビルダーの正規表現) |
12 件すべてが教材か例示です。ただし「演習教材だから大丈夫」という判断は、ファイルを開いて見たあとにしかできません。一覧だけ見て流せば、本物が混ざっていても分かりません。
まとめ
- リポジトリと本番イメージを 両方 検査する。リポジトリだけでは OS が抜け、イメージだけでは直す場所が見えない。
- 検査ツールをコンテナに入れたなら、結果から そのツール自身を引く。 167 件がそれだった。
- 数字に怯えず 「修正版があるか」 で分ける。768 件のうち 141 件、実際の対処は三つのパッケージと Dockerfile 二行。
- ダイジェストで固定したベースイメージには
apt-get upgradeがなければ OS の修正が永遠に入らない。 - アップグレード後に失敗した試験は 元の版で同じ試験をもう一度回して 対照する。
- 秘密検出はファイルを開くまで判断しない。