最近群里聊 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,理由很直接:
| 对比项 | Trivy | Clair | Anchore | Docker 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.2,Docker 由 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 登录后写 crontab:
0 3 * * 0 /volume1/docker/trivy/scan_all.sh >/dev/null 2>&1这里表示每周日凌晨 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-update |
| socket 权限被拒 | 用户不在 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: nginx@sha256:...,但这需要每次升级手动更新 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 --from=build /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 做一次全面体检,肯定不亏。