Hotdry.
ai-systems

构建Docker安全加固镜像的自动化扫描流水线:漏洞检测、签名验证与运行时策略

针对Docker Hardened Images设计三层安全扫描流水线,涵盖构建时漏洞检测、镜像签名验证与运行时安全策略实施,提供可落地的参数配置与监控方案。

在容器化部署成为主流的今天,镜像安全已成为软件供应链安全的关键环节。Docker Hardened Images(DHI)作为官方提供的安全加固镜像,承诺提供近零 CVE 的容器基础,但如何确保这些镜像在整个生命周期中持续安全,需要一套完整的自动化扫描与验证机制。本文将设计一个三层安全扫描流水线,涵盖构建时漏洞检测、镜像签名验证与运行时安全策略实施,为工程团队提供可落地的实施方案。

一、Docker 安全加固镜像的核心价值与扫描必要性

Docker Hardened Images 通过最小化 distroless 镜像设计,将攻击面减少高达 97%,同时承诺减少 95% 的 CVE 漏洞。然而,正如 Docker 官方文档所指出的:“Docker Hardened Images (DHIs) are designed to be secure by default, but like any container image, it's important to scan them regularly as part of your vulnerability management process.” 即使是最安全的镜像,也需要持续的监控与验证。

DHI 的核心安全特性包括:

  • 最小化镜像:基于 Debian 和 Alpine 的 distroless 镜像,移除不必要的组件
  • 完整 SBOM:提供软件物料清单,支持供应链透明度
  • SLSA Level 3 溯源:确保构建过程的可验证性
  • 自动补丁:Docker Scout 可在 24 小时内响应新发现的 CVE

二、三层安全扫描流水线设计

2.1 第一层:构建时漏洞扫描

构建时扫描是安全流水线的第一道防线,应在镜像构建完成后立即执行。推荐使用 Docker Scout 作为主要扫描工具,因其与 DHI 深度集成。

配置参数示例:

# 基础扫描命令
docker scout cves <namespace>/dhi-<image>:<tag> --platform linux/amd64

# 详细输出与过滤
docker scout cves <image> --output json --severity critical,high

# CI/CD集成阈值设置
docker scout cves <image> --exit-code --severity critical

GitHub Actions 工作流配置:

name: DHI Vulnerability Scan
on:
  push:
    branches: [main]
  pull_request:
    branches: ["**"]

env:
  REGISTRY: docker.io
  IMAGE_NAME: ${{ github.repository }}
  SHA: ${{ github.event.pull_request.head.sha || github.event.after }}

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Build and scan image
        run: |
          docker build -t $REGISTRY/$IMAGE_NAME:$SHA .
          docker scout cves $REGISTRY/$IMAGE_NAME:$SHA --exit-code

扫描阈值建议:

  • 严重(Critical)漏洞:零容忍,构建失败
  • 高危(High)漏洞:不超过 2 个,否则需要人工审核
  • 中危(Medium)漏洞:不超过 10 个,记录警告
  • 低危(Low)漏洞:仅记录,不阻断构建

2.2 第二层:镜像签名验证

镜像签名验证确保镜像在传输和存储过程中未被篡改。DHI 支持多种签名方案,推荐使用 Sigstore Cosign 进行无密钥签名验证。

签名验证流程:

  1. 验证 DHI 官方签名
# 使用Cosign验证Docker官方签名
cosign verify \
  --certificate-identity-regexp ".*docker.com" \
  --certificate-oidc-issuer "https://accounts.google.com" \
  docker.io/docker/dhi-base:latest
  1. 自定义镜像签名策略
# 生成签名密钥对
cosign generate-key-pair

# 签名自定义镜像
cosign sign --key cosign.key <your-image>:<tag>

# 验证签名
cosign verify --key cosign.pub <your-image>:<tag>
  1. Kubernetes 策略执行(使用 Kyverno)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: verify-image-signature
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "All container images must be signed"
        foreach:
          - list: "request.object.spec.containers"
            deny:
              conditions:
                all:
                  - key: "{{ element.image }}"
                    operator: NotEquals
                    value: "*"
                  - key: "{{ images.verify(\"{{ element.image }}\") }}"
                    operator: Equals
                    value: false

签名验证最佳实践:

  • 生产环境强制要求所有镜像必须签名
  • 开发环境可设置为警告模式
  • 定期轮换签名密钥(建议每 90 天)
  • 使用硬件安全模块(HSM)存储私钥

2.3 第三层:运行时安全策略

运行时安全策略确保容器在运行时的行为符合安全预期。DHI 提供了内置的安全特性,但需要额外的策略配置。

