最近群里好多折腾 NAS 的朋友都在转发一句话:Docker 也不安全了。说实话,看到这个标题第一反应是有点想笑,仔细一想又确实笑不出来。我自己是从黑群晖一路玩到自组 DIY NAS 的,七年间踩过不少坑,也亲眼见过有人因为一个来路不明的镜像,整台 NAS 被挖矿程序占满 CPU 的惨状。Docker 本身不是洪水猛兽,真正的问题是我们太习惯"拉镜像、跑容器、完事大吉"这种流程,很少有人会停下来问一句:我拉进来的这个镜像,里面到底装了什么?
今天这篇想跟你聊聊的,就是怎么给 NAS 上的 Docker 补上一道"安检门"——部署一个安全扫描器,把镜像里的漏洞、风险配置、可疑文件都翻出来晒晒太阳。我会以目前社区里用得最多的 Trivy 为主线,配合 Docker Bench Security 做宿主侧检查,完整走一遍从选型、部署到出报告、修漏洞的流程。这篇文章适合所有在用群晖、绿联、飞牛或者自组 Linux NAS 的朋友,尤其是那种 NAS 上跑着十几个容器、镜像来源五花八门的老哥——你越觉得自己"稳得很",越该花半小时把这道安检门装上。
1. 先说结论:Docker 到底哪里不安全了
1.1 镜像不是你想象的那种"干净罐头"
你得先理解一个反直觉的事实:Docker 镜像本质上不是一份文件,而是一堆只读层的堆叠。每一层都来自 Dockerfile 里的一条指令,而 Dockerfile 的每一行都可能引入了你根本不知道的东西。更扎心的是,很多人习惯直接从 Docker Hub 拉镜像,但 Docker Hub 上的镜像质量参差不齐——官方维护的镜像只是一小部分,大量镜像来自个人开发者或者自动构建脚本,你没法保证里面没有捆绑额外的二进制文件、定时任务脚本,甚至是留了后门的"加料版"软件。
我之前在同事的 NAS 上做过一次扫描实验,他跑了十几个容器,结果在三个镜像里发现了高危漏洞,其中一个 Elasticsearch 镜像的漏洞 CVSS 评分高达 9.8,可以直接导致远程代码执行。他当时的反应是"我用了两年了都没事"——这恰恰是最典型的侥幸心理。漏洞不是"一定出事",而是"随时可能出事",尤其是 NAS 这种常年挂在局域网甚至公网上的设备,暴露面比你的桌面电脑大得多。
1.2 NAS 的安全处境比普通服务器更尴尬
NAS 的特殊性在于它的定位:既要当文件服务器,又要跑各种服务,还要常年在线。很多人还会做端口映射,让外网也能访问 NAS 上的某些服务。这就意味着你的 Docker 容器直接面对的是一个不设防的开放网络。再加上不少 NAS 系统本身的包管理滞后,系统组件存在已知漏洞但迟迟没法升级,等于说"地基"已经不牢了,还在上面盖了一堆 Docker 容器。
另一个常被忽略的点是权限放大。很多人在 NAS 上部署容器时,图省事直接用了--privileged或者把宿主机的目录、甚至 Docker socket 挂载进了容器。一旦容器里的应用被攻破,攻击者就能顺着这个挂载点摸到宿主机,这就是标准的容器逃逸路径。安全扫描器能帮你把这类风险配置也揪出来,而不只是盯着镜像里的软件漏洞。
1.3 安全扫描器到底能干什么
说白了,扫描器干三件事:一是比对镜像里的软件包版本与漏洞数据库,列出已知 CVE;二是检查 Dockerfile 和部署配置里的危险写法;三是扫描宿主机 Docker 运行时的配置是否安全。有了它,你就能在把镜像部署上去之前先"体检"一遍,也能定期给已经在跑的容器做巡检,发现问题后及时换基础镜像、升级依赖、调整配置。
2. 扫描器选型:别盲目上企业级方案,NAS 资源经不起折腾
2.1 主流扫描器横评
市面上的容器安全扫描器不少,我帮你把常见的几个捋一遍,再说说为什么我最后选了 Trivy 作为主力。
| 扫描器 | 维护方 | 部署难度 | 资源占用 | 适用场景 |
|---|---|---|---|---|
| Trivy | Aqua Security | 低 | 低 | 个人、小团队首选,支持镜像/文件系统/仓库/配置扫描 |
| Grype | Anchore | 低 | 低 | Trivy 的替代品,偏向 CLI 使用 |
| Clair | Open Source | 高 | 高 | 需要 PostgreSQL,适合大规模私有镜像仓库 |
| Anchore Engine | Anchore | 中 | 中 | 企业级策略引擎,偏重合规 |
| Docker Bench Security | Docker | 低 | 极低 | 只检查宿主机配置,不扫镜像漏洞 |
Clair 虽然功能强大,但它依赖 PostgreSQL 数据库,还要维护一套 API 服务,在群晖这种内存只有几个 G 的小机器上跑起来压力不小,杀鸡用牛刀了。Anchore Engine 更适合有合规需求的企业团队,个人 NAS 用户玩不转也没必要。Grype 和 Trivy 定位类似,但 Trivy 的漏洞库更新频率更快,支持的扫描目标更全,社区资料也多,出问题容易搜到答案。
2.2 为什么 Trivy 适合 NAS 场景
Trivy 有几个让我觉得"这就是为 NAS 用户准备的"特质:
- 单二进制文件:官方提供 ARM64 架构的静态编译版本,直接下载就能用,不依赖运行时。
- 多架构支持:在 x86_64 和 ARM64 的 NAS 上都能跑,像绿联、极空间这种 ARM 平台的机器也不用发愁。
- 缓存设计合理:漏洞库会缓存在本地,第二次扫描就快得多。
- 扫描速度快:在资源受限的 NAS 上扫一个小镜像,通常几十秒内能出结果。
2.3 我的最终方案:Trivy + Docker Bench Security
我现在的组合是:用 Trivy 定期扫描所有本地镜像和运行中的容器,用 Docker Bench Security 做宿主机的基线检查。前者管"软件层面",后者管"配置层面",两个配合起来基本上覆盖了一个普通 NAS 用户能遇到的大部分 Docker 安全隐患。
Docker Bench Security 是一个 Shell 脚本,直接拉取官方镜像跑一次就好,它会按照 CIS Docker Benchmark 的几百条检查项逐条核对你的 Docker 配置,最后生成一份报告。它不扫镜像漏洞,但能告诉你是不是用了特权模式、有没有把 socket 暴露给容器、网络是否隔离等。这两个工具一轻一重,正好互补。
3. 在 NAS 上部署 Trivy:我踩过的坑全写在这里
3.1 三种部署方式怎么选
先说结论:如果你用的是群晖、绿联这类带图形界面的 NAS 系统,最省心的方式是直接装 Trivy 的二进制版本;如果你喜欢统一管理,用 Docker 容器方式跑也不差;但我不建议在 NAS 上用brew或者源码编译的方式装,依赖折腾起来太烦,而且升级容易出问题。
二进制安装的优点是零依赖、启动快、不占额外容器资源。缺点是没法通过 Docker 的方式做版本管理,需要手动下载替换。容器方式的优点是可以写进 docker-compose 统一管理,配合 NAS 上的定时任务就能自动扫描,缺点是需要单独留一个容器的资源开销。我个人倾向在 NAS 上用二进制方式,因为 NAS 本身就资源紧张,能省一点是一点。
3.2 群晖 DSM 环境实操
群晖从 DSM 7.2 开始内置了 Container Manager,你可以直接用 SSH 登录后台操作。先到 Trivy 的 GitHub Releases 页面下载对应架构的压缩包。群晖的 CPU 大部分是 Intel x86_64,小部分老机型是 ARM,下载前先确认一下。
# 进入临时目录,下载并解压 cd /tmp wget https://github.com/aquasecurity/trivy/releases/download/v0.56.1/trivy_0.56.1_Linux-64bit.tar.gz tar zxvf trivy_0.56.1_Linux-64bit.tar.gz # 把二进制移动到系统 PATH 中 sudo mv trivy /usr/local/bin/ sudo chmod +x /usr/local/bin/trivy # 验证安装 trivy --version这里有个容易踩的坑:很多 NAS 系统默认的 PATH 里没有/usr/local/bin,你装了之后执行trivy可能会提示命令找不到。两个解决办法:要么在/etc/profile里加上export PATH=$PATH:/usr/local/bin,要么每次用绝对路径/usr/local/bin/trivy调用。我建议直接改 profile 一次到位,毕竟后面还要配合定时任务用。
3.3 绿联 UGOS、飞牛 fnOS 和 DIY Linux NAS
绿联 UGOS Pro 和飞牛 fnOS 本质上是基于 Linux 的定制系统,操作方式和群晖大同小异。需要注意的一点是,这两个系统默认可能没有内置wget或者curl,先装一下基础工具再下载。飞牛 fnOS 还能直接用docker compose来跑 Trivy 容器,对我来说反而是最顺手的。
如果是自组 Linux NAS,比如用 Ubuntu Server 或者 Debian 当底座的,直接装官方二进制或者用 Docker 容器跑都行。我个人的建议是用 Docker 容器来扫其他镜像,但在扫 Docker socket 时要格外小心——扫描容器本身不能拿到宿主机的完全控制权,后面我会专门讲这个坑。
3.4 Docker 容器方式:用 docker-compose 统一管理
如果你选容器方式,下面这个 compose 文件可以直接抄。要注意的是挂载/var/run/docker.sock是必要的,否则 Trivy 没法直接扫描 Docker 守护进程管理的镜像。
version: "3.8" services: trivy: image: aquasec/trivy:latest container_name: trivy restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock - trivy-cache:/root/.cache - ./reports:/reports environment: - TRIVY_NO_PROGRESS=true command: image --exit-code 0 --severity HIGH,CRITICAL volumes: trivy-cache:这个配置的关键在于:漏洞数据库缓存放到了 Docker volume 里,这样每次重新创建容器不用重新下载数据库;报告输出目录挂载到了宿主机,方便直接查看结果。TRIVY_NO_PROGRESS=true能让日志输出更清爽,避免定时任务跑的时候产生大量无意义输出。
4. 实操扫描全流程:从第一次扫描到读懂报告
4.1 首次扫描:先给本地镜像做个体检
装好 Trivy 之后,第一件事是扫描本地已有的镜像。扫描前先更新漏洞库,这是很多新手漏掉的一步——Trivy 内置的漏洞库需要联网下载,不更新的话扫描结果就是过期的。
# 更新漏洞数据库 trivy image --update-cache # 扫描本地全部镜像 trivy image --severity HIGH,CRITICAL --no-progress $(docker images --format "{{.Repository}}:{{.Tag}}")上面的命令会遍历你机器上所有镜像,只显示高危和严重两个级别的漏洞。第一次跑的时候你会发现有些镜像要扫很长时间,这是因为要先获取镜像的层信息、再逐一匹配漏洞库。之后的再扫描因为有了缓存,速度会快很多。
4.2 扫描结果怎么读
Trivy 的输出是一个表格,按漏洞库来源分组,每条漏洞包含:漏洞编号(CVE)、影响软件包、当前版本、修复版本、严重程度以及参考链接。对普通用户来说,重点看两个东西:严重程度和有没有修复版本。
如果一条漏洞显示有修复版本,处理方式很简单——更新对应软件包,或者直接换一个打了补丁的基础镜像。如果显示"未修复",那意味着漏洞库中还没有官方修复方案,这种时候就要评估:这个容器是不是暴露在公网?是否真的需要运行这个服务?如果必须运行,就要做好网络隔离和访问控制。
我第一次扫自己的 NAS 时,扫描结果很扎心:一个跑了快一年的 Nginx 镜像带了好几个高危漏洞,原因是镜像拉取时间太早,后面的安全补丁根本没吃到。这就是典型的"镜像拉一次就不管了"的教训。
4.3 输出的三种格式:table、json、html
Trivy 支持多种输出格式。命令行看结果用默认的 table 就行,但如果想保留历史记录或者可视化,建议用 json 格式保存,再用工具转成 HTML 报告。
# 输出 JSON 报告 trivy image --format json --output /reports/nginx-report.json nginx:latest # 不过 JSON 文件可读性差,我一般直接生成 HTML trivy image --format html --output /reports/nginx-report.html nginx:latestHTML 报告的样式是内置的,打开浏览器就能看到按严重程度排序的漏洞列表,比在终端里看表格舒服得多。我一般把这些报告集中放在一个目录里,定时任务跑完后统一归档,每个月翻一次。
4.4 定时扫描:让安全巡检自动化
手动扫描只能管一时,真正的安全感来自定期巡检。在 NAS 上最方便的方式就是 cron。我就是靠 cron 每周日凌晨自动扫一次所有镜像,然后把报告存下来。
# 编辑定时任务 crontab -e # 每周日凌晨 3 点执行扫描,并保存 JSON 报告 0 3 * * 0 /usr/local/bin/trivy image --severity HIGH,CRITICAL --format json --output /volume1/docker/reports/$(date +\%Y\%m\%d).json $(docker images --format "{{.Repository}}:{{.Tag}}") >> /volume1/docker/reports/scan.log 2>&1群里不少朋友说定时任务下发之后不执行,后来发现是 NAS 系统里date命令的格式问题。date的百分号在 cron 里需要转义,写成\%Y\%m\%d才不会出错。另外记得给 crond 开启权限,群晖有些型号默认 cron 服务是关闭的,需要到计划任务里面新建一个用户自定义脚本,或者手动把 crond 启用起来。
4.5 Docker Bench Security:给宿主机做一次"体检"
镜像漏洞扫描解决的是"容器里装了什么",但宿主机本身的 Docker 配置是否安全,需要 Docker Bench Security 来查。它跟 Trivy 是互补关系,两者缺一不可。
# 运行 Docker Bench Security docker run -it --net host --pid host --cap-add audit_control \ -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \ -v /var/lib:/var/lib \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /usr/lib/systemd/system:/usr/lib/systemd/system \ -v /etc:/etc --label docker_bench_security \ docker/docker-bench-securityDocker Bench Security 会输出一大段检查结果,每条前面带有[PASS]、[WARN]、[FAIL]或[INFO]标记。我们的目标是让那些重大安全项尽量不出现[FAIL],尤其是涉及特权容器、宿主机目录挂载、docker socket 访问这几项。如果你的容器有合理理由使用特权模式,可以在报告里标注说明,但要明确知道其中的风险。
5. 常见问题与排查技巧实录
5.1 扫描速度太慢怎么办
Trivy 第一次扫描慢很正常,因为要下载漏洞库、解包镜像层做比对。但如果你发现每次扫描都很慢,八成是缓存出了问题。Trivy 的默认缓存目录是~/.cache/trivy,里面有一份db目录存放漏洞库数据。如果 NAS 重启后缓存目录丢失或者权限不对,Trivy 就会重新下载数据库,自然就慢了。
我在群晖上就遇到过这个问题。后来我把缓存目录迁移到了存储空间较大的卷上,然后在环境变量里指定TRIVY_CACHE_DIR=/volume1/docker/trivy-cache,问题就解决了。如果容器方式部署,记得把缓存挂载成 volume,否则容器重建一次就白下载一次。
5.2 漏洞库下载失败
Trivy 启动时需要从 GitHub Releases 下载漏洞库,国内网络环境经常会出现连接超时或者下载中断。这个问题可以通过配置代理解决,但注意别用违规的代理服务。我这边实测下来最靠谱的办法是提前手动下载漏洞库的离线包,然后放到本地目录让 Trivy 直接使用离线数据。
# 手动下载漏洞库离线包(在能正常访问 GitHub 的机器上操作) wget https://github.com/aquasecurity/trivy-db/archive/refs/heads/main.zip unzip main.zip -d /path/to/trivy-db # 然后在 NAS 上指定本地的数据库目录 export TRIVY_DB_DIR=/volume1/docker/trivy-db不过这个办法有一个坑:手动下载的漏洞库可能不是最新版本,扫描结果会有滞后。我的建议是每周手动更新一次离线包,这样既不依赖网络环境,又能保持漏洞库相对新鲜。
5.3 误报与漏报:别把扫描结果当圣旨
Trivy 的原理是基于软件包版本比对漏洞库,所以它只能发现那些已经进入 CVE 数据库、并且能通过版本号匹配的漏洞。这里就存在两类问题:一类是误报——你的软件包虽然版本号命中漏洞描述,但实际代码路径可能不受影响;另一类是漏报——编译型语言的二进制漏洞,或者私有定制软件里的问题,Trivy 根本扫不出来。
所以我的经验是:把 Trivy 当成"最低保障",而不是"完全保险"。扫描结果里 HIGH 和 CRITICAL 级别的漏洞,只要显示有修复版本,就尽快升级;显示"未修复"的,先判断这个服务是否暴露在公网,再做隔离处理。至于那些 LOW、MEDIUM 级别的,真的可以先放一放,毕竟 NAS 上要修的优先级太多了。
5.4 Docker socket 挂载的安全悖论
用 Trivy 容器扫描其他镜像时,必须挂载/var/run/docker.sock,但这一挂载本身就是个安全隐患。相当于你给了 Trivy 容器操作 Docker 守护进程的权限,如果 Trivy 容器本身被攻破,攻击者就能控制宿主机上的所有容器。
解决思路有两个:一是用非 root 用户运行 Trivy 容器,在 compose 文件里加上user: "1000:1000";二是尽量用二进制方式跑扫描,不碰 Docker socket。我自己就是二选一的组合——日常巡检用二进制,应急扫描才偶尔用容器方式。
5.5 定时任务不执行的排查套路
如果 cron 不生效,从这几个方向排查:先确认 crond 服务是否运行,群晖和部分绿联系统默认不开启;再检查脚本权限,cron 运行的脚本必须有可执行权限;最后看日志,把输出重定向到日志文件里,问题通常立刻浮出水面。
# 检查 crond 服务状态 systemctl status crond # 手动执行脚本,确认本身没报错 bash /volume1/docker/scripts/trivy-scan.sh6. 只装扫描器远远不够:NAS 上 Docker 安全的几个基本功
6.1 镜像来源把关:官方优先,星标数量不能全信
扫描器帮我们做的是"事后检查",但更有效的是"事前把关"。我的原则只有一条:能用官方镜像绝不用第三方镜像。就算要用第三方镜像,也要看这个镜像的 Dockerfile 是否公开、构建者是否有信誉、最近更新是什么时候。Docker Hub 的星标和下载量只能作为参考,不能作为安全依据——有些恶意镜像就是靠刷下载量来骗人上钩的。
6.2 最小权限原则:别什么都给 root
我在帮朋友排查容器问题的时候,发现很多人喜欢--privileged一把梭,原因往往是"不加它容器跑不起来"。其实绝大多数容器根本不需要特权模式,需要的是挂载宿主机的某个 USB 设备或者某个特殊目录。遇到这种需求,正确的做法是使用--device精确挂载设备,或者用--cap-add按需添加内核能力,而不是开放全部权限。
6.3 资源限制:给容器上紧箍咒
NAS 上的容器还容易出另一个问题:某个容器因为内存泄漏疯狂吃内存,导致整台 NAS 卡死。解决方式是在 compose 文件里给每个容器都加上资源限制。
services: web: image: nginx:latest deploy: resources: limits: cpus: "1.0" memory: 512M这个配置限制了容器最多使用 1 个 CPU 核心和 512MB 内存。虽然一开始跑起来可能会遇到容器性能不足的问题,但总比 NAS 被拖垮要好。安全扫描器解决的是"外部攻击"问题,资源限制解决的是"内部失控"问题,两者都是容器治理的基本功。
6.4 内网隔离:别让容器裸奔在局域网里
我强烈建议你把 NAS 上的容器分到不同的 Docker 网络中,比如将数据库容器放进内网网络,只让需要提供服务的容器(Nginx、应用服务器)暴露端口。这样即使应用容器被攻破,攻击者也摸不到数据库。扫描器扫不出这种网络拓扑层面的问题,但它是 Docker 安全里最实用的一招。
6.5 定期更新:不是一句空话
镜像拉取一次就丢弃是最要命的习惯。我给自己定了一个规矩:每两周检查一次 NAS 上的镜像是否有新版本,关键服务(反向代理、数据库、认证服务)有更新第一时间拉取并重建容器。配合之前配置的定时 Trivy 扫描,就能形成一个"检查-发现-修复"的安全闭环。
7. 最后一个提醒:安全是习惯,不是一次部署
我在 NAS 这条路上走了这么多年,最深的体会是:安全不是一个工具、一次配置就能搞定的,它更像一种日常习惯。Trivy 这类扫描器就像家门口的监控摄像头——装上它不能保证小偷不进门,但能让你及时发现异常、在事情变糟之前出手。更重要的是,你自己得养成"拉镜像之前先想想来源、部署容器之前先想想权限、定时看看扫描报告"的习惯。
最后再分享一个我自己的小技巧:把所有扫描报告统一放在 NAS 的一个共享文件夹里,然后让群晖的通知服务在扫描完成后发一条消息到手机上。这样每周日凌晨跑完例行扫描,我周一早上看一眼手机,就知道这周有没有需要处理的安全告警。机器在做机械的巡检,你只需要在关键节点做决策,这大概就是普通玩家能达到的最舒服的运维状态了。
安全扫描不是给"专业用户"的专属玩具,它应该成为每一个 NAS 用户的默认配置。装一个 Trivy 用不了你半小时,但它给你省下的,可能是重装系统、恢复数据、甚至保住隐私的代价。动手去装一个吧,你的 NAS 会感谢你的。