Docker容器化部署Greenbone GVM漏洞扫描平台实战指南
1. 为什么要把GVM塞进容器里
搞安全运维的朋友对Greenbone GVM(以前叫OpenVAS)应该不陌生,这套开源漏洞扫描平台在资产盘点、合规检查、暴露面梳理这些场景里出镜率极高。但凡是亲手装过原版的人,大概率都被它的依赖链折磨过:PostgreSQL要特定版本、Redis要调参数、GSA前端要编译、各种feed同步动辄几十分钟,换台机器重来一遍简直是噩梦。我第一次在物理机上部署的时候,光解决依赖冲突就耗掉了一整个下午,最后还因为系统自带的库版本不匹配导致扫描器起不来。
容器化正好戳中了这个痛点。把GVM拆成几个职责单一的容器——数据库一个、扫描引擎一个、Web前端一个——每个容器锁死自己的运行环境,宿主机只需要装个容器运行时就行。迁移的时候把数据卷一打包,换台机器几分钟就能恢复现场。这篇文章我就按自己的实操顺序,把从拉镜像到Web界面汉化的完整链路捋一遍,中间踩过的坑、调过的参数都会写清楚。适合两类人看:一是刚接触GVM想快速搭个可用环境的新手,二是被原生部署折腾过、想换成容器方案的老手。读完你至少能拿到一套能跑起来、界面是中文、feed能正常更新的扫描平台。
需要先说明一点,GVM的容器化方案市面上不止一种,有官方维护的镜像组合,也有社区打包的一体化镜像。我下面走的是多容器分离部署的路线,也就是数据库、扫描器、前端各自独立,这样排查问题的时候能快速定位是哪一层出的毛病,升级单个组件也不会牵一发动全身。一体化镜像虽然启动快,但出问题时日志混在一起,反而更难查。
2. 部署前的整体设计与选型考量
2.1 为什么选多容器分离而不是一体化镜像
先把这个决策说透,因为它直接决定了后面所有操作的组织方式。一体化镜像的好处是docker run一条命令就完事,适合做演示或者临时验证。但它的问题也很明显:数据库和扫描引擎跑在同一个容器里,资源争抢没法隔离,扫描任务重的时候PostgreSQL的响应会被拖慢;日志全部混在一个输出流里,扫描器报错和数据库慢查询搅在一起,排查成本高;升级的时候只能整包替换,没法单独更新feed或者单独调数据库参数。
多容器分离的代价是编排稍微复杂一点,需要自己维护容器间的网络连通和启动顺序。但换来的是清晰的职责边界:数据库容器只管存储,扫描器容器只管执行扫描任务,前端容器只管提供Web访问。我实际用下来,扫描器崩溃重启不会影响数据库里的历史报告,前端挂掉也不耽误后台正在跑的扫描任务。这种隔离性在生产环境里价值很大。
具体到组件划分,我采用的是四个容器的结构:
| 容器角色 | 职责 | 关键依赖 |
|---|---|---|
| 数据库容器 | 存储扫描配置、任务、报告数据 | 持久化数据卷 |
| 扫描引擎容器 | 执行实际漏洞探测、管理feed | 依赖数据库、需要feed数据卷 |
| 前端容器 | 提供Web管理界面 | 依赖扫描引擎的API |
| 辅助容器(可选) | feed同步、证书管理等 | 共享feed数据卷 |
这个划分不是拍脑袋定的,而是照着GVM本身的架构来的。GVM的扫描引擎(openvas-scanner)和管理的守护进程(gvmd)之间通过Unix socket通信,gvmd再和PostgreSQL打交道,前端GSA通过API跟gvmd交互。容器化的时候把gvmd和scanner放在一个容器里(它们耦合较紧),数据库单独拎出来,前端再单独一个,这样既尊重了原有架构,又拿到了隔离的好处。
2.2 镜像来源与版本锁定策略
镜像来源这块要谨慎。GVM的版本迭代比较快,不同版本之间的数据库schema可能有差异,feed的格式也会变。我的做法是锁定具体版本标签,不用latest。latest标签看着省事,但某天上游一更新,你重新拉镜像可能就直接起不来了,因为数据库schema对不上。锁定版本之后,升级变成一个主动的、可控的动作,而不是被动地被上游推着走。
拉镜像之前建议先确认一下宿主机的容器运行时版本,太老的版本对网络模式和数据卷挂载的支持可能有坑。我一般会先跑一遍docker version确认客户端和服务端都正常,再开始拉取。镜像体积不小,扫描引擎那个镜像拉下来好几个G,网络不好的时候建议挂个代理加速——这里说的是容器镜像仓库的访问加速,配置好之后拉取速度会稳定很多。
2.3 网络与数据卷的规划
网络方面,我习惯给这套服务单独建一个自定义bridge网络,而不是用默认的bridge。原因有两个:自定义网络里容器之间可以用容器名做DNS解析,配置连接地址的时候不用记IP;另外默认bridge的隔离性弱一些,单独建网络更干净。建网络的命令很简单:
docker network create gvm-net数据卷的规划是重中之重,直接关系到数据安全和迁移便利性。我至少会规划三个持久化卷:数据库数据卷、feed数据卷、以及扫描器产生的临时数据卷。数据库卷不用多说,丢了就等于所有历史报告和配置全没了。feed卷存的是漏洞测试脚本和CVE数据,体积大、更新频繁,单独挂卷之后重新部署容器不用重新下载feed,能省大量时间。临时数据卷存扫描过程中的中间文件,虽然可以重建,但挂出来方便排查问题。
提示:数据卷的命名建议带上项目前缀,比如
gvm-db-data、gvm-feed-data,避免和宿主机上其他项目的卷混淆。用docker volume ls能一眼看出归属。
3. 核心组件拉取与容器编排实操
3.1 数据库容器的启动与参数调优
数据库我选的是PostgreSQL,这是GVM官方支持的存储后端。启动容器的时候有几个参数必须显式指定,不能全靠默认值。首先是字符集,GVM的某些字段对编码敏感,建议在启动时通过环境变量把默认编码固定为UTF8。其次是连接数,扫描任务并发高的时候数据库连接会被打满,默认的100连接数在中等规模资产下可能不够,我一般会调到200左右。
启动命令大致长这样:
docker run -d \ --name gvm-db \ --network gvm-net \ -e POSTGRES_USER=gvmd \ -e POSTGRES_PASSWORD=你的强密码 \ -e POSTGRES_DB=gvmd \ -v gvm-db-data:/var/lib/postgresql/data \ --restart unless-stopped \ postgres:13这里版本选13是有讲究的。GVM对PostgreSQL的版本有兼容范围,太新的版本某些扩展可能还没适配,太老的版本性能又跟不上。13这个版本在我多次部署中表现稳定,扩展支持也完整。密码这块别图省事用默认的,数据库容器虽然在内网,但弱密码始终是隐患。
启动之后别急着往下走,先确认数据库真的ready了。docker logs gvm-db看到"database system is ready to accept connections"这行才算数。有时候容器状态是running但数据库还在初始化,这时候连扫描器会报连接失败,白白浪费排查时间。
3.2 扫描引擎容器的配置要点
扫描引擎容器是整个部署里配置最复杂的部分,因为它要同时连数据库、读feed、对外提供管理接口。启动之前有几个环境变量必须配对:数据库主机地址指向数据库容器的名字(靠自定义网络的DNS解析),数据库用户名密码和刚才建库时一致,另外还要指定feed的存储路径。
docker run -d \ --name gvm-scanner \ --network gvm-net \ -e DB_HOST=gvm-db \ -e DB_USER=gvmd \ -e DB_PASSWORD=你的强密码 \ -e DB_NAME=gvmd \ -v gvm-feed-data:/var/lib/gvm \ -v gvm-tmp-data:/var/lib/openvas \ --restart unless-stopped \ <扫描引擎镜像>:<锁定版本>这里有个容易忽略的点:扫描引擎容器启动时会尝试连接数据库并做schema迁移,如果数据库还没完全ready,迁移会失败,容器可能反复重启。我的做法是先起数据库,等确认ready之后再起扫描引擎,中间隔个十几秒。如果你用编排工具,可以配置依赖关系和健康检查,但手动部署的时候就得靠顺序控制。
feed数据卷挂载到/var/lib/gvm这个路径下,是因为GVM默认把feed存在这里。第一次启动时feed是空的,需要触发同步。同步过程比较耗时,取决于网络状况和feed体积,我实测下来完整同步一次大概要二三十分钟。同步期间扫描引擎可以正常启动,但扫描功能要等feed就绪后才能用。
3.3 前端容器的对接与访问验证
前端容器相对简单,它主要做两件事:连上扫描引擎的管理接口,然后把Web界面暴露出来。启动时通过环境变量指定后端地址,端口映射到宿主机的一个空闲端口。
docker run -d \ --name gvm-web \ --network gvm-net \ -e BACKEND_HOST=gvm-scanner \ -p 8080:80 \ --restart unless-stopped \ <前端镜像>:<锁定版本>端口映射这里提醒一句,宿主机上8080如果被占用,换成别的端口就行,容器内部端口不用动。启动之后浏览器访问http://宿主机IP:8080,能看到登录页就说明前端和后端的链路通了。第一次登录用默认管理员账号,登录后第一件事就是改密码,这个后面会细说。
三个容器都起来之后,用docker ps确认状态都是Up,再用docker logs逐个扫一眼有没有明显的报错。我习惯在这个阶段做一次连通性验证:从前端容器里ping一下扫描引擎容器名,从扫描引擎容器里连一下数据库,确认网络层没问题再进入下一阶段。
4. feed同步与Web界面汉化的关键操作
4.1 feed同步的触发与进度判断
feed是GVM的"弹药库",里面装着漏洞检测脚本(NVTs)、CVE数据、SCAP配置基准等。没有feed,扫描器就是个空壳,什么漏洞都测不出来。容器化部署下,feed同步有两种触发方式:一是容器启动时自动触发,二是手动进容器执行同步命令。
我倾向于手动触发,因为自动触发的时候如果网络抖动导致同步中断,容器可能卡在某个中间状态,反而不容易判断。手动执行的命令大致是进到扫描引擎容器里跑feed更新脚本:
docker exec -it gvm-scanner bash # 容器内执行feed同步 greenbone-feed-sync --type GVMD_DATA greenbone-feed-sync --type SCAP greenbone-feed-sync --type CERT同步过程中可以通过日志观察进度,也可以看feed目录下的文件数量变化。判断同步是否完成有个简单办法:看NVTs的数量。同步完成后NVTs数量通常在十几万这个量级,如果只有几千或者为零,说明还没同步完或者同步失败了。
注意:feed同步对网络稳定性要求较高,中途断掉的话建议清掉未完成的临时文件重新同步,不要在半成品基础上继续,否则可能出现feed数据不一致导致扫描结果异常。
4.2 汉化包的处理与界面语言切换
Web界面默认是英文的,对不少团队来说中文界面能降低使用门槛。GVM的Web前端支持多语言,汉化不需要改代码,走的是标准的语言包机制。具体操作分两步:确认前端镜像里是否已经内置了中文语言资源,如果没有则需要把语言包文件放到指定目录,然后在界面里切换语言。
先登录Web界面,进到用户设置里看语言选项有没有中文。如果有,直接选中文保存,刷新页面就生效了。如果没有,说明镜像里的语言资源不全,需要手动补。补的方式是从GVM的翻译资源里拿到中文的翻译文件,放到前端容器的语言资源目录下,然后重启前端容器。
这里有个实操细节:语言包文件的命名和目录结构要严格匹配前端框架的约定,放错位置界面不会报错但就是不生效,很容易让人以为汉化失败。我一般会先在前端容器里找到现有语言文件的存放路径,照着英文语言包的文件名和结构放中文包,这样最稳妥。
4.3 首次登录与基础安全配置
汉化搞定之后,别急着建扫描任务,先把基础安全配置做掉。默认管理员账号的密码是公开的,任何知道这套系统的人都能登进来,所以第一件事就是改密码,而且要用强密码。改完之后建议再建一个日常使用的普通账号,管理员账号只在需要改系统配置的时候用,降低误操作和账号泄露的风险。
接着检查一下扫描引擎的并发设置和超时参数。默认配置偏保守,扫描大网段的时候会显得很慢。可以在管理界面里调整最大并发扫描数和每个主机的超时时间,但别一上来就拉满,先小范围测试再逐步调高,避免把目标网络打挂或者触发对方的防护机制。
5. 常见问题排查与避坑经验
5.1 容器启动顺序导致的连接失败
这是最高频的问题。扫描引擎容器比数据库先起来,连不上数据库就退出,--restart unless-stopped会让它反复重启,日志里全是连接拒绝。解决办法要么是手动控制启动顺序,要么给数据库容器配健康检查,让扫描引擎等数据库健康后再启动。手动部署的话,我的习惯是数据库起来后等半分钟再起扫描引擎,稳当。
5.2 feed同步卡住或失败的排查思路
feed同步失败的原因五花八门,常见的有网络超时、磁盘空间不足、权限问题。排查顺序建议是:先看磁盘剩余空间(feed体积不小,空间不够会写到一半失败),再看容器日志里的具体报错,最后检查feed目录的属主和权限是否和容器内运行用户匹配。如果是网络问题导致的中断,清掉临时文件重来通常能解决。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 同步进度长时间不动 | 网络超时 | 检查网络后重新同步 |
| 同步报磁盘写入错误 | 空间不足 | 清理空间或扩容数据卷 |
| 同步完成但NVTs数量异常少 | 部分feed未拉全 | 清临时文件重新同步 |
| 权限拒绝 | 目录属主不匹配 | 调整数据卷目录权限 |
5.3 Web界面访问异常的分层排查
访问不了Web界面的时候,按层排查效率最高。先确认前端容器是否在跑,再确认端口映射是否正确,然后从前端容器内部访问后端接口看通不通,最后看后端容器日志有没有报错。这个顺序能快速定位问题出在网络层、前端层还是后端层。我遇到过端口被宿主机上其他服务占用的情况,表现就是浏览器连不上但容器状态正常,换个端口就好了。
5.4 数据备份与迁移的实操建议
容器化部署的一大好处是迁移方便,但前提是数据卷管理得当。备份的时候把数据库卷和feed卷打包就行,恢复的时候先起数据库容器挂上备份的卷,再起扫描引擎。有个细节要注意:数据库容器的数据卷里除了数据文件还有配置文件,迁移到不同版本的数据库镜像时配置文件可能不兼容,稳妥的做法是只迁移数据目录,配置文件让新容器自己生成。
6. 我在这套方案上踩过的坑
说几个文档里不太会写、但实际部署时很容易撞上的问题。第一个是时区,容器默认用UTC,扫描报告里的时间戳和本地时间对不上,排查事件的时候容易看错。解决办法是在启动容器时通过环境变量把时区设成东八区,数据库和扫描引擎都要设,不然两边时间不一致更乱。
第二个是资源限制。扫描引擎在跑大任务的时候CPU和内存占用会飙升,如果宿主机上还跑着别的服务,建议给扫描引擎容器加上资源上限,避免它把整台机器拖垮。我一般会给它限制在宿主机一半的CPU核数和内存,留出余量给其他服务。
第三个是feed更新和扫描任务别同时跑。feed更新期间扫描引擎的部分功能会受影响,如果这时候正好有扫描任务在跑,可能出现结果不完整或者任务卡死。我的习惯是安排feed更新在业务低峰期,更新完再跑扫描任务。
这套容器化方案我前后在几台不同配置的机器上部署过,整体稳定性不错,迁移和恢复也验证过多次。真正花时间的不是启动容器,而是feed同步和参数调优这两块,把这两步做扎实,后面用起来就很顺。汉化这块如果镜像自带语言包就是几分钟的事,不带的话补语言包也不复杂,关键是路径要对。后续如果想扩展,可以考虑把扫描任务的结果导出接到自己的资产管理系统里,GVM的API支持得还不错,这个方向我还在摸索,有进展再单独写一篇。