Seccomp 配置文件示例:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "accept",
        "accept4",
        "access",
        "alarm",
        "bind",
        "brk",
        "capget",
        "capset",
        "chdir",
        "chmod",
        "chown",
        "chown32",
        "clock_gettime",
        "clone",
        "close",
        "connect",
        "copy_file_range",
        "creat",
        "dup",
        "dup2",
        "dup3",
        "epoll_create",
        "epoll_create1",
        "epoll_ctl",
        "epoll_pwait",
        "epoll_wait",
        "eventfd",
        "eventfd2",
        "execve",
        "execveat",
        "exit",
        "exit_group",
        "faccessat",
        "faccessat2",
        "fadvise64",
        "fallocate",
        "fanotify_mark",
        "fchdir",
        "fchmod",
        "fchmodat",
        "fchown",
        "fchown32",
        "fchownat",
        "fcntl",
        "fcntl64",
        "fdatasync",
        "fgetxattr",
        "flistxattr",
        "flock",
        "fork",
        "fremovexattr",
        "fsetxattr",
        "fstat",
        "fstat64",
        "fstatat64",
        "fstatfs",
        "fstatfs64",
        "fsync",
        "ftruncate",
        "ftruncate64",
        "futex",
        "futimesat",
        "getcpu",
        "getcwd",
        "getdents",
        "getdents64",
        "getegid",
        "getegid32",
        "geteuid",
        "geteuid32",
        "getgid",
        "getgid32",
        "getgroups",
        "getgroups32",
        "getitimer",
        "getpeername",
        "getpgid",
        "getpgrp",
        "getpid",
        "getppid",
        "getpriority",
        "getrandom",
        "getresgid",
        "getresgid32",
        "getresuid",
        "getresuid32",
        "getrlimit",
        "get_robust_list",
        "getrusage",
        "getsid",
        "getsockname",
        "getsockopt",
        "get_thread_area",
        "gettid",
        "gettimeofday",
        "getuid",
        "getuid32",
        "getxattr",
        "inotify_add_watch",
        "inotify_init",
        "inotify_init1",
        "inotify_rm_watch",
        "io_cancel",
        "ioctl",
        "io_destroy",
        "io_getevents",
        "ioprio_get",
        "ioprio_set",
        "io_setup",
        "io_submit",
        "ipc",
        "kill",
        "lchown",
        "lchown32",
        "lgetxattr",
        "link",
        "linkat",
        "listen",
        "listxattr",
        "llistxattr",
        "_llseek",
        "lremovexattr",
        "lseek",
        "lsetxattr",
        "lstat",
        "lstat64",
        "madvise",
        "memfd_create",
        "mincore",
        "mkdir",
        "mkdirat",
        "mknod",
        "mknodat",
        "mlock",
        "mlock2",
        "mlockall",
        "mmap",
        "mmap2",
        "mprotect",
        "mq_getsetattr",
        "mq_notify",
        "mq_open",
        "mq_timedreceive",
        "mq_timedsend",
        "mq_unlink",
        "mremap",
        "msgctl",
        "msgget",
        "msgrcv",
        "msgsnd",
        "msync",
        "munlock",
        "munlockall",
        "munmap",
        "nanosleep",
        "newfstatat",
        "_newselect",
        "open",
        "openat",
        "pause",
        "pipe",
        "pipe2",
        "poll",
        "ppoll",
        "prctl",
        "pread64",
        "preadv",
        "preadv2",
        "prlimit64",
        "pselect6",
        "pwrite64",
        "pwritev",
        "pwritev2",
        "read",
        "readahead",
        "readlink",
        "readlinkat",
        "readv",
        "recv",
        "recvfrom",
        "recvmmsg",
        "recvmsg",
        "remap_file_pages",
        "removexattr",
        "rename",
        "renameat",
        "renameat2",
        "restart_syscall",
        "rmdir",
        "rt_sigaction",
        "rt_sigpending",
        "rt_sigprocmask",
        "rt_sigqueueinfo",
        "rt_sigreturn",
        "rt_sigsuspend",
        "rt_sigtimedwait",
        "rt_tgsigqueueinfo",
        "sched_getaffinity",
        "sched_getattr",
        "sched_getparam",
        "sched_get_priority_max",
        "sched_get_priority_min",
        "sched_getscheduler",
        "sched_rr_get_interval",
        "sched_setaffinity",
        "sched_setattr",
        "sched_setparam",
        "sched_setscheduler",
        "sched_yield",
        "seccomp",
        "select",
        "semctl",
        "semget",
        "semop",
        "semtimedop",
        "send",
        "sendfile",
        "sendfile64",
        "sendmmsg",
        "sendmsg",
        "sendto",
        "setfsgid",
        "setfsgid32",
        "setfsuid",
        "setfsuid32",
        "setgid",
        "setgid32",
        "setgroups",
        "setgroups32",
        "setitimer",
        "setpgid",
        "setpriority",
        "setregid",
        "setregid32",
        "setresgid",
        "setresgid32",
        "setresuid",
        "setresuid32",
        "setreuid",
        "setreuid32",
        "setrlimit",
        "set_robust_list",
        "setsid",
        "setsockopt",
        "set_thread_area",
        "set_tid_address",
        "setuid",
        "setuid32",
        "setxattr",
        "shmat",
        "shmctl",
        "shmdt",
        "shmget",
        "shutdown",
        "sigaltstack",
        "signalfd",
        "signalfd4",
        "sigreturn",
        "socket",
        "socketcall",
        "socketpair",
        "splice",
        "stat",
        "stat64",
        "statfs",
        "statfs64",
        "statx",
        "symlink",
        "symlinkat",
        "sync",
        "sync_file_range",
        "syncfs",
        "sysinfo",
        "tee",
        "tgkill",
        "time",
        "timer_create",
        "timer_delete",
        "timer_getoverrun",
        "timer_gettime",
        "timer_settime",
        "timerfd_create",
        "timerfd_gettime",
        "timerfd_settime",
        "times",
        "tkill",
        "truncate",
        "truncate64",
        "ugetrlimit",
        "umask",
        "uname",
        "unlink",
        "unlinkat",
        "utime",
        "utimensat",
        "utimes",
        "vfork",
        "vmsplice",
        "wait4",
        "waitid",
        "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

