2. Dockerfile 安全缺陷静态审计

Dockerfile 安全缺陷静态审计 学习卡片

审计目标与检查项定义

Dockerfile 静态审计的核心是把“人眼逐行看”变成“规则逐条匹配”。本例聚焦四类高频缺陷,并为每类缺陷定义可判定的匹配条件与严重级别:

缺陷类型判定条件(示例)严重级别
以 root 运行缺少 USER 指令,或 USER 值为 root / 0 / 0:0高
使用 latest 标签FROM 行镜像引用未指定 tag,或 tag 为 latest中
敏感信息硬编码ENV / ARG / RUN 中出现 PASSWORD、SECRET、TOKEN、API_KEY 等键名并赋值高
缺少 USER 指令整个 Dockerfile 中不存在任何 USER 指令高
判定顺序很重要:先判断“是否缺少 USER”,再判断“USER 是否为 root”。两者是不同缺陷,不应重复计数。

审计脚本实现

脚本采用逐行解析 + 指令归一化的方式,不依赖 Docker 守护进程,可直接对任意 Dockerfile 文本运行。关键点:处理多阶段构建时,只对最终阶段(最后一个 FROM 之后)做 USER 判定,避免把构建阶段的 root 误报为运行时 root。

#!/usr/bin/env python3
# dockerfile_audit.py —— Dockerfile 安全缺陷静态审计
import re, sys, json

SENSITIVE_KEYS = ("PASSWORD", "PASSWD", "SECRET", "TOKEN", "API_KEY", "ACCESS_KEY", "PRIVATE_KEY")

def parse_stages(lines):
    """按 FROM 切分构建阶段,返回 [(base_image, [指令行...]), ...]"""
    stages, cur = [], None
    for ln in lines:
        s = ln.strip()
        if not s or s.startswith("#"):
            continue
        if re.match(r"(?i)^FROM\s+", s):
            cur = {"base": s.split(None, 1)[1].strip(), "lines": []}
            stages.append(cur)
        elif cur is not None:
            cur["lines"].append(s)
    return stages

def check_from(base):
    """检查基础镜像标签:无 tag 或 latest 均判为缺陷"""
    ref = base.split(" AS ")[0].split(" as ")[0].strip()
    if "@sha256:" in ref:                      # 摘要固定,视为合规
        return None
    if ":" not in ref:
        return f"基础镜像 {ref} 未指定标签(隐式 latest)"
    tag = ref.rsplit(":", 1)[1]
    if tag == "latest":
        return f"基础镜像 {ref} 使用 latest 标签"
    return None

def check_sensitive(line):
    """检查 ENV/ARG/RUN 中的敏感键硬编码"""
    hits = []
    for k in SENSITIVE_KEYS:
        # 匹配 KEY=VALUE 或 KEY VALUE 两种写法
        if re.search(rf"(?i)\b{k}\b\s*[=\s]\s*\S+", line):
            hits.append(k)
    return hits

def audit(path):
    with open(path, encoding="utf-8") as f:
        lines = f.readlines()
    stages = parse_stages(lines)
    findings = []

    for idx, st in enumerate(stages):
        is_final = (idx == len(stages) - 1)
        tag_issue = check_from(st["base"])
        if tag_issue:
            findings.append({"stage": idx + 1, "type": "latest标签",
                             "level": "中", "detail": tag_issue})

        user_directives = [l for l in st["lines"] if re.match(r"(?i)^USER\s+", l)]
        if is_final:
            if not user_directives:
                findings.append({"stage": idx + 1, "type": "缺少USER指令",
                                 "level": "高",
                                 "detail": "最终阶段无 USER 指令,容器将以 root 运行"})
            else:
                u = user_directives[-1].split(None, 1)[1].strip()
                if u in ("root", "0", "0:0", "root:root"):
                    findings.append({"stage": idx + 1, "type": "以root运行",
                                     "level": "高",
                                     "detail": f"最终阶段 USER {u} 为 root 身份"})

        for l in st["lines"]:
            if re.match(r"(?i)^(ENV|ARG|RUN)\s+", l):
                for k in check_sensitive(l):
                    findings.append({"stage": idx + 1, "type": "敏感信息硬编码",
                                     "level": "高",
                                     "detail": f"第 {idx+1} 阶段出现 {k} 明文赋值"})
    return findings

def report(findings, path):
    print(f"=== Dockerfile 审计报告: {path} ===")
    if not findings:
        print("未发现目标缺陷。")
        return
    order = {"高": 0, "中": 1, "低": 2}
    for f in sorted(findings, key=lambda x: order[x["level"]]):
        print(f"[{f['level']}] {f['type']} | 阶段{f['stage']} | {f['detail']}")
    print(f"--- 合计 {len(findings)} 项缺陷 ---")

if __name__ == "__main__":
    report(audit(sys.argv[1]), sys.argv[1])

审计流程与判定路径

下图展示脚本对单个 Dockerfile 的处理链:从读取文件、切分阶段,到四类规则的判定分支,最终汇总为分级报告。注意“缺少 USER”与“USER 为 root”是互斥分支,而 latest 标签与敏感信息检查在每个阶段都会执行。

Dockerfile 逐行读取 / 去注释 按 FROM 切分阶段 标记最终阶段 规则判定分支 FROM 标签检查 无 tag / latest → 中危 ENV/ARG/RUN 扫描 敏感键明文 → 高危 最终阶段 USER 判定 无 USER 指令 → 缺少USER(高危) USER = root/0 → 以root运行(高危) USER 为非 root 普通用户 → 通过该规则 汇总分级报告 高危优先排序 → 缺陷类型 / 阶段 / 详情 / 合计 审计报告 文本 / JSON 输出

运行与结果判读

准备一个含缺陷的示例 Dockerfile 进行验证:

FROM python:latest
ENV DB_PASSWORD=Passw0rd123
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

执行 python3 dockerfile_audit.py Dockerfile,预期输出:

=== Dockerfile 审计报告: Dockerfile ===
[高] 缺少USER指令 | 阶段1 | 最终阶段无 USER 指令,容器将以 root 运行
[高] 敏感信息硬编码 | 阶段1 | 第 1 阶段出现 PASSWORD 明文赋值
[中] latest标签 | 阶段1 | 基础镜像 python:latest 使用 latest 标签
--- 合计 3 项缺陷 ---
脚本只做静态文本匹配,无法识别通过 RUN export SECRET=... 或外部构建参数注入的敏感信息。若需覆盖此类场景,应结合构建时参数审计与镜像层扫描。

修复对照与验证

缺陷修复前修复后
latest 标签FROM python:latestFROM python:3.12.4-slim
敏感信息硬编码ENV DB_PASSWORD=Passw0rd123改用 ARG + 构建时注入,或运行时挂载 Secret
缺少 USER无 USER 指令RUN useradd -m appuser + USER appuser

修复后重新运行脚本,预期输出“未发现目标缺陷”。若仍报“以root运行”,检查 USER 值是否被写成 0 或 root:root 形式。

审计脚本可直接接入 CI:在镜像构建前执行,发现高危缺陷时以非零退出码中断流水线,实现“缺陷左移”。