LabHub
博客

博客

用 Trivy 扫描生产镜像 — 768 条结果里真正要动手的只有三个包和 Dockerfile 的一行

한국어English日本語中文

为什么要扫描

这个站点由一个 Python 后端承担登录、博客、语言学习和实验评分。依赖在 requirements.lock 里连哈希一起固定,"里面有什么"很清楚,但"里面有没有已知漏洞"从未测过。我用 Trivy 扫了两个对象。

第二个是关键。只看仓库会漏掉操作系统包,只看镜像又看不出该在哪里修。

在生产镜像内部扫描的方法

镜像仓库(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 程序)的依赖。扫描工具在扫描对象里面,于是把自己也数了。12 条 CRITICAL 里有 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 还没有修复版本(其中包括 7 条 CRITICAL——glib、mbedtls、libxml2、perl-base)。

能修的 141 条归根结底是三组

严重度位置内容现在 → 修复版本
CRITICALPythonauthlib1.4.0 → 1.6.12(10 条)
HIGHPythonPillow11.3.0 → 12.3.0(18 条)
HIGHPythonpython-multipart0.0.20 → 0.0.31(7 条)
HIGHPythonstarlette0.41.3 → 0.49.1 以上(7 条,由 fastapi 引入)
HIGH操作系统util-linux 系列 9 个2.41-5 → 2.41.5-0+deb13u1(13 条)
HIGH操作系统openssl 系列 3 个3.5.6 → 3.5.7(10 条)
MEDIUMPythonpip25.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 的路径。但也没有理由把这个库留在那个版本,于是升级了。

操作系统包 — 摘要固定的另一面

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

用摘要固定可以让构建可复现,但 操作系统的安全修复永远进不来。 apt-get install 只添加新包,不升级已有的包。改了两行——把摘要升到当前版本,加入 apt-get upgrade -y,让每次构建都拿到当时可用的修复。

修完再测一次

用同一个 Dockerfile 在本地构建,用同样的方法再扫一遍。

修复前修复后
总计(去掉扫描工具)768640
有修复版本的14113
其中 CRITICAL10
其中 HIGH533
util-linux2.41-52.41.5-0+deb13u1
openssl3.5.6-1~deb13u23.5.7-1~deb13u2

剩下的 3 条 HIGH 只有 starlette 1.x 才修,与 fastapi 升级一起另行处理。剩下的 7 条 CRITICAL Debian 尚无修复,这个镜像够不到——那应该记作"等待",而不是"已修复"。

怎么知道升级是无害的

升三个包并重新生成 lock,其他东西也可能跟着动。用与生产构建相同的 python:3.12 + pip-tools 7.5.1 重新生成 lock,变化的只有这三个包

在用 --require-hashes 安装了该 lock 的容器里跑了全部 2,014 个测试,4 个失败。停在这里会被读成"升级弄坏了什么"。于是 在同一个容器里安装原来的 lock,再跑同样的 4 个——同样失败。它们是需要本地环境(运行中的服务、钩子、评分工具)的测试。没有对照组,"无关"只是猜测。

Pillow 跨了一个大版本,所以单独看了调用处:Image.openImageDrawImageFont.truetypeImageCmsImageOps——在 12.x 里全都还在。

密钥检测:12 条,真实泄露 0 条

Trivy 的密钥检测报了 12 条。我逐条打开看了。

检测到的实际是什么
RSA 私钥 2 个实验教材——gen-tokens.py 生成的演示密钥(demo-key-id-001
JWT 令牌 3 个实验正文里的示例令牌
Slack Webhook 1 个工具里的示例字符串(curl 生成器的正则)

12 条全是教材或示例。但"是实验材料所以没事"这个判断,只有打开文件之后才能做。只看列表就放过,真的混在里面也不会知道。

总结

登录后即可点赞

评论

还没有评论。

登录后即可发表评论