AppArmor 配置文件示例:

#include <tunables/global>

profile docker-hardened flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  
  # 允许基本文件操作
  /etc/ld.so.cache r,
  /lib/** r,
  /usr/lib/** r,
  
  # 限制网络访问
  network inet tcp,
  network inet udp,
  network inet6 tcp,
  network inet6 udp,
  
  # 禁止特权操作
  deny capability sys_module,
  deny capability sys_rawio,
  deny capability sys_admin,
  
  # 限制进程间通信
  deny ipc,
}

三、监控与告警机制

3.1 CVE 响应时间监控

Docker 承诺在 24 小时内响应新发现的 CVE。监控系统应跟踪以下指标:

  • CVE 发现到补丁发布时间:目标≤24 小时
  • 扫描覆盖率:目标 100% 镜像覆盖
  • 漏洞修复率:目标≥95% 关键漏洞在 7 天内修复

3.2 合规性检查

定期检查以下合规性要求:

  • 所有生产镜像必须使用 DHI 基础镜像
  • 所有镜像必须签名且验证通过
  • 运行时安全策略必须启用(Seccomp、AppArmor)
  • SBOM 必须完整且可追溯

3.3 异常检测

配置异常检测规则:

  • 镜像签名验证失败
  • 运行时权限提升尝试
  • 未授权的网络连接
  • 文件系统异常访问模式

四、实施路线图与风险控制

4.1 分阶段实施计划

阶段一(1-2 周):基础扫描集成

  • 集成 Docker Scout 到 CI/CD 流水线
  • 配置基础漏洞扫描阈值
  • 建立扫描报告机制

阶段二(3-4 周):签名验证实施

  • 部署 Cosign 签名基础设施
  • 配置镜像签名策略
  • 集成 Kyverno 策略验证

阶段三(5-8 周):运行时安全强化

  • 部署 Seccomp 和 AppArmor 配置文件
  • 配置运行时监控
  • 建立安全事件响应流程

4.2 风险控制措施

  1. 误报处理:建立漏洞验证流程,对关键漏洞进行人工确认
  2. 性能影响:扫描操作应在非关键路径执行,避免影响构建速度
  3. 密钥管理:使用 HSM 或云密钥管理服务保护签名密钥
  4. 回滚策略:确保安全策略失败时可快速回滚到安全状态

五、总结

构建 Docker 安全加固镜像的自动化扫描流水线是一个系统工程,需要从构建时、传输时、运行时三个维度全面防护。通过集成 Docker Scout 进行漏洞扫描、使用 Sigstore Cosign 进行签名验证、配置 Seccomp 和 AppArmor 实施运行时安全策略,可以构建一个完整的安全防护体系。

关键成功因素包括:

  • 自动化程度:尽可能减少人工干预
  • 可观测性:全面的监控和告警机制
  • 持续改进:定期评估和优化安全策略
  • 团队协作:安全团队与开发团队的紧密合作

随着 Docker Hardened Images 的不断演进,安全扫描流水线也需要持续更新,以适应新的安全威胁和合规要求。通过本文提供的框架和参数配置,工程团队可以快速建立起符合自身需求的安全扫描体系。

资料来源:

  1. Docker 官方文档:Scan Docker Hardened Images - https://docs.docker.com/dhi/how-to/scan/
  2. Docker 博客:How Docker Hardened Images Patch CVEs in 24 Hours - https://www.docker.com/blog/how-docker-hardened-images-patch-cves-in-24-hours/
查看归档