首页 / 资讯中心 / 文章详情

给NAS里的Docker镜像做安全体检:Trivy部署与扫描实践

给NAS里的Docker镜像做安全体检:Trivy部署与扫描实践 ★ FEATURED ARTICLE
最近群里聊 NAS十句话里有八句离不开 Docker。无论是群晖的 Container Manager还是绿联 UGOS Pro 的应用中心又或者飞牛 fnOS 上的一键安装都能让普通用户在几分钟内把 Nextcloud、Jellyfin、Jellyseerr 这类服务跑起来。但前阵子一波关于恶意镜像、基础镜像漏洞的消息传开不少“NAS佬”开始犯嘀咕自己囤了这么多第三方镜像哪些是干净可用的哪些可能在裸奔这件事光靠慌没用真正能落地的是给 Docker 环境装一个安全扫描器。我最近在自己那台群晖 DS920 上把开源扫描器 Trivy 完整部署了一遍给全屋所有容器做了一次集中“体检”。这篇文章不绕弯子直接讲清楚为什么要扫、选哪个工具、怎么在 NAS 上部署、以后怎么定时扫描以及我踩过的坑。适合手里有一台 NAS、日常用 Docker 装服务的玩家也适合刚开始接触自托管、想知道“镜像拉下来之后还能做点什么”的新手。1. NAS 上的 Docker 镜像到底藏着什么风险先说结论Docker 本身不是洪水猛兽出问题的是我们对“镜像”这件事的默认信任。很多人把“镜像能跑”和“镜像安全”划等号这是目前 NAS 圈子里最大的认知误区。1.1 别把“镜像能跑”当成“镜像安全”一个 Docker 镜像不是单个文件它是一层一层堆出来的完整运行环境。以最常见的 nginx 官方镜像为例底层可能是 Debian中间有 OpenSSL、zlib、libxml2 这一类系统组件上层还有 nginx 的程序文件。你从仓库拉下来的时候这些组件的版本就已经固定了。只要其中任何一个组件被爆出高危漏洞而这个镜像没有重新构建你的容器就带着这个漏洞一直在跑。我见过不少人在 2023 年拉了一个镜像到 2025 年还在用同一个 tag容器也没有重建。问题在于很多镜像仓库的latest标签并不代表“持续更新”它可能只是某一天构建出来的版本后续发布了新镜像老镜像的 tag 还留在那里。更麻烦的是除非你主动重新 pull否则 NAS 上的容器会一直使用当初拉取的那个镜像层。说白了Docker 镜像是一个“快照”不是“实时订阅”它不会自己偷偷打补丁。第三方社区镜像的风险更高。官方镜像一般有维护团队盯安全邮件列表但很多 NAS 玩家常用的下载工具、媒体管理工具、家用仪表盘都是个人开发者或小团队在维护。他们可能很勤快也可能几个月才动弹一次。镜像下载量高只能说明用的人多不能说明它经过了多少安全审计。仓库里偶尔会混入名字相似的仿冒镜像或者被人故意塞进恶意脚本的“野镜像”这些在自托管圈子里不是新闻。1.2 NAS 用户最容易踩的三个坑第一个坑是“只拉不扫只装不管”。容器跑起来之后很多人的安全意识就停留在“服务能打开、页面能显示”这个层面。镜像里的 OpenSSL、curl、log4j 这类组件有没有已知漏洞没人关心。直到某个服务被曝出远程执行漏洞才想起来去检查而这时候往往已经晚了。家庭 NAS 虽然没有企业服务器那么高的暴露面但如果你把端口映射到了公网或者开了路由器的 DMZ 主机风险等级完全不一样。第二个坑是“无脑用 latest”。我之前给群晖装某个内网笔记服务直接写了image: xxx/notes:latest。后来容器一直报版本过低我重新拉取镜像才发现这个项目的维护者把latest指向了完全不同的分支升级之后数据库结构变了差点把笔记数据搞坏。安全角度也一样latest是个移动靶你今天扫是干净的明天再 pull 可能就换了内容。没有固定 tag 或 digest 的部署扫描结果基本没有可追溯性。第三个坑是“镜像里藏敏感信息”。有些教程喜欢让你把数据库密码、API Key 直接写进 Dockerfile 的环境变量甚至打进镜像层。镜像一旦被推送到公共仓库这些信息就永久留在 layers 里删都删不干净。扫描器虽然不是专门查密钥的工具但 Trivy 这类工具连密钥扫描、配置文件扫描也做了你完全可以在部署前先自查一遍。从这些坑能看出来安全扫描器做的不是“保证不出事”而是“把账本摊开给你看”。与其天天赌镜像没问题不如每周花几分钟让工具帮你核对一遍所有已知漏洞和错误配置。2. 扫描器选型为什么我把目光放在 Trivy安全扫描器这个概念听着高端原理其实不复杂。它把镜像里的软件包信息提取出来跟公开漏洞库里的 CVE 条目做版本比对凡是“已安装版本”命中“受影响范围”的就标记成漏洞再按照 CVSS 评分分出严重级别。NAS 上能用这类工具的不止一个关键是怎么选。2.1 不是 Clair / Anchore 不好是它们跟 NAS 场景不匹配我最早考虑过 Clair它是很多企业镜像仓库在用的扫描引擎功能没得说但架构偏重一般需要搭配 PostgreSQL 跑一套服务端还要维护 API 和定时任务。对一台家用 NAS 来说为扫个镜像再常驻一个数据库和一个扫描服务有点杀鸡用牛刀的感觉。Anchore 也是老牌工具对企业级 CI/CD 支持很完整但它本身的部署方式偏向 Kubernetes 或大规模容器平台对群晖、绿联这种自带 Docker 套件的 NAS 并不友好。配置起来复杂度高日常维护成本也高。Docker Scout 是 Docker 官方出的跟 Docker Hub 绑定得比较深界面直观但在 NAS 场景下有明显短板需要 Docker Hub 账号私人镜像和 ghcr.io 这类第三方仓库的镜像支持不够灵活而且不容易在命令行里做定时任务。对喜欢“脚本一把梭”的 NAS 玩家来说反而不顺手。对比之后我选了 Trivy理由很直接对比项TrivyClairAnchoreDocker Scout部署成本一个 docker run 就能扫需要 PostgreSQL 服务端组件较多配置重依赖 Docker Hub 账号NAS 友好度支持本地 socket 直接扫偏向仓库级接入偏向集群/CI偏向 Docker Hub扫描对象镜像、目录、SBOM、IaC、密钥镜像镜像、CI 流水线镜像漏洞库覆盖OS 包 语言依赖 SBOM主要 OS 包OS 包OS 语言 Go定时任务/离线报告命令行友好适合 cron需要 API 调用需要 API 调用可视化为主Trivy 是单个开源二进制也能直接以官方镜像方式运行不需要常驻服务扫完就跑用完就退非常适合 NAS 这种喜欢“轻量常驻”的环境。它的漏洞库更新频率很高能覆盖 Debian、Ubuntu、Alpine、CentOS 这类系统包也能扫 Python、Node.js、Go、Java 等语言依赖。对我这种家里同时跑着十几个不同技术栈容器的人来说一个工具全包了。2.2 Trivy 能扫什么镜像、文件系统、SBOM 与密钥很多人以为 Trivy 只能扫容器镜像其实它更像一个“安全体检工具箱”。最常见的用法是扫镜像也就是把本地或远端镜像拉下来比对其中的软件包版本其次它能扫文件系统比如你把某个应用的配置目录挂载到宿主机Trivy 可以直接扫这个目录里的依赖和配置它还能生成 SBOM也就是软件物料清单把镜像到底用了哪些组件列成一张表方便追踪。这些能力对 NAS 用户都很实用。我举个具体场景家里跑着一个基于 Node.js 的自动化工具它的源码挂在 NAS 共享目录里没有打镜像。我可以用trivy fs /volume1/docker/myapp直接扫描这个目录里的 package.json 和 node_modules看看依赖有没有已知漏洞不需要为了扫描而专门构建一个镜像。另一个场景是检查 Dockerfile 本身Trivy 的 misconfig 检测能发现类似“容器用了 privileged 权限”“暴露了不必要端口”这类配置问题。它的密钥扫描也值得一提。镜像里一旦出现看起来像 AWS Access Key、GitHub Token、私钥片段的内容Trivy 会标出来。我遇到过某个第三方镜像把数据库口令写死在配置文件里就是靠这个功能发现的。别看功能“小”对家庭环境来说有时候比一个高危 CVE 更致命。3. 在 NAS 上部署 Trivy 并完成一次全面体检讲完选型下面是真正能动手的部分。我的测试环境是群晖 DS920系统 DSM 7.2.2Docker 由 Container Manager 接管。绿联 UGOS Pro 和飞牛 fnOS 的操作路径会略有不同但下面这些命令是通用的都基于 Linux 和 Docker 环境。3.1 部署前的准备与目录规划部署之前先明确一个原则Trivy 以一次性容器任务的方式运行不会长期占一块内存也不需要常驻进程。每次扫描时拉起来扫完退出跟用计算器一样要用才开。这样对 NAS 的负载影响最小。第一步确认你能通过 SSH 登录 NAS。群晖需要在“控制面板 - 终端机和 SNMP”里开启 SSH 功能然后使用管理员账号登录。绿联和飞牛通常在系统设置里也有类似选项。登录后确认 docker 命令可用docker version如果提示权限不足需要把当前用户加入 docker 组或者直接用 root 账号执行。群晖的 admin 账号默认有权限普通用户不一定。第二步规划目录。我习惯把所有跟 Docker 管理相关的文件放在统一目录下这样既方便备份也方便后续写脚本。这里我建议至少建两个目录一个放缓存一个放扫描报告mkdir -p /volume1/docker/trivy/cache mkdir -p /volume1/docker/trivy/reportcache目录用于存放 Trivy 的漏洞库缓存。Trivy 第一次扫描时需要从官方仓库拉取一个数据库文件里面是漏洞元数据体积不小而且每次更新会增量下载。如果不挂载缓存目录容器每次退出后缓存就丢了下一次扫描还得重新下载白白浪费时间。第三步拉取 Trivy 官方镜像docker pull aquasec/trivy:latest这里有两个细节。第一务必使用aquasec/trivy这个官方镜像不要随便用第三方转存版本。扫描安全工具的镜像本身都不干净那就太讽刺了。第二如果 NAS 联网环境一般首次拉取可能会比较慢耐心等一下就好后续扫描用的是本地缓存不依赖每次联网拉镜像。3.2 亲手跑一遍扫描当前全部容器镜像部署收尾后先扫一个大目标试试手比如扫描所有正在运行的容器镜像。这里要用到 Docker socket也就是把宿主机的/var/run/docker.sock挂载给 Trivy 容器让它直接跟 Docker 守护进程通信读取本地镜像列表和镜像元数据。docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /volume1/docker/trivy/cache:/root/.cache \ aquasec/trivy:latest image \ --severity HIGH,CRITICAL \ --no-progress \ --format table \ $(docker ps --format {{.Image}} | sort -u)这条命令我拆开解释一下--rm表示扫描完成后立刻删除容器省得 NAS 上留一堆退出状态的容器。-v /var/run/docker.sock:/var/run/docker.sock是权限通道挂载给 Trivy 让它扫描宿主机上的镜像。-v /volume1/docker/trivy/cache:/root/.cache是缓存持久化避免每次重新下载数据库。最后那段$(docker ps --format {{.Image}} | sort -u)会收集当前正在运行的镜像列表去掉重复项作为扫描目标传递进去。如果你还想扫描本地已经拉取但没运行的镜像可以把目标换成全部本地镜像docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /volume1/docker/trivy/cache:/root/.cache \ aquasec/trivy:latest image \ --severity HIGH,CRITICAL \ --no-progress \ --format table \ $(docker images --format {{.Repository}}:{{.Tag}} | grep -v none)注意我加了一个grep -v none因为构建缓存或历史镜像里经常出现没有标签的悬空镜像这些不扫也罢扫了反而容易混淆。第一次扫描通常会慢一些因为要下载数据库并且逐层解包镜像。我扫家里 12 个常用镜像第一次跑了大约 15 分钟之后再来就快多了几分钟就能出结果。3.3 结果怎么看严重级别、修复版本与“误报”扫描完会看到一张表格每一行代表一个漏洞关键信息是“库名、已安装版本、修复版本、严重级别”。严重级别按 CVSS 分为 CRITICAL、HIGH、MEDIUM、LOW、UNKNOWN 几档我们日常重点关注 HIGH 和 CRITICAL。有个概念必须先讲扫描结果里出现漏洞不代表你的服务“马上就要被黑”它只说明镜像里的组件版本命中了已知漏洞库。比如一个内网访问的 RSS 阅读器镜像里有个中危漏洞但攻击者需要先拿到内网权限才能利用实际风险就很低。反过来说如果你把带高危漏洞的服务映射到了公网那性质就完全不同了。看结果时重点看“Fixed Version”这一列。如果修复版本不为空说明上游已经修了你应该尽快升级镜像或更新组件如果为空可能表示上游还没有修复或者该版本已经停止维护。对停止维护的旧镜像更推荐直接换新版本镜像而不是在一个没人维护的版本上继续缝缝补补。还要做好心理准备扫描结果里一定会有误报和“不可修复”项。比如某些镜像用了多阶段构建运行镜像里根本没有编译工具链但扫描器从历史层里识别出了旧文件也会报一条。这时候不必惊慌处理原则是“先看影响组件再看是否在运行层存在”。我见过一个 Dockerfile 在构建阶段临时安装了 git后面删了但镜像层还在扫描器就报出了一个 git 漏洞。实际上运行容器里根本没有 git 进程这个漏洞就是“看得见、用不上”。看完结果修复思路一般有三条路。第一条是升级官方镜像把 tag 从nginx:1.26改成nginx:1.28或更新版本第二条是重新构建自己的镜像基础镜像换成更新的alpine或debian:bookworm-slim第三条是用最小化镜像减少攻击面。比如把 Runtime 镜像从python:3.12换成python:3.12-alpine或者干脆用多阶段构建把编译工具留在构建阶段运行镜像只保留程序文件和必要的运行库。3.4 定时扫描让 NAS 每周自动体检人工扫描一次不难难的是坚持。我一开始也是心血来潮扫一次过了两周又忘了直到后来看到一个镜像仓库的公告说某个老版本存在严重漏洞我才想起来自己跑的是同一个版本。从那之后我就把扫描做成了定时任务每周日凌晨自动跑起床后看一眼邮件或者日志就够了。在群晖上设置定时任务很简单打开“控制面板 - 任务计划 - 新增 - 用户自定义脚本”把下面的脚本内容填进去。这里我写一个完整版本扫描当前运行镜像并把结果追加到日志文件同时生成一份 JSON 报告方便以后对比。#!/bin/sh REPORT_DIR/volume1/docker/trivy/report LOG_FILE/volume1/docker/trivy/scan.log DATE$(date %Y%m%d-%H%M) docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /volume1/docker/trivy/cache:/root/.cache \ -v $REPORT_DIR:/report \ aquasec/trivy:latest image \ --severity HIGH,CRITICAL \ --no-progress \ --format json \ --output /report/$DATE.json \ $(docker ps --format {{.Image}} | sort -u) echo $DATE 扫描完成 $LOG_FILE需要说明的是我在脚本里故意没有加--exit-code 1。默认情况下Trivy 扫描出不安全项并不会让进程非零退出方便定时任务继续往下走。如果你希望“发现高危就报警”可以把--exit-code 1加进去然后用后续逻辑判断退出码。但家用场景我推荐先别加避免因为误报把任务计划卡死。绿联 UGOS Pro 和飞牛 fnOS 的玩法类似。绿联可以在“控制面板 - 任务计划”里添加脚本飞牛可以直接用 SSH 登录后写 crontab0 3 * * 0 /volume1/docker/trivy/scan_all.sh /dev/null 21这里表示每周日凌晨 3 点执行脚本。凌晨扫的好处是大部分服务使用率低镜像扫描和网络下载不会影响家里人看视频或者备份照片。4. 常见问题排查与扫描之外的加固部署过程看着顺实操中总会碰到各种问题。下面这些是我自己踩过、或者在朋友机器上帮忙排查过的坑按出现频率从高到低列出来。4.1 高频报错与解决实录第一个高频报错是数据库下载失败或者卡住。Trivy 默认要从官方容器仓库拉取漏洞数据库不同宽带环境下访问仓库的速度差别很大。第一次扫描如果卡在“Downloading the vulnerability database”这一行不要急着重试先看看是不是网络波动的问题。解决办法是预先下载好数据库再扫描。Trivy 提供了手动下载命令docker run --rm \ -v /volume1/docker/trivy/cache:/root/.cache \ aquasec/trivy:latest image --download-db-only这条命令会把漏洞数据库拉到本地缓存目录。之后扫描的时候加上--skip-db-update跳过更新速度立刻快一大截。定时任务里同样可以先用--download-db-only预热再在扫描时选择跳过更新把网络依赖降到最低。第二个问题是权限不足。日志里出现/var/run/docker.sock: permission denied多半是当前用户没有 docker 组权限。解决方案是把用户加进 docker 组或者用 sudo / root 执行。需要提醒挂载 docker socket 本身就是高权限操作一定要确保镜像来源可信。Trivy 官方镜像可以信任第三方来源的镜像不要随便给它挂 socket。第三个问题是扫描结果把UNKNOWN级别也列出来了看起来一堆数据很吓人。UNKNOWN一般表示漏洞库里有这个 CVE但缺少足够的 CVSS 评分信息不代表实际危害低也不代表高。处理方式很简单命令里只保留要关心的级别比如--severity CRITICAL,HIGH其他级别一律不展示。第四个问题是镜像太多导致扫描时间过长。我见过有人把自己 NAS 里的镜像全部扫一遍缓存目录高达好几个 GB加上网络数据库更新一次要跑半小时。这种情况可以把扫描范围缩小到“正在运行的镜像”或者按容器分组扫描比如先扫jellyfin*再扫nextcloud*避免一次拉全家。另外一个技巧是清理旧镜像和悬空镜像删除之后扫描目标和缓存都会小很多。下面整理一张速查表方便以后直接翻问题常见原因处理办法数据库下载失败/卡住网络波动或仓库访问慢先手动--download-db-only再扫描加--skip-db-updatesocket 权限被拒用户不在 docker 组sudo usermod -aG docker $USER重新登录扫描时间过长本地镜像太多只扫运行中的镜像先清理悬空镜像UNKNOWN 一堆漏洞库缺评分只显示 HIGH/CRITICAL误报太多镜像层包含构建期文件看详情确认漏洞组件是否在运行层4.2 扫描之外我坚持的几个安全习惯扫描器只能帮你发现已知问题真正降低风险还得靠日常习惯。我不是安全专家只是自托管玩得比较多总结下来这几个动作最有效。第一固定镜像版本不用latest做生产部署。我现在的 compose 文件里写的是image: nginx:1.28.2或者image: ghcr.io/home-assistant/home-assistant:stable这种明确 tag。宁可升级的时候多改一行也不愿某天被一个未知的新版本替换掉。更极致的是固定 digest在 compose 里写成image: nginxsha256:...但这需要每次升级手动更新 digest稍微麻烦适合追求稳定的人。第二非必要不挂/var/run/docker.sock。很多 NAS 教程为了让容器能直接控制宿主机 Docker会教你把 socket 挂进去这是很高风险的操作。一旦你的容器被攻破攻击者就能通过 Docker API 创建特权容器直接拿到宿主机控制权。我的原则是只有对 Trivy、Watchtower 这类官方管理工具才挂 socket其他应用一律不碰。第三端口映射宁少勿多。家庭局域网内服务完全可以用容器 IP 或者 host 网络访问没必要把所有端口都映射到0.0.0.0。如果某个服务确实需要公网访问尽量用 NAS 自带的防火墙或者路由器端口转发只开放必要端口同时限制来源 IP。第四敏感信息不要进镜像层。数据库密码、API Token、SMTP 口令这些全部放在.env文件里compose 里用${VAR}引用。Trivy 的密钥扫描可以作为一层检查但别指望它兜底。5. 扫描不是终点把安全变成 NAS 的日常习惯工具装完了定时任务也跑起来了但说实话真正的门槛不在部署这几步而在能不能把这个动作坚持下去。我见过太多人扫了一次看到一片红绿表格截图发完群然后就没有然后了。下一次再捡起来要么是某个服务真出问题要么是又看到一条耸人听闻的消息。5.1 用“最小镜像 多阶段构建”减少暴露面有了扫描结果之后你会发现自己魔改的镜像往往是重灾区。原因很简单基础镜像太肥。很多人写 Dockerfile 时直接FROM python:3.12里面带着编译工具、开发头文件、一堆用不到的包管理器缓存。攻击面大扫描出来的漏洞自然多。我建议能改成多阶段构建就改。以一个 Node.js 应用为例FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci FROM node:22-alpine WORKDIR /app COPY --frombuild /app/node_modules ./node_modules COPY . . USER node CMD [node, server.js]运行阶段不装npm不保留package-lock.json里的开发依赖镜像体积能小很多扫描出来的依赖漏洞也会大幅减少。对 NAS 上自己写的小工具这种改动花不了多少时间长期收益非常明显。5.2 建立“更新-重扫-对比”的闭环我现在养成一个习惯每次升级某个镜像或重建某个容器之后不再直接点“完成”而是顺手跑一次 Trivy把新旧结果对比一下。升级后如果高危漏洞数量降了说明这次更新有意义如果升完反而冒出新漏洞那就重新评估这个新版本值不值得上。这个方法尤其适合那些喜欢用 Watchtower 自动更新的朋友。Watchtower 自动拉新镜像并重建容器表面上看很方便但它不会告诉你新镜像里有什么新漏洞。我个人的做法是对官方活跃项目可以开自动更新对第三方小众镜像全部手动升级手动升级之后立刻扫描。这样既能吃到新版本修复又不会盲目信任“新”这个字。最后分享一个体会安全这件事在 NAS 上永远不会“完全搞定”。你装了一个扫描器扫出漏洞修了一轮然后新的 CVE 又会发布镜像也会有新版本。与其追求一个绝对安全的结果不如把“定期体检”当成和清理磁盘空间、备份数据一样的日常任务。Trivy 不会让你的 NAS 变成铜墙铁壁但它至少能让你知道家里的那台 7x24 小时开机的设备现在到底带着哪些伤在跑。今晚睡前给你的 Docker 做一次全面体检肯定不亏。
阅读完成 · 觉得有帮助?
咨询建站