1. 容器镜像基础信息采集与安全基线检查

镜像安全基线检查的对象与数据流

对容器镜像做安全基线检查,第一步不是直接跑漏洞扫描,而是先建立“镜像身份档案”:镜像从哪来、由哪些层叠加、运行时暴露什么端口、注入了哪些环境变量、默认以什么用户启动。这些信息决定了后续漏洞扫描的上下文——同一个 CVE 在不同基础镜像、不同暴露面下的风险等级完全不同。

本案例以 nginx:1.25.3 为教学示例镜像,完成两条采集链路:

  • 元数据采集:用 docker inspect 提取镜像 ID、RepoDigests、层数量、暴露端口、环境变量、Entrypoint/Cmd、默认 User。
  • 漏洞基线扫描:用 trivy image 输出 OS 包与语言依赖的已知漏洞清单,并按严重级别过滤。
镜像安全基线检查:采集对象与数据流 目标镜像 nginx:1.25.3 本地已 pull docker inspect 镜像元数据采集 Id / RepoDigests / Layers ExposedPorts / Env / User trivy image 漏洞基线扫描 OS 包 + 语言依赖 CVE / 严重级别 / 修复版本 镜像身份档案 镜像 ID / 摘要 / 层数 暴露端口 / 环境变量 默认用户 / 启动命令 漏洞清单 CVE 编号 / 包名 / 版本 严重级别 / 修复版本 按 CRITICAL/HIGH 过滤 基线判定规则(示例阈值) 通过 无 CRITICAL 漏洞 默认用户非 root 警告 存在 HIGH 漏洞 暴露非必要端口 不通过 存在 CRITICAL 漏洞 默认以 root 运行

元数据采集:docker inspect 关键字段提取

直接执行 docker inspect nginx:1.25.3 会返回一个巨大的 JSON 数组,其中真正与安全基线相关的字段集中在少数几个路径。下面用 --format 模板一次性提取关键字段,避免人工翻找。

docker inspect nginx:1.25.3 --format '
镜像ID: {{.Id}}
摘要: {{index .RepoDigests 0}}
层数: {{len .RootFS.Layers}}
暴露端口: {{range $p, $conf := .Config.ExposedPorts}}{{$p}} {{end}}
环境变量: {{range .Config.Env}}{{println .}}{{end}}
默认用户: {{.Config.User}}
Entrypoint: {{.Config.Entrypoint}}
Cmd: {{.Config.Cmd}}
'

示例输出(教学示例,实际值以本地镜像为准):

镜像ID: sha256:4b8f...c3a1
摘要: nginx@sha256:9c2f...e7b4
层数: 7
暴露端口: 80/tcp
环境变量: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
环境变量: NGINX_VERSION=1.25.3
环境变量: PKG_RELEASE=1~bookworm
默认用户: (空)
Entrypoint: [/docker-entrypoint.sh]
Cmd: [nginx -g daemon off;]
关键判读:默认用户为空意味着容器以 root 运行,这是基线检查中的高风险项。暴露端口只有 80/tcp 属于正常范围;若出现 22/tcp 或 2375/tcp 等非必要端口,应标记为警告。

漏洞基线扫描:trivy 输出与过滤

trivy 对镜像的扫描会同时覆盖 OS 包(如 Debian 的 dpkg 包)和语言依赖(如 npm、pip 包)。使用 --severity 过滤出需要立即关注的级别,并用 --format json 保留结构化数据以便后续处理。

trivy image --severity CRITICAL,HIGH --format json -o trivy-nginx.json nginx:1.25.3

若只需快速查看表格结果,可省略 --format json:

trivy image --severity CRITICAL,HIGH nginx:1.25.3

示例输出片段(教学示例,实际漏洞以扫描时数据库为准):

nginx:1.25.3 (debian 12.2)
Total: 3 (CRITICAL: 1, HIGH: 2)

┌────────────┬────────────────┬──────────┬───────────────────┐
│  Library   │ Vulnerability  │ Severity │ Fixed Version     │
├────────────┼────────────────┼──────────┼───────────────────┤
│ libssl3    │ CVE-2023-5678  │ CRITICAL │ 3.0.11-1~deb12u2  │
│ libc6      │ CVE-2023-4911  │ HIGH     │ 2.36-9+deb12u4    │
│ zlib1g     │ CVE-2023-45853 │ HIGH     │ 1:1.2.13.dfsg-1   │
└────────────┴────────────────┴──────────┴───────────────────┘

基线检查结果汇总与判定

将两条链路的结果合并,形成一份可执行的基线检查结论。以下为教学示例的汇总表:

检查项 采集值 基线要求 判定
镜像摘要 sha256:9c2f...e7b4 使用固定摘要而非 latest 通过
层数量 7 ≤ 15 层 通过
暴露端口 80/tcp 仅暴露业务必需端口 通过
默认用户 空(root) 非 root 用户 不通过
CRITICAL 漏洞 1 个(libssl3) 0 个 不通过
HIGH 漏洞 2 个(libc6、zlib1g) ≤ 0 个 警告
结论:该镜像存在 1 个 CRITICAL 漏洞且默认以 root 运行,基线检查不通过。优先处理 libssl3 的 CVE-2023-5678(升级到 3.0.11-1~deb12u2),并在 Dockerfile 中增加非 root 用户指令后重新构建镜像。

可复用的检查脚本

将上述两条链路封装为一个可重复执行的脚本,输出元数据摘要和漏洞统计,便于批量检查多个镜像。

#!/bin/bash
IMAGE="${1:-nginx:1.25.3}"

echo "=== 镜像元数据 ==="
docker inspect "$IMAGE" --format '
镜像ID: {{.Id}}
摘要: {{index .RepoDigests 0}}
层数: {{len .RootFS.Layers}}
暴露端口: {{range $p, $conf := .Config.ExposedPorts}}{{$p}} {{end}}
默认用户: {{.Config.User}}
'

echo ""
echo "=== 漏洞扫描(CRITICAL/HIGH)==="
trivy image --severity CRITICAL,HIGH --format table "$IMAGE"

echo ""
echo "=== 漏洞统计 ==="
trivy image --severity CRITICAL,HIGH --format json "$IMAGE" \
  | python3 -c "
import json,sys
data=json.load(sys.stdin)
crit=high=0
for r in data.get('Results',[]):
    for v in r.get('Vulnerabilities',[]):
        if v['Severity']=='CRITICAL': crit+=1
        elif v['Severity']=='HIGH': high+=1
print(f'CRITICAL: {crit}, HIGH: {high}')
"

执行方式:bash check-image.sh nginx:1.25.3。脚本输出可直接作为基线检查记录归档,CRITICAL 数量大于 0 或默认用户为空时判定为不通过。