Docker安全最佳实践

本文是Docker专栏系列的第八篇,系统全面地讲解Docker容器安全的方方面面。从底层Linux隔离机制到镜像供应链安全,从Dockerfile安全编写到运行时监控响应,从守护进程加固到网络隔离策略,从密钥管理到合规审计,涵盖容器安全全生命周期。文章配有大量实战命令、配置模板和真实案例分析,适合有一定Docker基础的开发者和运维人员深入学习和实践。


第一章 Docker安全概述

1.1 容器安全的重要性与挑战

在云原生时代,Docker容器已经成为应用交付和运行的标准载体。据CNCF(云原生计算基金会)2024年度调查报告显示,超过94%的企业在生产环境中使用容器技术,而Docker及其生态(Kubernetes、containerd等)是其中的核心。然而,容器技术的广泛应用也带来了前所未有的安全挑战。

容器安全之所以重要,原因在于以下几个方面:

第一,容器共享主机内核。 与传统虚拟机不同,所有容器共享宿主机的操作系统内核。这意味着一旦攻击者突破容器边界(即"容器逃逸"),就可能直接获得宿主机乃至同宿主机上所有其他容器的控制权。这种"共享内核"架构是容器安全与传统虚拟机安全最本质的区别,也是最大的风险来源。

第二,容器镜像的供应链风险。 容器镜像通常由基础镜像、依赖库、应用代码等多层构成。如果其中任何一层被植入恶意代码或包含已知漏洞,整个容器都会受到影响。2021年爆发的Codecov供应链攻击事件,以及2024年多起针对Docker Hub公共镜像的投毒事件,都深刻说明了镜像供应链安全的严峻性。

第三,默认配置的安全性不足。 Docker的默认配置在安全性和易用性之间做了折中。例如,默认情况下容器以root用户运行、默认拥有相当数量的Linux Capabilities、默认没有启用用户命名空间重映射等。这些默认配置虽然方便了开发者快速上手,但在生产环境中却可能成为严重的安全隐患。

第四,攻击面广泛。 容器技术涉及镜像构建、镜像分发、容器运行、网络通信、存储卷、守护进程等多个环节,每个环节都有其独特的安全风险。攻击者可以从任何一个薄弱环节入手,实施攻击。

下面是近年来一些著名的容器安全事件:

时间事件影响
2018年Docker Hub 19万账号泄露敏感镜像仓库被未授权访问
2019年runc容器逃逸漏洞(CVE-2019-5736)攻击者可获取宿主机root权限
2021年Docker Hub恶意镜像挖矿大量容器被植入加密货币挖矿程序
2023年多个npm包注入恶意Docker镜像供应链攻击蔓延至容器生态
2024年Docker Hub容器镜像投毒事件数百个含后门的镜像被下载数万次

容器安全面临的挑战可以归纳为以下几点:

挑战1: 共享内核带来的隔离弱化 —— 容器隔离依赖Linux内核特性,而非硬件级隔离
挑战2: 镜像供应链信任链不完整 —— 镜像来源验证机制不够完善
挑战3: 运行时安全可视性不足 —— 容器生命周期短,传统安全工具难以覆盖
挑战4: 配置复杂度高 —— 安全参数众多,容易遗漏或误配
挑战5: 东西向流量管控困难 —— 容器间通信缺乏默认的网络隔离
挑战6: 密钥管理分散 —— 凭据容易硬编码在镜像或环境变量中

正是基于这些挑战,本文将从底层原理出发,系统讲解Docker安全的每一个维度,帮助读者构建起完整的容器安全知识体系和实战能力。

1.2 Docker安全模型(命名空间隔离 + cgroups限制 + 能力机制)

Docker的安全模型建立在Linux内核提供的三大安全机制之上:命名空间(Namespaces)控制组(cgroups)Linux能力机制(Capabilities)。此外,还辅以SeccompAppArmor/SELinux等安全模块进行纵深防御。理解这些底层机制是掌握容器安全的前提。

1.2.1 命名空间(Namespaces)——隔离的基础

命名空间是Linux内核提供的一种资源隔离机制。它可以让一组进程看到一组独立的系统资源,仿佛运行在独立的系统中。Docker利用命名空间实现了容器间的资源隔离。

Linux内核目前提供了以下命名空间:

# 查看当前系统支持的所有命名空间类型
ls -l /proc/self/ns/
# 输出示例:
# cgroup -> cgroup:[4026531835]
# ipc -> ipc:[4026531839]
# mnt -> mnt:[4026531840]
# net -> net:[4026531992]
# pid -> pid:[4026531836]
# user -> user:[4026531837]
# uts -> uts:[4026531838]

# 查看某个容器的命名空间
docker inspect --format '{{.State.Pid}}' <容器名>  # 获取容器进程PID
ls -l /proc/<PID>/ns/  # 查看该进程的命名空间

Docker默认使用以下命名空间:

命名空间隔离资源安全作用
PID进程ID容器内只能看到自己的进程,无法看到宿主机或其他容器的进程
NET网络栈容器拥有独立的网络接口、IP地址、路由表、防火墙规则
MNT挂载点容器拥有独立的文件系统挂载视图
IPC进程间通信隔离System V IPC和POSIX消息队列
UTS主机名和域名容器拥有独立的主机名
USER用户和用户组ID将容器内的用户映射为宿主机上的不同用户
1.2.2 控制组(cgroups)——资源限制

cgroups(Control Groups)是Linux内核提供的资源限制和统计机制。它可以对进程组使用的CPU、内存、磁盘I/O、网络带宽等资源进行限制、记录和隔离。Docker利用cgroups防止单个容器耗尽宿主机资源(即防止资源耗尽型DoS攻击)。

# 查看容器的cgroup限制
docker run -d --name test-container --memory="512m" --cpus="1.0" nginx

# 查看容器的cgroup信息
cat /sys/fs/cgroup/memory/docker/<容器ID>/memory.limit_in_bytes
# 输出: 536870912 (即512MB)

# 限制CPU使用
docker run --cpus="0.5" --cpu-shares=512 nginx  # 限制使用0.5个CPU

# 限制内存使用
docker run --memory="512m" --memory-swap="1g" nginx  # 内存限制512MB,swap限制1GB

# 限制PID数量(防止fork炸弹)
docker run --pids-limit=200 nginx  # 容器内最多200个进程
1.2.3 Linux能力机制(Capabilities)——权限细分

传统的Linux权限模型是二元的:要么是root(拥有所有权限),要么是普通用户(权限受限)。这种模型过于粗放。Linux Capabilities机制将root权限细分为近40种独立的能力,每种能力对应一类特定的特权操作。Docker默认只授予容器一小部分能力,丢弃了大量危险的root能力,从而实现最小权限原则。

# 查看容器默认拥有的Capabilities
docker run --rm alpine cat /proc/1/status | grep Cap
# CapEff: 00000000a80425fb  (这是容器默认的能力位掩码)

# 解码能力位掩码
docker run --rm alpine sh -c 'apk add --no-cache libcap 2>/dev/null; capsh --decode=00000000a80425fb'
# 输出容器默认拥有的能力列表

# 查看所有可用的Capabilities
capsh --print

这三大机制协同工作,构成了Docker安全的基础:命名空间提供隔离,cgroups提供限制,Capabilities提供权限细分。在后续章节中,我们将对每个机制进行深入分析。

1.3 容器安全与虚拟机安全的区别

理解容器安全与虚拟机安全的区别,对于选择正确的安全策略至关重要。这两种虚拟化技术在隔离模型上有本质不同。

虚拟机(VM)安全模型:

虚拟机通过Hypervisor(如KVM、VMware ESXi、Hyper-V)在硬件层面实现隔离。每个虚拟机拥有完整的操作系统(包括独立的内核),通过硬件辅助虚拟化(Intel VT-x/AMD-V)实现强隔离。即使虚拟机被攻破,攻击者也需要突破Hypervisor才能影响宿主机或其他虚拟机(这种攻击称为"虚拟机逃逸",难度极高)。

容器安全模型:

容器通过Linux内核特性(命名空间、cgroups等)实现隔离,所有容器共享宿主机内核。容器没有独立的内核,隔离强度依赖于内核的安全机制。一旦内核存在漏洞或配置不当,容器逃逸的风险远高于虚拟机逃逸。

下表详细对比了两者的安全特性:

安全维度虚拟机(VM)容器(Container)
隔离机制硬件级虚拟化(Hypervisor)操作系统级虚拟化(命名空间+cgroups)
内核共享不共享,每个VM有独立内核共享宿主机内核
隔离强度强(需突破Hypervisor)较弱(依赖内核安全机制)
攻击面Hypervisor + Guest OS共享内核 + 容器运行时
资源开销大(每个VM需完整OS)小(共享内核,轻量)
逃逸难度相对较低(内核漏洞利用)
安全补丁每个VM独立打补丁宿主机内核补丁影响所有容器
防火墙/网络安全每个VM独立网络栈共享内核网络栈,需额外隔离
合规性成熟(PCI-DSS等已覆盖)较新(法规仍在适应中)
启动速度分钟级秒级
┌─────────────────────────────────────────────────────────────┐
│                      虚拟机架构                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │
│  │  App A   │  │  App B   │  │  App C   │                  │
│  │ Bins/Libs│  │ Bins/Libs│  │ Bins/Libs│                  │
│  │ Guest OS │  │ Guest OS │  │ Guest OS │  ← 每个VM有独立内核 │
│  └──────────┘  └──────────┘  └──────────┘                  │
│  ┌──────────────────────────────────────┐                   │
│  │           Hypervisor (VMM)           │  ← 硬件级隔离       │
│  └──────────────────────────────────────┘                   │
│  ┌──────────────────────────────────────┐                   │
│  │              Host Hardware            │                   │
│  └──────────────────────────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                      容器架构                                │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │
│  │  App A   │  │  App B   │  │  App C   │                  │
│  │ Bins/Libs│  │ Bins/Libs│  │ Bins/Libs│                  │
│  └──────────┘  └──────────┘  └──────────┘                  │
│  ┌──────────────────────────────────────┐                   │
│  │          Docker Engine               │  ← 容器运行时       │
│  ├──────────────────────────────────────┤                   │
│  │           Host OS Kernel             │  ← 共享内核!        │
│  └──────────────────────────────────────┘                   │
│  ┌──────────────────────────────────────┐                   │
│  │              Host Hardware            │                   │
│  └──────────────────────────────────────┘                   │
└─────────────────────────────────────────────────────────────┘

实践建议: 在安全要求极高的场景(如多租户公有云、金融核心系统),建议采用"虚拟机+容器"的混合架构——先用虚拟机实现强隔离,再在虚拟机内部运行容器,从而兼顾安全性与灵活性。实际上,大多数公有云容器服务(如AWS ECS、Azure AKS)正是采用这种模型。

1.4 容器安全攻击面分析

容器安全的攻击面可以从容器生命周期的各个阶段来分析。理解攻击面有助于我们有的放矢地部署防御措施。

1.4.1 镜像构建阶段攻击面
攻击面1: 基础镜像含漏洞 —— 使用过时或未维护的基础镜像
攻击面2: 依赖包含漏洞   —— npm/pip/maven引入含漏洞的第三方库
攻击面3: 敏感信息泄露   —— 密码、密钥、证书硬编码在Dockerfile或镜像中
攻击面4: 恶意基础镜像   —— 第三方镜像被植入后门或挖矿程序
攻击面5: 构建工具漏洞   —— Docker BuildKit或构建工具链存在安全漏洞
1.4.2 镜像分发阶段攻击面
攻击面1: 镜像仓库未授权访问 —— 私有镜像仓库缺少认证或权限控制
攻击面2: 中间人攻击         —— 镜像传输过程中被篡改(未启用TLS或内容信任)
攻击面3: 镜像投毒           —— 攻击者上传同名恶意镜像到公共仓库
攻击面4: 签名绕过           —— 未验证镜像签名,使用被篡改的镜像
1.4.3 容器运行阶段攻击面
攻击面1: 容器逃逸     —— 利用内核漏洞或配置错误逃逸到宿主机
攻击面2: 特权滥用     —— 使用--privileged或过度授权Capabilities
攻击面3: 资源耗尽     —— 无资源限制导致DoS攻击
攻击面4: 网络攻击     —— 容器间未隔离,横向移动攻击
攻击面5: 进程注入     —— 攻击者通过漏洞获取容器内执行权限
攻击面6: 挂载卷泄露   —— 敏感目录挂载到容器导致信息泄露
1.4.4 守护进程攻击面
攻击面1: Docker Socket暴露 —— 将docker.sock挂载到容器中
攻击面2: Daemon未认证     —— 远程API未启用TLS认证
攻击面3: Daemon权限过高   —— Docker Daemon以root运行

下面是一个典型的容器攻击链示意:

攻击者投毒公共镜像仓库
    ↓
开发者拉取恶意镜像(未验证签名)
    ↓
容器运行恶意镜像
    ↓
恶意程序获取容器内root权限
    ↓
利用内核漏洞或Docker配置错误进行容器逃逸
    ↓
获取宿主机root权限
    ↓
横向移动到同宿主机上的其他容器
    ↓
攻击容器编排平台(Kubernetes API Server)
    ↓
控制整个集群

通过攻击面分析,我们可以清楚地看到:容器安全不是一个单点问题,而是一个系统工程,需要在生命周期的每个环节部署相应的安全控制措施。

1.5 容器安全生命周期(构建→分发→运行→编排)

容器安全贯穿于容器的整个生命周期。业界通常将容器安全生命周期分为四个阶段:构建(Build)分发(Distribute)运行(Run)编排(Orchestrate)。每个阶段都有特定的安全目标和实践。

阶段一:构建安全(Build Security)

构建阶段的安全目标是确保产出的镜像是安全的、最小化的、不含已知漏洞和敏感信息的。

┌─────────────────────────────────────────────────────────┐
│                   构建阶段安全实践                        │
├─────────────────────────────────────────────────────────┤
│  1. 选择可信基础镜像(官方镜像/Distroless)               │
│  2. Dockerfile安全编写(非root、最小化、多阶段构建)      │
│  3. 使用.dockerignore排除敏感文件                        │
│  4. 使用BuildKit密钥管理(不硬编码密钥)                   │
│  5. 固定软件版本(固定基础镜像tag、固定依赖版本)           │
│  6. 镜像漏洞扫描(集成到CI/CD流水线)                     │
│  7. 镜像签名(Docker Content Trust)                      │
└─────────────────────────────────────────────────────────┘
阶段二:分发安全(Distribution Security)

分发阶段的安全目标是确保镜像在存储和传输过程中不被篡改,且只有授权用户才能访问。

┌─────────────────────────────────────────────────────────┐
│                   分发阶段安全实践                        │
├─────────────────────────────────────────────────────────┤
│  1. 使用私有镜像仓库并配置认证(Harbor等)                 │
│  2. 启用镜像仓库TLS加密传输                              │
│  3. 启用镜像签名验证(Notary/Cosign)                     │
│  4. 配置镜像仓库RBAC权限控制                             │
│  5. 镜像漏洞扫描(推送时扫描)                            │
│  6. 镜像保留策略(定期清理旧镜像)                         │
│  7. 镜像不可变性配置(防止覆盖已推送的镜像)               │
└─────────────────────────────────────────────────────────┘
阶段三:运行安全(Runtime Security)

运行阶段的安全目标是确保容器在运行时不会对宿主机和其他容器造成安全威胁,并能及时发现和响应安全事件。

┌─────────────────────────────────────────────────────────┐
│                   运行阶段安全实践                        │
├─────────────────────────────────────────────────────────┤
│  1. 以非root用户运行容器                                 │
│  2. 只读文件系统                                         │
│  3. 最小化Capabilities(--cap-drop ALL)                  │
│  4. 禁止特权模式                                         │
│  5. 启用Seccomp/AppArmor/SELinux                        │
│  6. 资源限制(CPU/内存/PID)                              │
│  7. 网络隔离(自定义网络+网络策略)                       │
│  8. 运行时安全监控(Falco/Sysdig)                        │
│  9. 启用no-new-privileges                                │
└─────────────────────────────────────────────────────────┘
阶段四:编排安全(Orchestration Security)

编排阶段(主要是Kubernetes)的安全目标是确保容器编排平台自身的安全配置,以及集群级别的安全控制。

┌─────────────────────────────────────────────────────────┐
│                   编排阶段安全实践                        │
├─────────────────────────────────────────────────────────┤
│  1. RBAC权限控制(最小权限原则)                          │
│  2. 网络策略(Network Policy)                            │
│  3. Pod安全策略/PSA(Pod Security Admission)            │
│  4. Secret加密存储(etcd加密)                            │
│  5. API Server安全配置(TLS、审计日志)                   │
│  6. 准入控制器(Admission Controller)                    │
│  7. 定期安全基准检查(kube-bench)                        │
└─────────────────────────────────────────────────────────┘

1.6 主流容器安全框架与标准(CIS Docker Benchmark、NIST)

为了帮助组织系统地实施容器安全,业界制定了一系列安全框架和基准标准。以下介绍两个最重要的标准。

1.6.1 CIS Docker Benchmark

CIS(Center for Internet Security)Docker Benchmark是由互联网安全中心发布的Docker安全配置基准。它提供了一套经过行业验证的最佳实践,涵盖D安全的各个方面,是目前应用最广泛的容器安全配置标准。

CIS Docker Benchmark最新版本(2.1.0版本)的主要检查项包括:

检查类别检查项数量主要内容
1. 宿主机配置10项内核版本、分离分区、审计配置
2. Docker Daemon配置15项网络TLS、日志级别、用户命名空间
3. Docker Daemon文件11项文件权限、所有权
4. 容器镜像与构建文件11项镜像创建、用户权限、内容信任
5. 容器运行时29项用户、网络、特权、Capabilities
6. Docker安全操作9项镜像扫描、审核
7. Docker Swarm配置12项mTLS、密钥管理、节点加入

使用Docker Bench Security工具可以自动化检查CIS基准:

# 下载并运行Docker Bench Security
# 该脚本会自动检查Docker环境是否符合CIS Docker Benchmark
docker run --rm --net host --pid host --userns host --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  --label docker_bench_security \
  docker/docker-bench-security

# 输出示例:
# [PASS] 1.1  - Ensure a separate partition for containers has been created
# [WARN] 1.2  - Ensure the container host has been Hardened by not automating system updates
# [NOTE] 2.1  - Ensure network traffic is restricted between containers on the default bridge
# [PASS] 4.1  - Ensure a user for the container has been created
# [WARN] 5.3  - Ensure ASCII priviledged ports are not exposed within containers
# ...
1.6.2 NIST容器安全指南

NIST(美国国家标准与技术研究院)发布了SP 800-190《Application Container Security Guide》,这是容器安全领域的权威指南。NIST容器安全指南的核心建议包括:

建议1: 容器不是虚拟机的替代品 —— 理解容器隔离的局限性,高安全场景需结合VM
建议2: 使用基于硬件的隔离技术 —— 对于多租户场景,考虑使用硬件虚拟化容器运行时(如Kata Containers)
建议3: 对镜像进行漏洞扫描     —— 在部署前扫描所有镜像中的已知漏洞
建议4: 启用内容信任           —— 使用签名验证确保镜像完整性
建议5: 不要以特权模式运行容器  —— 除非绝对必要
建议6: 使用资源限制           —— 防止资源耗尽攻击
建议7: 启用审计日志           —— 记录所有容器操作以便事后追踪
建议8: 使用最小化的基础镜像   —— 减少攻击面

此外,还有其他一些值得关注的容器安全标准和框架:

标准/框架发布组织主要关注点
CIS Kubernetes BenchmarkCISKubernetes集群安全配置
NIST SP 800-190NIST容器安全通用指南
SLSA框架Google/Linux基金会软件供应链安全等级
OCI Image SpecOCI容器镜像格式标准
PCI-DSS容器指南PCI SSC支付卡行业容器合规
FedRAMP容器指南GSA美国联邦政府云安全合规

这些框架和标准为容器安全实践提供了系统性的指导。在实际工作中,建议以CIS Docker Benchmark为基础,结合NIST指南和行业合规要求(如PCI-DSS、等保2.0),制定适合自己组织的安全策略。


第二章 容器隔离机制

2.1 Linux命名空间安全分析(PID/NET/MNT/IPC/UTS/USER)

Linux命名空间是容器隔离的基石。Docker利用命名空间为每个容器创建独立的系统资源视图。深入理解每种命名空间的安全特性及其局限性,是做好容器安全的前提。

2.1.1 PID命名空间(进程隔离)

PID命名空间隔离进程ID。在PID命名空间内,第一个进程的PID为1,该命名空间内的进程只能看到同一命名空间内的其他进程,无法看到宿主机或其他容器的进程。

# 在宿主机上查看进程
ps aux | head -5
# 可以看到宿主机上所有进程

# 进入容器查看进程
docker run -it --rm alpine ps aux
# PID  USER   TIME  COMMAND
#   1  root   0:00  ps aux   ← 容器内只能看到自己的进程

# 容器内的PID 1在宿主机上的真实PID
docker run -d --name pid-test alpine sleep 3600
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' pid-test)
echo "容器内PID 1 在宿主机上的真实PID是: $CONTAINER_PID"
# 输出: 容器内PID 1 在宿主机上的真实PID是: 12345

# 从宿主机查看该进程
ps aux | grep $CONTAINER_PID

安全分析: PID命名空间阻止容器内进程看到宿主机和其他容器的进程,防止了进程信息泄露和进程间信号攻击(如通过kill命令杀死其他进程)。但需要注意,PID命名空间本身并不能阻止进程间通过其他方式(如网络、共享文件)交互。

2.1.2 NET命名空间(网络隔离)

NET命名空间隔离网络栈,包括网络接口、IP地址、路由表、端口号、防火墙规则、/proc/net目录、/sys/class/net目录等。

# 查看宿主机的网络接口
ip addr show

# 查看容器的网络接口
docker run -it --rm alpine ip addr show
# 可以看到容器拥有独立的eth0接口,与宿主机不同

# 查看容器的路由表
docker run -it --rm alpine ip route
# default via 172.17.0.1 dev eth0

# 查看容器的iptables规则(默认情况下容器内没有iptables规则)
docker run -it --rm alpine sh -c 'apk add iptables 2>/dev/null; iptables -L'

安全分析: NET命名空间为每个容器提供独立的网络栈,实现了网络层面的基本隔离。但默认的bridge网络模式下,所有容器在同一子网内,可以互相通信。需要通过自定义网络或网络策略来加强隔离。

2.1.3 MNT命名空间(文件系统隔离)

MNT命名空间隔离文件系统挂载点。每个容器拥有独立的文件系统挂载视图,容器内的进程只能看到自己命名空间内的挂载点。

# 查看宿主机的挂载点
mount | head -10

# 查看容器的挂载点
docker run -it --rm alpine mount
# /dev/sda1 on /etc/hostname type ext3 (rw,relatime)
# /dev/sda1 on /etc/hosts type ext3 (rw,relatime)
# /dev/sda1 on /etc/resolv.conf type ext3 (rw,relatime)
# proc on /proc type proc (rw,nosuid,nodev,relatime)
# ...

# 容器无法看到宿主机的挂载点(除非显式挂载)
docker run -it --rm alpine ls /  # 只能看到容器自身的文件系统
2.1.4 IPC命名空间(进程间通信隔离)

IPC命名空间隔离System V IPC对象和POSIX消息队列。它防止容器通过共享内存等IPC机制与其他容器或宿主机通信。

# 查看宿主机的IPC信息
ipcs

# 查看容器的IPC信息
docker run -it --rm alpine ipcs
# 只能看到容器自己的IPC对象(通常为空)
2.1.5 UTS命名空间(主机名隔离)

UTS命名空间隔离主机名(hostname)和域名(NIS domain name)。每个容器可以拥有独立的主机名。

# 查看宿主机主机名
hostname

# 查看容器主机名
docker run -it --rm alpine hostname
# 输出一串随机字符(容器ID的前12位)

# 自定义容器主机名
docker run -it --rm --hostname my-container alpine hostname
# 输出: my-container
2.1.6 USER命名空间(用户隔离)

USER命名空间是最重要的安全命名空间之一。它将容器内的用户和用户组ID映射为宿主机上的不同用户和用户组ID。例如,容器内的root用户(UID 0)可以映射为宿主机上的非特权用户(如UID 100000)。

# 启用用户命名空间重映射(需要在daemon.json中配置)
# 配置文件: /etc/docker/daemon.json
cat > /etc/docker/daemon.json << 'EOF'
{
  "userns-remap": "default"
}
EOF

# 重启Docker
systemctl restart docker

# 此时容器内的root用户在宿主机上映射为非特权用户
docker run -it --rm alpine id
# uid=0(root) gid=0(root) groups=0(root)  ← 容器内仍然是root

# 但在宿主机上,该进程的实际UID是100000
docker run -d --name userns-test alpine sleep 3600
HOST_PID=$(docker inspect --format '{{.State.Pid}}' userns-test)
ps -o uid,pid,comm -p $HOST_PID
# UID为100000(而非0),即使容器内是root

2.2 命名空间的局限性与逃逸风险

虽然命名空间提供了基本的隔离,但它并非完美无缺。命名空间隔离存在以下局限性:

局限性一:共享内核漏洞

所有容器共享宿主机内核。如果内核存在安全漏洞(如Dirty COW CVE-2016-5195),攻击者可以利用这些漏洞突破命名空间隔离,实现容器逃逸。

# 检查内核版本(旧内核可能存在已知漏洞)
uname -r
# 5.15.0-91-generic

# 检查内核是否及时打了安全补丁
# 使用uname查看版本后,对照CVE数据库检查已知漏洞

局限性二:命名空间不是安全边界

命名空间的设计初衷是资源隔离,而非安全隔离。Linux内核文档明确指出:“命名空间不是安全边界”(Namespaces are not a security boundary)。这意味着不应将命名空间作为唯一的安全防线,而应结合其他安全机制(如seccomp、AppArmor/SELinux)进行纵深防御。

局限性三:特权容器可突破隔离

--privileged模式运行的容器几乎可以突破所有命名空间隔离:

# 危险操作演示:特权容器可以访问宿主机设备
docker run -it --privileged alpine sh
# 在特权容器内:
mount /dev/sda1 /mnt          # 挂载宿主机磁盘
chroot /mnt                    # 切换根目录到宿主机文件系统
# 此时攻击者已完全控制宿主机!

局限性四:Docker Socket挂载导致逃逸

# 危险操作:将Docker Socket挂载到容器中
docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker:cli
# 在容器内:
docker run -it -v /:/host alpine chroot /host
# 通过Docker Socket启动新容器并挂载宿主机根目录,实现逃逸

著名的容器逃逸漏洞:

CVE编号漏洞名称影响
CVE-2019-5736runc容器逃逸攻击者可覆盖宿主机上的runc二进制文件,获取宿主机root权限
CVE-2016-5195Dirty COW内核竞态条件漏洞,可提升权限
CVE-2022-0185内核文件系统上下文溢出可用于容器逃逸
CVE-2022-0492cgroup release_agent可用于容器逃逸
CVE-2024-21626runc文件描述符泄露容器逃逸漏洞

2.3 cgroups安全限制(CPU/内存/IO/PID)

cgroups(Control Groups)是Linux内核提供的资源限制、优先级分配、资源统计和进程控制机制。在容器安全中,cgroups主要用于防止资源耗尽型DoS攻击。

2.3.1 CPU限制
# 限制容器使用的CPU核心数
docker run -d --name cpu-test --cpus="1.5" nginx
# 容器最多使用1.5个CPU核心

# 限制CPU份额(相对权重,非绝对限制)
docker run -d --cpu-shares=512 nginx
# 默认值为1024,设置为512表示权重减半

# 绑定CPU核心
docker run -d --cpuset-cpus="0,1" nginx
# 容器只能在CPU 0和CPU 1上运行

# 查看容器的CPU限制
cat /sys/fs/cgroup/cpu/docker/<容器ID>/cpu.cfs_quota_us
# 150000 (表示每100000微秒周期内可使用150000微秒CPU时间,即1.5个CPU)

# 测试CPU限制(运行压力测试)
docker run -it --rm --cpus="0.5" alpine sh -c \
  'apk add --no-cache stress-ng 2>/dev/null && stress-ng --cpu 4 --timeout 10s --metrics'
# 即使指定4个CPU压力线程,实际CPU使用率也被限制在0.5个CPU
2.3.2 内存限制
# 限制容器最大内存使用量
docker run -d --name mem-test --memory="512m" nginx

# 限制内存+swap总量
docker run -d --memory="512m" --memory-swap="1g" nginx
# 内存限制512MB,内存+swap总共限制1GB(swap可用512MB)

# 设置内存软限制(在内存紧张时优先回收)
docker run -d --memory-reservation="256m" nginx

# 设置OOM Kill优先级(值越低越优先被杀死)
docker run -d --oom-kill-disable --memory="512m" nginx  # 禁止OOM Kill(危险!)
docker run -d --oom-score-adj=500 nginx  # 调整OOM优先级

# 测试内存限制
docker run -it --rm --memory="100m" alpine sh -c \
  'apk add --no-cache stress-ng 2>/dev/null && stress-ng --vm 1 --vm-bytes 200M --timeout 5s'
# 容器尝试分配200MB内存但限制为100MB,进程将被OOM Kill
2.3.3 IO限制
# 限制磁盘读写速率
docker run -d --device-read-bps="/dev/sda:10mb" nginx    # 读限制10MB/s
docker run -d --device-write-bps="/dev/sda:10mb" nginx   # 写限制10MB/s

# 限制IO操作次数(IOPS)
docker run -d --device-read-iops="/dev/sda:1000" nginx   # 读1000 IOPS
docker run -d --device-write-iops="/dev/sda:1000" nginx  # 写1000 IOPS

# 设置IO权重(相对优先级)
docker run -d --blkio-weight=500 nginx  # 默认500,范围10-1000
2.3.4 PID限制

PID限制可以防止fork炸弹(fork bomb)攻击:

# 限制容器内最大进程数
docker run -d --name pids-test --pids-limit=100 nginx
# 容器内最多只能有100个进程

# 测试fork炸弹防护
# 不加限制(危险,可能导致宿主机崩溃,请勿在生产环境执行)
# docker run -it --rm --pids-limit=0 alpine sh -c ':(){ :|: & };:'

# 加限制后(安全)
docker run -it --rm --pids-limit=100 alpine sh -c ':(){ :|: & };:'
# fork炸弹很快达到100进程限制,被自动阻止
# 输出: sh: can't fork: Resource temporarily unavailable

2.4 Linux Capabilities机制详解

Linux Capabilities机制将传统的root权限细分为近40种独立的能力。每种能力对应一类特定的特权操作。这种细粒度的权限控制使得容器可以只获取所需的最小权限,而非完整的root权限。

# 查看系统支持的所有Capabilities
# 需要安装libcap工具包
apt-get install -y libcap2-bin  # Debian/Ubuntu
# 或
yum install -y libcap           # CentOS/RHEL

capsh --print
# 输出当前进程的所有Capabilities
# Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,...

# 查看所有Capabilities的完整列表和说明
man capabilities

以下是一些关键的Capabilities及其安全影响:

Capability说明安全风险
CAP_SYS_ADMIN系统管理(最强大的能力)几乎等价于root,可挂载文件系统、修改网络配置等
CAP_NET_ADMIN网络管理可修改网络配置、防火墙规则、网络接口
CAP_SYS_PTRACE进程追踪可附加到其他进程,读取/修改其内存
CAP_SYS_MODULE加载内核模块可加载恶意内核模块,完全控制系统
CAP_DAC_OVERRIDE绕过文件权限检查可读写任意文件,绕过读写权限
CAP_NET_RAW原始网络访问可构造任意网络包,进行网络攻击
CAP_SYS_CHROOT修改根目录可能用于chroot逃逸
CAP_SETUID修改进程UID可切换到任意用户身份
CAP_KILL发送信号可向任意进程发送信号(包括SIGKILL)

2.5 Docker默认丢弃的Capabilities

Docker默认只授予容器一小部分Capabilities,丢弃了大量危险的能力。理解默认授予和丢弃的能力列表,对于安全配置至关重要。

# 查看Docker容器默认拥有的Capabilities
docker run --rm alpine sh -c \
  'apk add --no-cache libcap 2>/dev/null && capsh --print'

# 默认授予的Capabilities(约14个):
# cap_chown              - 修改文件所有者
# cap_dac_override       - 绕过文件读写权限检查
# cap_fowner             - 绕过文件所有者权限检查
# cap_fsetid             - 设置setuid位
# cap_kill               - 发送信号
# cap_setgid             - 设置GID
# cap_setuid             - 设置UID
# cap_setpcap            - 设置进程能力
# cap_net_bind_service   - 绑定1024以下端口
# cap_net_raw            - 原始网络包访问
# cap_sys_chroot         - chroot
# cap_mknod              - 创建设备文件
# cap_audit_write        - 审计日志写入
# cap_setfcap            - 设置文件能力

# 默认丢弃的危险Capabilities(部分):
# cap_sys_admin          - 系统管理(最危险)
# cap_sys_module         - 加载内核模块
# cap_sys_ptrace         - 进程追踪
# cap_sys_boot           - 系统重启
# cap_sys_nice           - 修改进程优先级
# cap_sys_time           - 修改系统时间
# cap_sys_tty_config     - TTY配置
# cap_linux_immutable    - 设置不可变文件
# cap_net_broadcast      - 网络广播
# cap_net_admin          - 网络管理
# cap_ipc_lock           - IPC锁定
# cap_ipc_owner          - IPC所有者
# cap_sys_pacct          - 进程记账
# cap_sys_rawio          - 原始IO操作
# cap_block_suspend      - 阻止系统挂起
# cap_mac_admin          - MAC管理
# cap_mac_override       - MAC覆盖
# cap_wake_alarm         - 唤醒报警
# cap_audit_control      - 审计控制
# cap_audit_read         - 审计读取
# cap_bpf                - BPF操作
# cap_perfmon            - 性能监控

安全实践:丢弃所有Capabilities,只添加必要的

# 最安全做法:丢弃所有Capabilities
docker run --rm --cap-drop=ALL alpine sh -c 'id && ls /'

# 如果需要绑定80/443端口,只添加NET_BIND_SERVICE
docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx

# 如果需要Ping功能,只添加NET_RAW
docker run --rm --cap-drop=ALL --cap-add=NET_RAW alpine ping -c 1 8.8.8.8

# 绝对不要添加SYS_ADMIN(除非完全理解风险)
# 以下命令极其危险,仅作演示说明,请勿在生产环境使用:
# docker run --rm --cap-add=SYS_ADMIN alpine

2.6 Seccomp(安全计算模式)详解

Seccomp(Secure Computing Mode)是Linux内核提供的一种系统调用过滤机制。它可以限制进程能执行的系统调用,从而减小内核的攻击面。Docker默认为每个容器启用seccomp,使用一个默认的profile阻止了约44个危险的系统调用。

2.6.1 Seccomp工作原理

Seccomp通过BPF(Berkeley Packet Filter)过滤器在内核态拦截系统调用。当进程发起系统调用时,seccomp过滤器会检查该调用是否被允许,如果被阻止,进程将收到SIGSYS信号或获得EPERM错误。

# 查看Docker默认的seccomp profile
# Docker默认profile位于:
# /etc/docker/seccomp-default.json (或Docker安装目录中)

# 查看容器是否启用了seccomp
docker run --rm alpine sh -c 'grep Seccomp /proc/1/status'
# Seccomp: 2  (2表示seccomp filter模式,即已启用过滤)

# 查看当前seccomp状态
# 0: 禁用  1: strict模式  2: filter模式
2.6.2 Docker默认seccomp profile

Docker默认seccomp profile阻止了以下危险的系统调用:

// Docker默认seccomp profile阻止的部分系统调用
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "_sysctl",           // 修改内核参数
        "bpf",               // 加载BPF程序(可能被滥用)
        "clone",             // 创建进程(有限制地允许)
        "fanotify_init",     // 文件系统事件监控
        "kexec_file_load",   // 加载新内核
        "kexec_load",        // 加载新内核
        "keyctl",            // 内核密钥管理
        "lookup_dcookie",    // 查找dcookie
        "mount",             // 挂载文件系统
        "move_mount",        // 移动挂载点
        "name_to_handle_at", // 文件句柄操作
        "open_by_handle_at", // 通过句柄打开文件
        "perf_event_open",   // 性能事件监控
        "pivot_root",        // 修改根文件系统
        "ptrace",            // 进程追踪
        "reboot",            // 重启系统
        "setdomainname",     // 设置域名
        "sethostname",       // 设置主机名
        "setns",             // 加入命名空间
        "swapoff",           // 关闭swap
        "swapon",            // 开启swap
        "umount",            // 卸载文件系统
        "umount2",           // 卸载文件系统
        "unshare"            // 创建新命名空间
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
2.6.3 自定义Seccomp Profile
// 文件: custom-seccomp.json
// 自定义seccomp profile示例:阻止容器内执行新程序
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86"
  ],
  "syscalls": [
    {
      "names": [
        "execve",
        "execveat"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}
# 使用自定义seccomp profile运行容器
docker run --rm --security-opt seccomp=custom-seccomp.json alpine sh -c 'ls /'
# 正常执行

docker run --rm --security-opt seccomp=custom-seccomp.json alpine sh -c 'cat /etc/hostname'
# 正常执行

docker run --rm --security-opt seccomp=custom-seccomp.json alpine sh -c 'ls -la'
# 正常执行(ls是shell内置命令,不调用execve)

# 但尝试执行新程序会被阻止
docker run --rm --security-opt seccomp=custom-seccomp.json alpine sh -c '/bin/ls /'
# 报错: OCI runtime exec failed: ...

# 禁用seccomp(不推荐,仅用于调试)
docker run --rm --security-opt seccomp=unconfined alpine sh -c 'grep Seccomp /proc/1/status'
# Seccomp: 0  (已禁用)

2.7 AppArmor与SELinux强制访问控制

AppArmor和SELinux是Linux内核提供的两种强制访问控制(MAC)安全模块。它们在传统DAC(自主访问控制)的基础上,提供了额外的安全策略层。

2.7.1 AppArmor

AppArmor(Application Armor)通过路径(path-based)的方式限制程序对文件和资源的访问。它是Ubuntu和Debian系统默认的MAC模块。

# 检查AppArmor是否已启用
sudo apparmor_status
# apparmor module is loaded.
# profiles are loaded.

# 查看已加载的AppArmor profiles
sudo aa-status

# Docker默认的AppArmor profile
docker run --rm alpine sh -c 'cat /proc/1/attr/current'
# 输出类似于: docker-default (enforce)

# 创建自定义AppArmor profile
# 文件: /etc/apparmor.d/docker-custom
cat > /etc/apparmor.d/docker-custom << 'PROFILE'
#include <tunables/global>

profile docker-custom flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  # 禁止写入/proc/sys
  deny /proc/sys/[^k]** w,
  deny /proc/sys/k[^e]** w,
  deny /proc/sys/ke[^r]** w,
  deny /proc/sys/ker[^n]** w,
  deny /proc/sys/kern[^e]** w,
  deny /proc/sys/kerne[^l]** w,
  deny /proc/sys/kernel/[^s]** w,

  # 禁止挂载操作
  deny mount,
  deny umount,

  # 禁止ptrace
  deny ptrace,

  # 允许网络
  network,
  capability,
  file,
  umount,
}
PROFILE

# 加载自定义profile
sudo apparmor_parser -r /etc/apparmor.d/docker-custom

# 使用自定义AppArmor profile运行容器
docker run --rm --security-opt apparmor=docker-custom alpine sh
2.7.2 SELinux

SELinux(Security-Enhanced Linux)通过标签(label-based)的方式实现强制访问控制。它是CentOS、RHEL和Fedora系统默认的MAC模块。

# 检查SELinux状态
sestatus
# SELinux status: enabled
# Current mode: enforcing

# 查看容器的SELinux标签
docker run --rm alpine sh -c 'cat /proc/self/attr/current'
# 输出SELinux上下文标签

# 使用SELinux标签运行容器
# :c表示container类型
docker run --rm --security-opt label=type:container_t alpine sh -c 'id'

# 为容器指定多类别安全(MCS)标签
docker run --rm --security-opt label=level:s0:c100,c200 alpine sh

# Docker的SELinux策略
# container_t     - 容器默认类型
# container_ro_t  - 只读容器文件
# container_var_run_t - 容器运行时文件
# svirt_lxc_net_t - 虚拟化容器类型

# 禁用SELinux(不推荐)
docker run --rm --security-opt label=disable alpine sh

AppArmor与SELinux对比:

特性AppArmorSELinux
访问控制模型基于路径(Path-based)基于标签(Label-based)
配置复杂度较低较高
默认发行版Ubuntu, DebianCentOS, RHEL, Fedora
学习曲线较平缓较陡峭
粒度中等细粒度
策略语言自有语法自有语法
Docker支持默认启用需要SELinux已启用

2.8 Rootless Docker(无根模式运行)

Rootless Docker是Docker的一项重要安全特性,它允许Docker Daemon和容器以非root用户身份运行。这大大降低了Docker Daemon被攻破后的影响范围——即使攻击者通过容器逃逸获取了Docker Daemon的权限,也只能获得普通用户权限,而非root权限。

# 安装Rootless Docker(以普通用户身份执行)
# 前置条件:安装uidmap和slirp4netns
sudo apt-get install -y uidmap slirp4netns  # Debian/Ubuntu
# 或
sudo yum install -y shadow-utils slirp4netns  # CentOS/RHEL

# 设置subuid和subgid(为当前用户分配UID/GID范围)
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

# 安装Rootless Docker
curl -fsSL https://get.docker.com/rootless | sh

# 配置环境变量
cat >> ~/.bashrc << 'EOF'
export DOCKER_HOST=unix:///run/user/1000/docker.sock
export PATH=$HOME/bin:$PATH
EOF
source ~/.bashrc

# 启动Rootless Docker Daemon
systemctl --user start docker
systemctl --user enable docker

# 验证Rootless Docker运行
docker info
# Security Options:
#   seccomp
#   rootless
#   ...

# Rootless模式下,即使容器逃逸,攻击者也只获得普通用户权限
docker run -it --rm alpine id
# uid=0(root) gid=0(root)  ← 容器内仍然是root
# 但实际映射到宿主机的普通用户

# Rootless模式的网络限制
# 默认使用slirp4netns网络,不支持ping(ICMP)
docker run --rm alpine ping -c 1 8.8.8.8
# 需要额外配置才能支持ICMP

# Rootless模式的端口映射(需要特殊配置)
docker run -d -p 8080:80 nginx
# 在Rootless模式下,需要配置systemd的端口转发

Rootless Docker的限制:

  • 不能使用--privileged模式
  • 不能挂载某些设备文件
  • 网络性能略有降低(使用slirp4netns用户态网络)
  • 不能使用overlayfs存储驱动(需要fuse-overlayfs)
  • 端口绑定1024以下端口需要额外配置

2.9 各隔离机制对比与协同

容器安全的各个隔离机制不是孤立的,它们相互配合、层层设防,构成了容器的纵深防御体系。

┌──────────────────────────────────────────────────────────────────┐
│                    容器安全纵深防御体系                           │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  第1层: 命名空间(Namespaces) ── 资源隔离                          │
│    PID/NET/MNT/IPC/UTS/USER → 让容器看到独立的世界                │
│                                                                  │
│  第2层: cgroups ── 资源限制                                       │
│    CPU/内存/IO/PID限制 → 防止资源耗尽攻击                         │
│                                                                  │
│  第3层: Capabilities ── 权限细分                                  │
│    丢弃危险能力 → 最小权限原则                                    │
│                                                                  │
│  第4层: Seccomp ── 系统调用过滤                                   │
│    阻止危险系统调用 → 缩小内核攻击面                              │
│                                                                  │
│  第5层: AppArmor/SELinux ── 强制访问控制                         │
│    文件/资源访问策略 → 额外的安全策略层                           │
│                                                                  │
│  第6层: Rootless Docker ── 降低Daemon权限                        │
│    非root运行Daemon → 限制逃逸后的影响范围                        │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘
隔离机制防御目标配置方式性能影响
命名空间资源视图隔离自动启用极小
cgroups资源耗尽攻击docker run参数极小
Capabilities权限滥用–cap-drop/–cap-add
Seccomp内核攻击面–security-opt seccomp极小
AppArmor/SELinux文件访问控制–security-opt apparmor/label较小
Rootless DockerDaemon权限独立安装配置网络/存储略降

最佳实践建议: 在生产环境中,应同时启用所有安全层次。不要依赖单一的隔离机制。以下是一个安全加固的容器运行命令模板:

# 安全加固的容器运行命令模板
docker run -d \
  --name secure-app \
  --user 1000:1000 \                                          # 非root用户运行
  --read-only \                                               # 只读文件系统
  --cap-drop=ALL \                                            # 丢弃所有能力
  --cap-add=NET_BIND_SERVICE \                                # 只添加必要能力
  --security-opt no-new-privileges \                          # 禁止获取新权限
  --security-opt seccomp=/path/to/seccomp-profile.json \      # 自定义seccomp
  --security-opt apparmor=docker-default \                    # 启用AppArmor
  --memory="512m" \                                           # 内存限制
  --cpus="1.0" \                                              # CPU限制
  --pids-limit=200 \                                          # 进程数限制
  --tmpfs /tmp:rw,size=64m \                                  # 临时文件系统
  --network=secure-network \                                  # 自定义隔离网络
  --restart=unless-stopped \
  my-app:latest

第三章 镜像安全

镜像安全是容器安全的源头。一个不安全的镜像,无论运行时安全配置多么完善,都无法保证整体安全。本章将从镜像来源、签名验证、漏洞扫描、供应链安全等多个维度,全面讲解镜像安全。

3.1 镜像来源安全(官方镜像 vs 第三方镜像)

容器镜像的来源直接决定了其安全基线。不同来源的镜像在安全性上存在显著差异。

3.1.1 Docker Hub官方镜像

Docker Hub官方镜像是由Docker公司或上游软件项目维护的镜像,通常经过基本的安全审查。但"官方"并不意味着"绝对安全"——官方镜像也可能包含已知漏洞,需要定期扫描和更新。

# 使用官方镜像(不带仓库名表示Docker Hub官方镜像)
docker pull nginx:1.25-alpine       # 官方nginx镜像,固定版本+alpine基础
docker pull postgres:16-alpine      # 官方postgres镜像
docker pull redis:7-alpine          # 官方redis镜像

# 查看镜像的详细信息
docker inspect nginx:1.25-alpine | jq '.[0].Config.Labels'
# 查看镜像标签中的元信息

# 查看镜像的层级结构
docker history nginx:1.25-alpine
# IMAGE          CREATED       CREATED BY                                      SIZE
# 321340f8e9d0   2 weeks ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B
# ...
3.1.2 第三方镜像的风险

第三方镜像是最大的安全风险来源。任何人都可以向Docker Hub推送镜像,其中可能包含恶意代码、后门或大量已知漏洞。

# 危险:使用未经验证的第三方镜像
docker pull randomuser/some-app:latest  # 极其危险!

# 检查镜像的下载量、评分和更新时间
# 在Docker Hub网页上查看镜像信息:
# - Pulls(下载量):下载量过低需警惕
# - Stars(评分):评分过低需警惕
# - Last Pushed(最后更新):长期未更新可能有安全风险
# - Dockerfile链接:是否有公开的Dockerfile可供审查

# 验证镜像的发布者是否可信
docker pull --quiet nginx@sha256:<具体的digest值>  # 使用digest而非tag拉取
# 使用digest可以确保拉取到特定版本的镜像,防止被替换

镜像来源安全建议:

镜像来源信任等级建议措施
Docker Hub官方镜像仍需定期扫描,固定版本
企业私有仓库镜像中高配置认证、签名、扫描
Docker Hub认证发布者(Docker Official)中高验证发布者身份
Docker Hub社区镜像必须扫描,审查Dockerfile
未知来源镜像极低禁止使用

3.2 镜像签名与验证(Docker Content Trust, Notary)

镜像签名与验证是确保镜像完整性和来源可信的关键机制。Docker Content Trust(DCT)利用Notary项目实现镜像的数字签名和验证。

3.2.1 启用Docker Content Trust
# 通过环境变量启用Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# 启用后,拉取镜像时会自动验证签名
docker pull nginx:latest
# 如果镜像未签名,拉取将失败
# Error: remote trust data does not exist

# 推送镜像时自动签名
docker push myregistry.com/my-app:v1.0
# Signing and pushing trust data

# 在daemon.json中全局启用内容信任
cat > /etc/docker/daemon.json << 'EOF'
{
  "content-trust": {
    "mode": "enforced"
  }
}
EOF
3.2.2 Notary签名工作流程
镜像签名流程:
1. 开发者构建镜像 → docker build -t my-app:v1.0 .
2. 推送镜像到仓库 → docker push my-app:v1.0
3. DCT自动生成签名密钥(首次推送时)
4. 使用私钥对镜像进行签名
5. 签名数据存储在Notary服务器
6. 镜像数据存储在镜像仓库

镜像验证流程:
1. 用户拉取镜像 → docker pull my-app:v1.0
2. DCT从Notary服务器获取签名数据
3. 使用公钥验证签名
4. 签名验证通过 → 拉取镜像
5. 签名验证失败 → 拒绝拉取
# 使用Notary客户端管理签名
# 安装Notary客户端
wget https://github.com/theupdateframework/notary/releases/download/v0.7.0/notary-Linux-amd64
chmod +x notary-Linux-amd64
mv notary-Linux-amd64 /usr/local/bin/notary

# 查看镜像的签名信息
notary -s https://notary.docker.io lookup docker.io/library/nginx

# 查看镜像签名列表
notary -s https://notary.docker.io list docker.io/library/nginx

# 初始化签名(生成密钥对)
notary -s https://notary.docker.io init docker.io/myuser/my-app

# 发布签名
notary -s https://notary.docker.io publish docker.io/myuser/my-app

# 导出公钥(用于验证)
notary -s https://notary.docker.io key export docker.io/myuser/my-app pub.key

# 导入公钥(在其他机器上验证)
notary key import pub.key --role targets
3.2.3 使用Cosign进行镜像签名(现代方案)

Cosign是Sigstore项目提供的现代镜像签名工具,比Notary更简单易用。

# 安装Cosign
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
mv cosign-linux-amd64 /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign

# 生成密钥对
cosign generate-key-pair
# 生成 cosign.key(私钥) 和 cosign.pub(公钥)

# 对镜像进行签名
cosign sign --key cosign.key myregistry.com/my-app:v1.0
# 输入私钥密码后完成签名

# 验证镜像签名
cosign verify --key cosign.pub myregistry.com/my-app:v1.0

# 使用OIDC进行无密钥签名(keyless signing)
cosign sign --identity-token <token> myregistry.com/my-app:v1.0

# 在Kubernetes中自动验证签名(使用Kyverno)
# Kyverno策略:只允许运行已签名的镜像

3.3 镜像漏洞扫描(Trivy、Clair、Anchore、Snyk)

镜像漏洞扫描是发现镜像中已知安全漏洞的关键手段。以下是主流镜像扫描工具的对比和使用方法。

3.3.1 Trivy

Trivy是由Aqua Security开发的开源漏洞扫描工具,支持扫描镜像、文件系统、Git仓库等,是目前最受欢迎的容器漏洞扫描工具之一。

# 安装Trivy
# Debian/Ubuntu
sudo apt-get install trivy
# 或通过二进制安装
wget https://github.com/aquasecurity/trivy/releases/latest/download/trivy_0.50.0_Linux-64bit.deb
sudo dpkg -i trivy_0.50.0_Linux-64bit.deb

# 扫描Docker镜像
trivy image nginx:1.25-alpine
# 输出:
# nginx:1.25-alpine (alpine 3.19)
# Total: 2 (HIGH: 1, CRITICAL: 0, MEDIUM: 1, LOW: 0)
#
# ┌──────────────────┬────────────────┬──────────┬───────────────┬───────────────┬──────────────────────────────────┐
# │     Library      │ Vulnerability  │ Severity │ Fixed Version │ Current Version│            Description            │
# ├──────────────────┼────────────────┼──────────┼───────────────┼───────────────┼──────────────────────────────────┤
# │ libcrypto3       │ CVE-2024-0727  │ HIGH     │ 3.1.4-r5      │ 3.1.4-r4      │ OpenSSL DoS vulnerability         │
# │ libssl3          │ CVE-2024-0727  │ HIGH     │ 3.1.4-r5      │ 3.1.4-r4      │ OpenSSL DoS vulnerability         │
# └──────────────────┴────────────────┴──────────┴───────────────┴───────────────┴──────────────────────────────────┘

# 扫描特定严重级别的漏洞
trivy image --severity HIGH,CRITICAL nginx:1.25-alpine

# 输出为JSON格式(用于CI/CD集成)
trivy image --format json --output scan-results.json nginx:1.25-alpine

# 设置扫描退出码(发现漏洞时CI/CD失败)
trivy image --exit-code 1 --severity HIGH,CRITICAL nginx:1.25-alpine
# 如果发现HIGH或CRITICAL漏洞,退出码为1,CI/CD流水线将失败

# 扫描本地文件系统(用于扫描Dockerfile或IaC文件)
trivy fs --severity HIGH,CRITICAL /path/to/project

# 扫描Dockerfile
trivy config Dockerfile
# 检查Dockerfile中的安全配置问题

# 扫描Kubernetes配置文件
trivy config k8s-deployment.yaml

# 使用忽略文件排除已知无关漏洞
cat > .trivyignore << 'EOF'
CVE-2024-0727  # 已知风险但暂不修复
CVE-2023-1234
EOF
trivy image --ignorefile .trivyignore nginx:1.25-alpine
3.3.2 Clair

Clair是CoreOS(现Red Hat)开发的开源镜像漏洞扫描工具,主要被Quay容器仓库集成使用。

# 使用Docker Compose部署Clair
cat > docker-compose.yml << 'EOF'
version: '3'
services:
  clair:
    image: quay.io/coreos/clair:latest
    ports:
      - "6060:6060"
      - "6061:6061"
    environment:
      - CLAIR_MODE=combo
    volumes:
      - ./config:/config
    depends_on:
      - postgres
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: clair
      POSTGRES_DB: clair
EOF

# 启动Clair
docker-compose up -d

# 使用Clair API扫描镜像
# 分析镜像层
curl -X POST http://localhost:6060/v1/layers \
  -H "Content-Type: application/json" \
  -d '{
    "Layer": {
      "Name": "layer1",
      "Path": "nginx:1.25-alpine",
      "Format": "Docker"
    }
  }'

# 获取漏洞报告
curl http://localhost:6060/v1/layers/layer1/vulnerabilities
3.3.3 Anchore Engine

Anchore Engine提供策略驱动的镜像安全分析,支持自定义安全策略。

# 部署Anchore Engine
docker run -d --name anchore-db postgres:15
docker run -d --name anchore-engine \
  -e ANCHORE_DB_HOST=anchore-db \
  -e ANCHORE_DB_USER=postgres \
  -e ANCHORE_DB_PASSWORD=anchore \
  -p 8228:8228 \
  anchore/anchore-engine:latest

# 添加镜像进行扫描
anchore-cli image add nginx:1.25-alpine

# 查看扫描结果
anchore-cli image vuln nginx:1.25-alpine all

# 查看策略评估结果
anchore-cli evaluate check nginx:1.25-alpine

# 自定义安全策略
anchore-cli policy add --policy-file custom-policy.json

主流扫描工具对比:

工具开源扫描速度易用性CI/CD集成主要特点
Trivy优秀一体化扫描,支持IaC
Clair良好仓库集成,API驱动
Anchore良好策略驱动,深度分析
Snyk商业优秀持续监控,修复建议
Grype优秀与Syft配合,SBOM
Aqua商业优秀企业级,运行时防护

3.4 镜像中的敏感信息泄露防范

镜像中硬编码敏感信息(密码、密钥、证书、Token等)是最常见的安全错误之一。这些信息一旦被打包进镜像,就可能被任何能获取该镜像的人提取出来。

3.4.1 常见的敏感信息泄露方式
# 危险示例1: 在ENV中硬编码密码
ENV DATABASE_PASSWORD=MySecret123!

# 危险示例2: 在ARG中传递密码(会残留在构建历史中)
ARG API_KEY=sk-1234567890abcdef

# 危险示例3: 在RUN命令中使用密码
RUN curl -u admin:password123 http://internal-registry/repo

# 危险示例4: 复制包含敏感信息的文件
COPY .env /app/.env
COPY config/secrets.json /app/secrets.json

# 危险示例5: 在COPY时包含.git目录
COPY . /app/
# .git目录中可能包含历史提交中的敏感信息
3.4.2 检测镜像中的敏感信息
# 使用Trivy扫描镜像中的敏感信息
trivy image --scanners secret nginx:1.25-alpine

# 使用TruffleHog扫描镜像
docker run --rm -it trufflesecurity/trufflehog:latest docker --image nginx:1.25-alpine

# 使用Gitleaks扫描代码仓库中的敏感信息
gitleaks detect --source /path/to/repo --report-path leaks.json

# 检查镜像的构建历史(可能泄露ARG中的敏感信息)
docker history --no-trunc my-app:v1.0
# 查看每一层的构建命令,检查是否包含敏感信息

# 提取镜像中的所有文件进行检查
docker create --name temp-container my-app:v1.0
docker export temp-container | tar -tv
docker rm temp-container
3.4.3 防范敏感信息泄露的最佳实践
# 安全实践1: 使用BuildKit secrets传递构建时密钥
# syntax=docker/dockerfile:1.4
FROM alpine:3.19
# 使用--mount=type=secret挂载密钥(不会残留在镜像层中)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install
# 密钥只在构建时可见,不会写入镜像

# 安全实践2: 使用运行时密钥管理(Docker Secrets/Vault)
FROM alpine:3.19
# 不在镜像中存储密钥,运行时从Secrets中读取
CMD ["sh", "-c", "cat /run/secrets/db_password && exec myapp"]

# 安全实践3: 使用.dockerignore排除敏感文件
# .dockerignore文件内容:
# .env
# .env.*
# config/secrets.*
# *.pem
# *.key
# .git/
# .gitignore

3.5 镜像供应链安全(SLSA框架)

SLSA(Supply-chain Levels for Software Artifacts)是Google发起、Linux基金会维护的软件供应链安全框架。它定义了从源代码到最终制品的供应链安全等级。

3.5.1 SLSA安全等级
SLSA Level 1 - 构建过程文档化
  要求: 构建过程有文档,制品有出处证明(provenance)
  目标: 知道制品是怎么构建的

SLSA Level 2 - 托管构建+受控构建
  要求: 构建在托管平台上进行,构建过程有版本控制
  目标: 构建环境可信

SLSA Level 3 - 加强构建平台安全
  要求: 构建平台有隔离构建、不可篡改的构建日志
  目标: 构建过程不可篡改

SLSA Level 4 - 双人审核+可重现构建
  要求: 两人以上审核、可重现构建、特定构建平台
  目标: 最高级别的供应链保证
3.5.2 生成SBOM(软件物料清单)

SBOM(Software Bill of Materials)是镜像中所有组件的清单,是供应链安全的基础。

# 使用Syft生成SBOM
# 安装Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# 生成SBOM(JSON格式)
syft nginx:1.25-alpine -o json > sbom.json

# 生成SBOM(CycloneDX格式)
syft nginx:1.25-alpine -o cyclonedx-json > sbom-cyclonedx.json

# 生成SBOM(SPDX格式)
syft nginx:1.25-alpine -o spdx-json > sbom-spdx.json

# 查看SBOM内容
cat sbom.json | jq '.artifacts[] | {name, version, type}' | head -20
# {
#   "name": "alpine-baselayout",
#   "version": "3.4.3-r2",
#   "type": "apk"
# }
# {
#   "name": "musl",
#   "version": "1.2.4-r1",
#   "type": "apk"
# }

# 生成镜像的出处证明(provenance)
# 使用cosign生成attestation
cosign attest --predicate sbom-cyclonedx.json \
  --type cyclonedx \
  --key cosign.key \
  myregistry.com/my-app:v1.0

# 验证attestation
cosign verify-attestation --key cosign.pub \
  --type cyclonedx \
  myregistry.com/my-app:v1.0

3.6 安全基础镜像选择(distroless、scratch、alpine)

基础镜像的选择直接影响镜像的安全性和大小。以下是几种安全基础镜像的对比。

3.6.1 scratch镜像

scratch是一个空镜像,不含任何文件,是所有镜像的"零基础"。

# 使用scratch构建最小化镜像(适合静态编译的Go程序)
FROM scratch
# 复制编译好的二进制文件
COPY myapp /myapp
# 复制CA证书(如果需要HTTPS)
COPY ca-certificates.crt /etc/ssl/certs/
# 入口点
ENTRYPOINT ["/myapp"]
# 构建scratch镜像
docker build -t myapp:scratch .

# 查看镜像大小(极小,通常只有几MB)
docker images myapp:scratch
# REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
# myapp        scratch   a1b2c3d4e5f6   10 seconds ago  2.3MB
3.6.2 Distroless镜像

Distroless是Google提供的去除了包管理器、Shell等非必要组件的基础镜像,只包含运行应用所需的最小化运行时。

# 使用Distroless运行Java应用
FROM gcr.io/distroless/java17-debian12:latest
COPY app.jar /app.jar
CMD ["/app.jar"]

# 使用Distroless运行Go应用
FROM gcr.io/distroless/static-debian12:latest
COPY myapp /myapp
CMD ["/myapp"]

# 使用Distroless运行Node.js应用
FROM gcr.io/distroless/nodejs20-debian12:latest
COPY app.js /app.js
CMD ["app.js"]

Distroless的安全优势:

  • 没有Shell(sh/bash),攻击者无法在容器内执行Shell命令
  • 没有包管理器(apt/yum/apk),攻击者无法安装恶意软件
  • 默认以非root用户运行
  • 镜像体积小,攻击面小
  • 只包含应用运行所需的最小化依赖
3.6.3 Alpine镜像

Alpine Linux是一个面向安全的轻量级Linux发行版,基础镜像仅约5MB。

FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY myapp /myapp
RUN adduser -D -h /home/appuser appuser
USER appuser
ENTRYPOINT ["/myapp"]

安全基础镜像对比:

基础镜像大小包含Shell包含包管理器安全性适用场景
scratch~0MB最高静态编译程序(Go/Rust)
Distroless~2-20MB极高Java/Node.js/Python等
Alpine~5MB是(apk)需要Shell的场景
Debian Slim~20-80MB是(apt)需要完整Linux环境的场景
Ubuntu~30MB是(apt)需要Ubuntu兼容性
CentOS/RHEL UBI~80-200MB是(yum/dnf)企业级RHEL兼容

3.7 镜像加固策略

镜像加固是在基础镜像之上进一步减少攻击面、提升安全性的过程。

# 完整的安全加固Dockerfile示例
# 使用多阶段构建
FROM golang:1.22-alpine AS builder

# 安装构建工具
RUN apk add --no-cache git ca-certificates

# 创建非root用户
RUN adduser -D -g '' appuser

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download

COPY . .
# 静态编译(不依赖CGO)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags='-w -s -extldflags "-static"' \
    -o myapp .

# 最终阶段:使用scratch或distroless
FROM scratch

# 复制CA证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 复制非root用户的passwd文件
COPY --from=builder /etc/passwd /etc/passwd

# 复制编译好的二进制
COPY --from=builder /build/myapp /myapp

# 以非root用户运行
USER appuser

# 健康检查(使用二进制自身的健康检查端点)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD ["/myapp", "--health-check"]

ENTRYPOINT ["/myapp"]

3.8 镜像安全扫描CI/CD集成

将镜像安全扫描集成到CI/CD流水线中,可以确保每个镜像在部署前都经过安全检查。

# GitLab CI/CD配置: 镜像安全扫描流水线
# 文件: .gitlab-ci.yml
stages:
  - build
  - scan
  - sign
  - deploy

variables:
  IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

# 构建阶段
build:
  stage: build
  script:
    - docker build -t $IMAGE_NAME .
    - docker push $IMAGE_NAME
  only:
    - main

# 安全扫描阶段
scan_trivy:
  stage: scan
  image: aquasec/trivy:latest
  script:
    # 扫描镜像漏洞
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME
    # 扫描镜像中的敏感信息
    - trivy image --scanners secret --exit-code 1 $IMAGE_NAME
    # 生成SBOM
    - trivy image --format cyclonedx --output sbom.json $IMAGE_NAME
  artifacts:
    reports:
      dependency_scanning: sbom.json
  only:
    - main

# 生成SBOM阶段
generate_sbom:
  stage: scan
  image: anchore/syft:latest
  script:
    - syft $IMAGE_NAME -o cyclonedx-json > sbom-syft.json
  artifacts:
    paths:
      - sbom-syft.json

# 镜像签名阶段
sign_image:
  stage: sign
  image: gcr.io/projectsigstore/cosign:latest
  script:
    # 使用Cosign对镜像签名
    - cosign sign --key $COSIGN_PRIVATE_KEY $IMAGE_NAME
  only:
    - main

# 部署阶段(只有扫描和签名都通过才部署)
deploy:
  stage: deploy
  script:
    # 验证镜像签名
    - cosign verify --key $COSIGN_PUBLIC_KEY $IMAGE_NAME
    # 部署到Kubernetes
    - kubectl apply -f k8s/
  only:
    - main
  when: manual  # 手动触发部署
# GitHub Actions配置: 镜像安全扫描
# 文件: .github/workflows/security.yml
cat > .github/workflows/security.yml << 'EOF'
name: Container Security

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'table'
          exit-code: '1'
          severity: 'HIGH,CRITICAL'

      - name: Run Trivy secret scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          scanners: 'secret'
          exit-code: '1'

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: myapp:${{ github.sha }}
          format: cyclonedx-json

      - name: Sign image with Cosign
        uses: sigstore/cosign-installer@v3
        with:
          cosign-release: 'v2.2.0'

      - run: |
          echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key
          cosign sign --key cosign.key --yes myapp:${{ github.sha }}
EOF

3.9 镜像安全审计清单

以下是一个完整的镜像安全审计清单,可用于定期审查镜像安全性:

□ 镜像来源安全
  □ 使用可信来源的官方或私有仓库镜像
  □ 使用digest而非tag固定镜像版本
  □ 验证镜像签名(DCT/Cosign)
  □ 不使用latest标签

□ 镜像内容安全
  □ 使用最小化基础镜像(scratch/distroless/alpine)
  □ 镜像不包含已知HIGH/CRITICAL漏洞
  □ 镜像中不包含敏感信息(密码/密钥/证书)
  □ 镜像不包含不必要的工具(Shell/包管理器/编译器)
  □ 定期更新基础镜像

□ 镜像构建安全
  □ Dockerfile遵循安全编写规范
  □ 使用多阶段构建
  □ 使用.dockerignore排除敏感文件
  □ 构建时使用BuildKit secrets管理密钥
  □ 固定所有软件包版本

□ 镜像分发安全
  □ 私有镜像仓库启用认证和TLS
  □ 镜像仓库配置RBAC权限控制
  □ 启用镜像不可变性(immutable tags)
  □ 定期清理旧镜像版本
  □ 推送时自动扫描

□ 镜像合规性
  □ 符合CIS Docker Benchmark
  □ 符合组织内部安全策略
  □ 定期生成安全审计报告
  □ 保留SBOM和签名记录

第四章 Dockerfile安全编写

Dockerfile是容器镜像构建的核心。一个安全编写的Dockerfile可以从源头消除大量安全风险。本章将系统讲解Dockerfile安全编写的各项原则和实践。

4.1 最小权限原则(非root用户运行)

默认情况下,Docker容器以root用户运行。这意味着如果攻击者利用应用漏洞获得了容器内的代码执行权限,他们将直接拥有root权限,大幅增加了容器逃逸的风险。因此,Dockerfile中应始终创建并使用非root用户来运行应用。

# 危险示例:以root用户运行
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# 此时容器以root运行,存在安全风险

# 安全示例:创建非root用户并使用
FROM node:20-alpine

# 创建专用用户和用户组
# -D: 不创建密码 -H: 不创建home目录 -g: 设置GID
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 先复制package.json,利用Docker层缓存
COPY package*.json ./
RUN npm install --production

# 复制应用代码并设置正确的权限
COPY --chown=appuser:appgroup . .

# 确保app目录的所有权
RUN chown -R appuser:appgroup /app

# 切换到非root用户
USER appuser

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "server.js"]
# 验证容器运行的用户
docker run --rm myapp:latest id
# uid=100(appuser) gid=101(appgroup) groups=101(appgroup)

# 使用固定UID/GID(在某些场景下更安全)
FROM alpine:3.19
RUN addgroup -g 1000 -S appgroup && \
    adduser -u 1000 -S appuser -G appgroup
USER 1000:1000
# 使用固定UID可以在宿主机上更精确地映射权限

注意事项:

  • 非root用户无法绑定1024以下的端口。如果应用需要绑定80/443端口,有两种解决方案:一是使用CAP_NET_BIND_SERVICE能力,二是使用端口映射将容器内部的高端口映射到宿主机的80/443端口。
  • 确保应用所需的目录和文件对非root用户有正确的读写权限。
  • 使用USER指令后,后续的RUNCMDENTRYPOINT都将以该用户身份执行。

4.2 最小化镜像(减少攻击面)

镜像越大,包含的组件越多,潜在的攻击面就越广。最小化镜像是安全的基础原则。

# 危险示例:使用完整的基础镜像,安装大量不必要的包
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip \
    vim \
    curl \
    wget \
    git \
    net-tools \
    iputils-ping \
    && rm -rf /var/lib/apt/lists/*
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python3", "app.py"]
# 镜像大小可能超过500MB,包含大量不必要工具

# 安全示例:使用最小化基础镜像,只安装必要依赖
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN adduser -D appuser && chown -R appuser /app
USER appuser
CMD ["python3", "app.py"]
# 镜像大小可能只有50-80MB

最小化镜像的具体策略:

# 策略1: 清理包管理器缓存
RUN apk add --no-cache python3 && \
    # --no-cache已经避免了缓存,但如果有临时文件也要清理
    rm -rf /tmp/* /var/tmp/*

# 策略2: 合并RUN指令减少镜像层数
# 危险:多个RUN指令创建多个层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 以上会创建3个层,即使删除了apt缓存,中间层仍然包含它

# 安全:合并为一个RUN指令
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*
# 只创建一个层,缓存被正确清理

# 策略3: 删除不必要的文档和man页面
RUN apk add --no-cache python3 && \
    find /usr/share -type f -name '*.pyc' -delete && \
    find /usr/share -type d -name '__pycache__' -exec rm -rf {} + && \
    rm -rf /usr/share/man /usr/share/doc

# 策略4: 使用--no-install-recommends(Debian/Ubuntu)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        python3=3.10.* && \
    rm -rf /var/lib/apt/lists/*
# --no-install-recommends: 不安装"推荐"包,只安装"依赖"包

4.3 不存储敏感信息(密码、密钥、证书)

在Dockerfile中硬编码敏感信息是严重的安全错误。这些信息会被永久记录在镜像层中,即使后续删除也无法完全清除。

# === 极度危险的示例(绝对禁止!) ===

# 危险1: ENV硬编码密码
ENV DB_PASSWORD=P@ssw0rd123
ENV API_SECRET_KEY=sk-abc123xyz

# 危险2: ARG传递密钥(ARG值会出现在docker history中)
ARG NPM_TOKEN=secret_token_here
RUN npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}

# 危险3: RUN命令中直接使用密码
RUN mysql -h dbhost -u admin -ppassword123 -e "CREATE DATABASE app"

# 危险4: COPY包含密钥的配置文件
COPY secrets/ /app/secrets/
COPY .env.production /app/.env
COPY id_rsa /app/.ssh/id_rsa

# === 正确的安全实践 ===

# 实践1: 使用BuildKit secrets(构建时密钥)
# syntax=docker/dockerfile:1.4
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# 使用--mount=type=secret,密钥不会出现在镜像层中
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN \
    npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} && \
    npm install --production && \
    npm config delete //registry.npmjs.org/:_authToken
# 构建命令: docker build --secret id=npm_token,env=NPM_TOKEN .

# 实践2: 运行时通过环境变量或Secrets传递
FROM python:3.12-alpine
WORKDIR /app
COPY . .
# 不在镜像中设置密码,运行时通过环境变量或Docker Secrets传递
CMD ["python3", "app.py"]
# 运行命令: docker run -e DB_PASSWORD=$DB_PASSWORD myapp
# 或使用Docker Secrets: docker run --secret db_password myapp

4.4 使用多阶段构建减少攻击面

多阶段构建(Multi-stage Build)是减少镜像大小和攻击面的最有效手段之一。它允许在构建过程中使用多个FROM指令,最终镜像只包含最后一个阶段的产物。

# 多阶段构建示例: Go应用
# 阶段1: 构建阶段(包含编译器和源代码)
FROM golang:1.22-alpine AS builder

# 安装构建依赖
RUN apk add --no-cache git make

WORKDIR /build
# 下载依赖
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp -ldflags="-s -w" .

# 阶段2: 运行阶段(只包含编译产物)
FROM scratch

# 从构建阶段复制必要的文件
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/myapp /myapp
COPY --from=builder /etc/passwd /etc/passwd

# 创建非root用户
# 注意:scratch镜像没有adduser命令,需要从构建阶段复制passwd文件
# 在构建阶段执行: echo "appuser:x:1000:1000::/:" >> /etc/passwd

USER 1000

ENTRYPOINT ["/myapp"]
# 多阶段构建示例: Java应用
# 阶段1: Maven构建
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline  # 预下载依赖,利用缓存
COPY src ./src
RUN mvn package -DskipTests

# 阶段2: 运行阶段(使用JRE而非JDK,减少体积)
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /build/target/*.jar /app/app.jar
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget --spider -q http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

多阶段构建的安全收益:

项目单阶段构建多阶段构建
镜像大小大(包含编译器、源码)小(只包含运行时)
攻击面大(包含构建工具)小(无构建工具)
源代码泄露可能(源码在镜像中)不会(源码不进入最终镜像)
构建依赖暴露是(如gcc、make)
缓存文件可能残留不会

4.5 合理使用COPY vs ADD

Dockerfile中COPYADD指令都可以将文件复制到镜像中,但ADD有一些隐式行为可能导致安全问题。

# COPY: 简单的文件复制,行为可预测
COPY app.jar /app/app.jar
COPY config/ /app/config/
COPY *.py /app/

# ADD: 有额外的隐式行为,可能带来安全风险
# 风险1: ADD会自动解压tar压缩包
ADD app.tar.gz /app/  # 自动解压到/app/
# 如果tar包中包含恶意文件(如符号链接指向宿主机文件),可能导致安全问题

# 风险2: ADD支持远程URL(已弃用,但有遗留风险)
ADD http://example.com/file.txt /app/
# 从远程URL下载文件,存在中间人攻击风险
# 如果远程服务器被攻破,可能下载恶意文件

# 安全建议:始终使用COPY,除非确实需要ADD的解压功能
# 需要解压时,也应先验证文件再解压
COPY app.tar.gz /tmp/
RUN tar -xzf /tmp/app.tar.gz -C /app/ && rm /tmp/app.tar.gz && \
    # 验证解压后的文件权限
    find /app -type f -exec chmod 644 {} \;

4.6 固定软件版本(–no-install-recommends, 固定版本号)

固定软件版本是保证构建可重复性和安全性的重要手段。如果使用不固定版本的包,每次构建可能引入不同的(可能有漏洞的)依赖。

# 危险示例:不固定版本
FROM alpine:latest          # 使用latest,基础镜像可能随时变化
RUN apk add --no-cache curl # 不固定curl版本
RUN pip install flask       # 不固定flask版本

# 安全示例:固定所有版本
FROM alpine:3.19.1          # 固定基础镜像版本(最好用digest)
RUN apk add --no-cache \
    curl=8.5.0-r0 \         # 固定curl版本
    ca-certificates=20230506-r0
RUN pip install flask==3.0.2  # 固定flask版本

# 最佳实践:使用digest固定基础镜像(最精确)
FROM alpine@sha256:13b7e62e8dfab61c2e6545a5e57a6f2a4e0e0a99e0e4e0e0e0e0e0e0e0e0e0e0e
# digest是镜像内容的SHA256哈希,可以确保每次拉取的镜像完全一致

# 固定Python/Node.js依赖版本
# requirements.txt
# flask==3.0.2
# requests==2.31.0
# SQLAlchemy==2.0.25
# 使用 == 精确固定,不使用 >= 或 ~=

# package.json
# "dependencies": {
#   "express": "4.19.2",     // 精确版本
#   "lodash": "4.17.21"
# }
# 使用 package-lock.json 或 yarn.lock 锁定依赖树

4.7 及时更新基础镜像

基础镜像也需要定期更新以修复安全漏洞。应该建立定期更新基础镜像的流程。

# 检查当前使用的基础镜像是否有更新
docker pull alpine:3.19.1
# 如果有新版本,Docker会拉取新层

# 查看镜像的创建时间和版本信息
docker inspect alpine:3.19.1 | jq '.[0].Created'

# 使用Renovate Bot或Dependabot自动更新基础镜像
# Renovate Bot配置示例(renovate.json)
cat > renovate.json << 'EOF'
{
  "extends": ["config:recommended"],
  "docker": {
    "fileMatch": ["^Dockerfile$", "^Dockerfile\\..+$"],
    "pinDigests": true
  }
}
EOF

# GitHub Dependabot配置(.github/dependabot.yml)
cat > .github/dependabot.yml << 'EOF'
version: 2
updates:
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
EOF

4.8 使用.dockerignore排除敏感文件

.dockerignore文件类似于.gitignore,用于排除不需要复制到镜像中的文件。这是防止敏感信息泄露到镜像中的重要防线。

# .dockerignore 完整安全示例

# 版本控制
.git
.gitignore
.gitattributes

# 敏感配置文件
.env
.env.*
*.env
config/secrets.*
config/credentials.*
secrets/
*.pem
*.key
*.crt
*.p12
*.pfx
id_rsa
id_rsa.pub
known_hosts

# CI/CD配置
.github/
.gitlab-ci.yml
Jenkinsfile
.circleci/

# Docker相关
Dockerfile*
docker-compose*.yml
.dockerignore

# 文档
*.md
docs/
LICENSE

# 测试文件
test/
tests/
spec/
__tests__/
*.test.js
*.spec.js
coverage/
.nyc_output/

# 开发工具配置
.vscode/
.idea/
*.swp
*.swo
*~

# 日志文件
*.log
logs/

# 依赖目录(在镜像内安装)
node_modules/
vendor/
__pycache__/
*.pyc

# 构建产物(在多阶段构建中处理)
dist/
build/
target/
*.egg-info/

# 操作系统文件
.DS_Store
Thumbs.db
# 验证.dockerignore是否生效
# 创建测试文件
echo "SECRET_KEY=super_secret" > .env
echo "password" > secrets.txt

# 构建镜像
docker build -t test-image .

# 检查敏感文件是否被排除
docker run --rm test-image ls -la /app/
# 不应看到.env和secrets.txt文件

docker run --rm test-image find / -name ".env" 2>/dev/null
# 不应有输出

4.9 使用BuildKit密钥管理(–mount=type=secret)

BuildKit是Docker的新一代构建引擎,提供了更强的密钥管理能力。使用--mount=type=secret可以在构建时安全地传递密钥,而不会将密钥写入镜像层。

# 使用BuildKit secret的Dockerfile
# syntax=docker/dockerfile:1.4

FROM node:20-alpine AS builder
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 方式1: 使用secret挂载npm token
# id指定secret的标识符,env指定环境变量名
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN \
    # 配置npm使用token
    echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > /root/.npmrc && \
    npm install --production && \
    # 安装完成后删除npmrc(虽然不写入镜像层,但安全起见还是删除)
    rm /root/.npmrc

# 方式2: 使用secret挂载SSH密钥(用于从私有Git仓库拉取代码)
RUN --mount=type=ssh \
    # 使用SSH密钥从私有仓库拉取依赖
    git clone git@github.com:myorg/private-repo.git /tmp/private && \
    cd /tmp/private && npm install && \
    cp -r node_modules /app/node_modules && \
    rm -rf /tmp/private

# 方式3: 使用secret挂载SSL证书(用于从私有HTTPS仓库拉取)
RUN --mount=type=secret,id=ssl_cert,target=/etc/ssl/certs/private.crt \
    curl --cacert /etc/ssl/certs/private.crt \
    https://internal-registry.com/package.tar.gz -o /tmp/package.tar.gz && \
    tar -xzf /tmp/package.tar.gz -C /app/ && \
    rm /tmp/package.tar.gz

COPY . .
RUN npm run build

# 最终阶段
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# 使用BuildKit构建(需要启用BuildKit)
# 方式1: 通过环境变量启用
export DOCKER_BUILDKIT=1

# 方式2: 在daemon.json中永久启用
cat > /etc/docker/daemon.json << 'EOF'
{
  "features": {
    "buildkit": true
  }
}
EOF

# 构建时传递secrets
# 方式1: 从环境变量传递
export NPM_TOKEN=your_npm_token_here
docker build --secret id=npm_token,env=NPM_TOKEN -t myapp .

# 方式2: 从文件传递
echo "your_npm_token_here" > /tmp/npm_token
docker build --secret id=npm_token,src=/tmp/npm_token -t myapp .
rm /tmp/npm_token  # 构建完成后删除临时文件

# 构建时传递SSH密钥
docker build --ssh default=$SSH_AUTH_SOCK -t myapp .

# 验证密钥没有泄露到镜像中
docker history myapp
# 查看构建历史,确认没有密钥出现在任何层中
docker run --rm myapp cat /root/.npmrc
# 应该报错:文件不存在(因为npmrc在构建后被删除,且不在最终镜像中)

4.10 安全Dockerfile模板与检查清单

以下提供一个完整的安全Dockerfile模板和检查清单。

# ============================================================
# 安全Dockerfile模板 - 适用于生产环境
# ============================================================

# 使用BuildKit语法
# syntax=docker/dockerfile:1.4

# --- 阶段1: 构建阶段 ---
FROM golang:1.22-alpine AS builder

# 安装构建依赖(固定版本)
RUN apk add --no-cache \
    git=2.43.0-r0 \
    make=4.4.1-r1 \
    ca-certificates=20230506-r0

# 创建构建用户(不以root构建)
RUN adduser -D -g '' builder
USER builder
WORKDIR /home/builder/build

# 复制依赖文件
COPY --chown=builder:builder go.mod go.sum ./
RUN go mod download

# 复制源代码
COPY --chown=builder:builder . .

# 静态编译(安全优化:去掉调试信息、禁用CGO)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build \
    -ldflags='-s -w -extldflags "-static" -X main.Version=1.0.0' \
    -trimpath \
    -o myapp .

# --- 阶段2: 运行阶段 ---
FROM scratch

# 镜像元数据(OCI标准标签)
LABEL org.opencontainers.image.title="myapp" \
      org.opencontainers.image.description="Secure application" \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.source="https://github.com/myorg/myapp" \
      org.opencontainers.image.licenses="MIT"

# 从构建阶段复制必要文件
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /home/builder/build/myapp /myapp

# 创建非root用户(通过修改passwd文件实现)
# 在scratch镜像中,直接使用数字UID
USER 65532:65532

# 暴露应用端口(非特权端口)
EXPOSE 8080

# 设置健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD ["/myapp", "healthcheck"]

# 设置安全的入口点
ENTRYPOINT ["/myapp"]

# 默认参数
CMD ["serve", "--config", "/etc/myapp/config.yaml"]

Dockerfile安全检查清单:

□ 基础镜像安全
  □ 使用最小化基础镜像(scratch/distroless/alpine)
  □ 固定基础镜像版本(不使用latest)
  □ 使用digest固定基础镜像(最高级别保证)

□ 用户权限
  □ 创建非root用户
  □ 使用USER指令切换到非root用户
  □ 使用固定UID/GID

□ 敏感信息
  □ 不在ENV/ARG中硬编码密码
  □ 不在RUN命令中使用密码
  □ 使用BuildKit secrets传递构建时密钥
  □ 使用.dockerignore排除敏感文件

□ 依赖管理
  □ 固定所有软件包版本
  □ 使用--no-install-recommends(Debian/Ubuntu)
  □ 使用--no-cache(Alpine)
  □ 使用lock文件(package-lock.json/yarn.lock/requirements.txt)

□ 镜像优化
  □ 使用多阶段构建
  □ 合并RUN指令减少层数
  □ 清理缓存和临时文件
  □ 使用COPY而非ADD(除非需要解压)

□ 安全配置
  □ 设置HEALTHCHECK
  □ 添加OCI标准标签
  □ 暴露非特权端口(>1024)
  □ 设置正确的文件权限(--chown)

□ 构建安全
  □ 启用BuildKit
  □ 使用--secret传递密钥
  □ 验证密钥未泄露到镜像层
  □ 使用.dockerignore

第五章 容器运行时安全

即使镜像构建得再安全,如果容器运行时的安全配置不当,仍然可能被攻击者利用。本章详细讲解容器运行时的各项安全配置,提供完整的docker run安全参数模板。

5.1 以非root用户运行容器(–user)

在运行时以非root用户启动容器是最基本也是最重要的安全措施之一。即使镜像中已经配置了非root用户,运行时也可以通过--user参数覆盖。

# 方式1: 使用镜像中预设的非root用户
docker run --rm -d --name app myapp:latest
# 如果Dockerfile中有 USER appuser,则容器以appuser运行

# 方式2: 运行时指定用户(覆盖镜像中的USER设置)
docker run --rm -d --name app --user 1000:1000 myapp:latest
# 以UID 1000, GID 1000运行

# 方式3: 使用用户名(镜像中需存在该用户)
docker run --rm -d --name app --user appuser myapp:latest

# 方式4: 指定用户并设置额外用户组
docker run --rm -d --name app \
  --user 1000:1000 \
  --group-add 992 \  # 添加额外用户组(如ssl-cert组)
  myapp:latest

# 验证容器运行的用户身份
docker exec app id
# uid=1000 gid=1000 groups=1000,992

# 结合user namespace remap使用(更安全)
# 启用userns-remap后,容器内的UID 0映射为宿主机的非特权用户
docker run --rm -d --name app --user 0 myapp:latest
# 容器内是root,但在宿主机上映射为普通用户(如100000)
docker exec app cat /proc/self/uid_map
# 查看UID映射关系

注意事项: 某些应用需要特定UID才能正常工作(如需要读取特定权限的文件)。在设置--user时需要确保应用所需的文件权限正确。

5.2 只读文件系统(–read-only)

只读文件系统可以防止攻击者修改容器内的文件(如植入后门、修改配置),是重要的运行时安全控制。

# 启用只读文件系统
docker run --rm -d --name app --read-only myapp:latest
# 容器根文件系统变为只读,任何写操作都会失败

# 但应用通常需要写入临时文件,因此需要配合tmpfs
docker run --rm -d --name app \
  --read-only \
  --tmpfs /tmp:rw,size=64m,mode=1777 \     # 临时文件目录
  --tmpfs /var/cache:rw,size=128m \         # 缓存目录
  --tmpfs /var/run:rw,size=32m \            # 运行时目录
  --tmpfs /app/logs:rw,size=64m \           # 应用日志目录
  myapp:latest

# 验证只读文件系统
docker exec app touch /test.txt
# 报错: touch: /test.txt: Read-only file system

docker exec app touch /tmp/test.txt
# 成功: /tmp是tmpfs挂载,可写

# 对于需要持久化写入的数据,使用volume挂载
docker run --rm -d --name app \
  --read-only \
  --tmpfs /tmp \
  -v app-data:/app/data \  # 数据目录使用volume(可写)
  myapp:latest

只读文件系统的安全收益:

  • 防止攻击者植入恶意二进制文件
  • 防止修改应用配置文件
  • 防止修改系统文件
  • 防止持久化后门
  • 防止挖矿程序写入

5.3 限制Capabilities(–cap-drop, --cap-add)

Capabilities控制是减少容器权限的关键手段。最佳实践是先丢弃所有能力,再只添加应用必需的能力。

# 查看Docker容器默认拥有的Capabilities
docker run --rm alpine sh -c \
  'apk add --no-cache libcap 2>/dev/null && capsh --print' | head -5

# 安全实践1: 丢弃所有Capabilities,只添加必要的
# 这是最安全的做法
docker run --rm -d --name app \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \  # 只保留绑定低端口的能力
  myapp:latest

# 安全实践2: 丢弃特定危险Capabilities
docker run --rm -d --name app \
  --cap-drop=SYS_ADMIN \       # 丢弃系统管理能力(最危险)
  --cap-drop=SYS_PTRACE \      # 丢弃进程追踪能力
  --cap-drop=SYS_MODULE \      # 丢弃内核模块加载能力
  --cap-drop=NET_ADMIN \       # 丢弃网络管理能力
  --cap-drop=NET_RAW \         # 丢弃原始网络包能力(禁ping)
  --cap-drop=DAC_OVERRIDE \    # 丢弃文件权限绕过能力
  myapp:latest

# 不同应用所需的Capabilities
# Nginx(需要绑定80端口)
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE --cap-add=CHOWN nginx

# 需要ping功能的应用
docker run --cap-drop=ALL --cap-add=NET_RAW alpine ping 8.8.8.8

# 需要修改系统时间的应用(NTP服务)
docker run --cap-drop=ALL --cap-add=SYS_TIME ntpd

# 需要auditd日志的应用
docker run --cap-drop=ALL --cap-add=AUDIT_WRITE app

# 验证容器的Capabilities
docker exec app sh -c 'cat /proc/1/status | grep Cap'
# CapEff: 0000000000001000  (只有NET_BIND_SERVICE)

Capabilities安全等级:

危险等级Capabilities风险说明
极高危SYS_ADMIN几乎等价于root,可执行mount、chroot等
高危SYS_PTRACE可附加到任意进程,读取/修改内存
高危SYS_MODULE可加载内核模块,完全控制系统
高危NET_ADMIN可修改网络配置、防火墙
中危NET_RAW可构造原始网络包
中危DAC_OVERRIDE可绕过文件读写权限
低危NET_BIND_SERVICE绑定1024以下端口
低危CHOWN修改文件所有者
低危KILL发送信号

5.4 禁止特权模式(–privileged的危害)

--privileged模式赋予容器几乎等同于宿主机root的所有权限,是最危险的Docker运行参数。在生产环境中应绝对禁止使用。

# === 极度危险的示例(绝对禁止在生产环境使用!) ===
# 特权模式:赋予容器所有Capabilities,访问所有设备
docker run --rm -it --privileged alpine sh
# 在特权容器内可以:
# - 访问宿主机所有设备文件(/dev/sda等)
# - 加载内核模块
# - 修改网络配置
# - 执行挂载操作
# - 几乎可以做任何宿主机root能做的事

# 特权容器逃演示例(仅用于理解风险,请勿在生产环境执行):
# 在特权容器内:
dmesg                  # 查看宿主机内核日志
mount /dev/sda1 /mnt   # 挂载宿主机磁盘
chroot /mnt            # 切换到宿主机文件系统
# 此时攻击者已完全控制宿主机!

# === 安全替代方案 ===
# 如果应用确实需要特定设备访问,只挂载特定设备
docker run --rm -d --name app \
  --device=/dev/sda1 \  # 只挂载特定设备
  --cap-drop=ALL \
  --cap-add=SYS_RAWIO \
  myapp:latest

# 如果应用需要特定的Capabilities,逐个添加
docker run --rm -d --name app \
  --cap-drop=ALL \
  --cap-add=NET_ADMIN \  # 只添加网络管理能力
  myapp:latest

--privileged与安全配置对比:

安全特性–privileged安全配置
Capabilities全部授予最小化(只添加必要)
设备访问全部设备仅必要设备
Seccomp禁用启用
AppArmor禁用启用
命名空间部分禁用全部启用
内核模块可加载不可加载
宿主机文件可访问不可访问

5.5 Seccomp Profile配置

第二章已经介绍了Seccomp的基本原理,本节深入讲解如何在运行时配置Seccomp Profile。

# 查看容器默认的seccomp状态
docker run --rm alpine grep Seccomp /proc/1/status
# Seccomp: 2  (2=filter模式,已启用)

# 使用默认seccomp profile(推荐)
docker run --rm -d --name app \
  --security-opt seccomp=default \
  myapp:latest

# 使用自定义seccomp profile
docker run --rm -d --name app \
  --security-opt seccomp=/path/to/custom-seccomp.json \
  myapp:latest

# 禁用seccomp(不推荐,仅用于调试)
docker run --rm -d --name app \
  --security-opt seccomp=unconfined \
  myapp:latest
// 文件: restrictive-seccomp.json
// 严格限制的seccomp profile示例
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "defaultErrnoRet": 1,
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "ptrace",          // 禁止进程追踪
        "process_vm_readv", // 禁止读取其他进程内存
        "process_vm_writev",// 禁止写入其他进程内存
        "kexec_load",      // 禁止加载新内核
        "kexec_file_load", // 禁止加载新内核
        "reboot",          // 禁止重启
        "mount",           // 禁止挂载
        "umount",          // 禁止卸载
        "umount2",         // 禁止卸载
        "pivot_root",      // 禁止修改根文件系统
        "swapon",          // 禁止启用swap
        "swapoff",         // 禁止关闭swap
        "init_module",     // 禁止加载内核模块
        "finit_module",    // 禁止加载内核模块
        "delete_module",   // 禁止删除内核模块
        "iopl",            // 禁止IO权限
        "ioperm",          // 禁止IO权限
        "settimeofday",    // 禁止修改系统时间
        "clock_settime",   // 禁止修改时钟
        "perf_event_open"  // 禁止性能事件
      ],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1
    }
  ]
}

5.6 AppArmor Profile配置

# 查看AppArmor状态
sudo aa-status
# 查看Docker默认profile
docker run --rm alpine cat /proc/1/attr/current

# 创建针对特定应用的AppArmor profile
cat > /etc/apparmor.d/docker-nginx << 'PROFILE'
#include <tunables/global>

profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # 允许网络操作
  network inet stream,
  network inet6 stream,
  network inet dgram,
  network inet6 dgram,

  # 允许必要的capabilities
  capability net_bind_service,
  capability setgid,
  capability setuid,
  capability dac_override,
  capability chown,
  capability kill,

  # 允许nginx访问特定路径
  /usr/sbin/nginx rix,          # 允许执行nginx
  /etc/nginx/** r,              # 允许读取nginx配置
  /var/log/nginx/** rw,         # 允许读写nginx日志
  /var/cache/nginx/** rw,       # 允许读写nginx缓存
  /run/nginx.pid rw,            # 允许读写PID文件
  /run/nginx/** rw,

  # 禁止访问敏感路径
  deny /proc/*/mem rw,
  deny /proc/sys/** rw,
  deny /sys/** rw,
  deny /etc/shadow r,
  deny /etc/passwd w,
  deny /root/** rwlx,

  # 允许信号
  signal (receive) peer=unconfined,

  # 允许文件系统操作
  file,
  umount,
}
PROFILE

# 加载AppArmor profile
sudo apparmor_parser -r /etc/apparmor.d/docker-nginx

# 使用自定义AppArmor profile运行容器
docker run --rm -d --name nginx \
  --security-opt apparmor=docker-nginx \
  -p 8080:80 \
  nginx:1.25-alpine

# 验证AppArmor profile已加载
docker exec nginx cat /proc/1/attr/current
# 输出: docker-nginx (enforce)

5.7 SELinux配置

# 查看SELinux状态
sestatus

# 查看容器的SELinux上下文
docker run --rm alpine cat /proc/self/attr/current

# 运行容器时指定SELinux标签
# type: 指定SELinux类型标签
# level: 指定MLS/MCS级别
docker run --rm -d --name app \
  --security-opt label=type:container_t \
  --security-opt label=level:s0:c100,c200 \
  myapp:latest

# 禁用SELinux(不推荐)
docker run --rm -d --name app \
  --security-opt label=disable \
  myapp:latest

# 常用SELinux容器类型
# container_t        - 默认容器类型,受限访问
# svirt_lxc_net_t    - 虚拟化容器类型
# container_ro_t     - 只读容器文件
# container_var_run_t - 运行时文件

# 为挂载卷指定SELinux标签
# :z  - 共享卷(多个容器可访问)
# :Z  - 私有卷(仅当前容器可访问)
docker run --rm -d --name app \
  -v /host/data:/app/data:Z \  # 私有卷,自动设置SELinux标签
  myapp:latest

# 查看文件的SELinux上下文
ls -Z /var/lib/docker/volumes/

# 修改文件的SELinux上下文
sudo chcon -Rt container_file_t /host/data/

5.8 资源限制防止DoS攻击

资源限制是防止资源耗尽型DoS攻击的关键手段。通过限制CPU、内存、IO和PID,可以确保单个容器不会影响其他容器和宿主机的正常运行。

# 完整的资源限制示例
docker run --rm -d --name app \
  --cpus="2.0" \                    # 限制最多使用2个CPU核心
  --cpu-shares=1024 \              # CPU相对权重(默认1024)
  --cpuset-cpus="0,1" \            # 绑定到CPU 0和1
  --cpu-period=100000 \            # CPU调度周期(微秒)
  --cpu-quota=200000 \             # CPU配额(100000周期内200000微秒=2核)
  --memory="1g" \                  # 内存限制1GB
  --memory-swap="2g" \            # 内存+Swap限制2GB
  --memory-reservation="512m" \   # 内存软限制512MB
  --memory-swappiness=60 \        # Swap使用倾向(0-100)
  --oom-kill-disable=false \      # 允许OOM Kill(安全做法)
  --oom-score-adj=500 \           # OOM Kill优先级(-1000到1000)
  --pids-limit=200 \              # 最多200个进程
  --blkio-weight=500 \            # IO权重(10-1000)
  --device-read-bps="/dev/sda:10mb" \   # 读限制10MB/s
  --device-write-bps="/dev/sda:10mb" \  # 写限制10MB/s
  --device-read-iops="/dev/sda:1000" \  # 读IOPS限制
  --device-write-iops="/dev/sda:1000" \ # 写IOPS限制
  --ulimit nofile=65535:65535 \   # 文件描述符限制
  --ulimit nproc=100:100 \        # 进程数限制
  --ulimit core=0:0 \             # 禁止core dump
  myapp:latest

# 验证资源限制
docker inspect app | jq '.[0].HostConfig.Memory'
# 输出: 1073741824 (1GB)

docker stats app
# CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %   NET I/O   BLOCK I/O   PIDS
# app         0.50%   50MiB / 1GiB        4.88%  5kB/0B    0B/0B       15

5.9 禁止容器获取新权限(–security-opt no-new-privileges)

no-new-privileges标志可以防止容器内的进程通过setuid/setgid程序或execve获取新的权限。这是防止权限提升攻击的重要措施。

# 启用no-new-privileges
docker run --rm -d --name app \
  --security-opt no-new-privileges \
  myapp:latest

# 验证no-new-privileges是否生效
docker exec app sh -c 'cat /proc/1/status | grep NoNewPrivs'
# NoNewPrivs: 1  (1表示已启用)

# 测试:在启用no-new-privileges的容器中尝试通过setuid程序提权
docker run --rm --security-opt no-new-privileges alpine sh -c '
  # 创建一个setuid root的程序
  apk add --no-cache gcc 2>/dev/null
  cat > /tmp/test.c << "EOF"
  #include <stdio.h>
  #include <unistd.h>
  int main() {
      printf("UID: %d, EUID: %d\n", getuid(), geteuid());
      return 0;
  }
  EOF
  gcc -o /tmp/test /tmp/test.c
  chmod u+s /tmp/test
  # 以普通用户运行setuid程序
  su nobody -c "/tmp/test"
'
# 即使设置了setuid位,EUID也不会变为0(root)
# 输出: UID: 65534, EUID: 65534 (no-new-privileges阻止了提权)

# 不启用no-new-privileges时(危险)
docker run --rm alpine sh -c '
  apk add --no-cache gcc 2>/dev/null
  cat > /tmp/test.c << "EOF"
  #include <stdio.h>
  #include <unistd.h>
  int main() {
      printf("UID: %d, EUID: %d\n", getuid(), geteuid());
      return 0;
  }
  EOF
  gcc -o /tmp/test /tmp/test.c
  chmod u+s /tmp/test
  su nobody -c "/tmp/test"
'
# 输出: UID: 65534, EUID: 0 (EUID变为0,通过setuid获取了root权限!)

5.10 临时文件系统(tmpfs)安全使用

tmpfs是一种基于内存的临时文件系统,常用于容器中需要临时写入的场景。结合--read-only使用时,tmpfs是唯一可写的区域。

# 使用tmpfs挂载临时目录
docker run --rm -d --name app \
  --read-only \
  --tmpfs /tmp:rw,size=64m,mode=1777,uid=1000,gid=1000 \
  --tmpfs /var/cache:rw,size=128m \
  --tmpfs /app/logs:rw,size=64m,noexec,nosuid \
  myapp:latest

# tmpfs选项说明:
# rw        - 可读写
# size      - 最大大小
# mode      - 权限模式(1777表示所有人可读写执行+sticky bit)
# uid       - 所有者UID
# gid       - 所有者GID
# noexec    - 禁止执行二进制文件(安全选项)
# nosuid    - 禁止setuid/setgid位(安全选项)
# nodev     - 禁止设备文件(安全选项)

# 验证tmpfs挂载
docker exec app mount | grep tmpfs
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,relatime,size=65536k)
# tmpfs on /var/cache type tmpfs (rw,nosuid,nodev,relatime,size=131072k)
# tmpfs on /app/logs type tmpfs (rw,noexec,nosuid,nodev,relatime,size=65536k)

# 检查tmpfs的noexec是否生效
docker exec app sh -c 'echo "#!/bin/sh" > /tmp/test.sh && chmod +x /tmp/test.sh && /tmp/test.sh'
# 如果/tmp有noexec选项,执行会失败: Permission denied

5.11 完整的docker run安全参数模板

以下是一个综合了所有安全配置的完整docker run命令模板:

# ============================================================
# 完整的安全加固 docker run 命令模板
# ============================================================
docker run -d \
  \
  `# 容器基本信息` \
  --name secure-app \
  --hostname secure-app \
  --restart=unless-stopped \
  \
  `# 用户权限: 非root运行` \
  --user 1000:1000 \
  --group-add 992 \
  \
  `# 文件系统: 只读` \
  --read-only \
  --tmpfs /tmp:rw,size=64m,mode=1777,noexec,nosuid,nodev \
  --tmpfs /var/cache:rw,size=128m,noexec,nosuid,nodev \
  --tmpfs /var/run:rw,size=32m,noexec,nosuid,nodev \
  --tmpfs /app/logs:rw,size=64m,noexec,nosuid,nodev \
  \
  `# Capabilities: 最小权限` \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  \
  `# 安全选项: 多层防护` \
  --security-opt no-new-privileges:true \
  --security-opt seccomp=/etc/docker/seccomp-profiles/restrictive.json \
  --security-opt apparmor=docker-default \
  --security-opt label=type:container_t \
  \
  `# 资源限制: 防止DoS` \
  --cpus="2.0" \
  --cpuset-cpus="0,1" \
  --memory="1g" \
  --memory-swap="2g" \
  --memory-reservation="512m" \
  --memory-swappiness=60 \
  --pids-limit=200 \
  --blkio-weight=500 \
  --device-read-bps="/dev/sda:50mb" \
  --device-write-bps="/dev/sda:50mb" \
  --ulimit nofile=65535:65535 \
  --ulimit nproc=100:100 \
  --ulimit core=0:0 \
  \
  `# 网络安全: 隔离网络` \
  --network=secure-net \
  --dns=8.8.8.8 \
  --dns-search=internal.example.com \
  \
  `# 端口: 最小化暴露` \
  -p 127.0.0.1:8080:8080 \
  \
  `# 存储: 只挂载必要卷` \
  -v app-data:/app/data:rw,Z \
  -v /etc/ssl/certs/app.crt:/app/certs/app.crt:ro \
  \
  `# 健康检查` \
  --health-cmd="curl -f http://localhost:8080/health || exit 1" \
  --health-interval=30s \
  --health-timeout=3s \
  --health-retries=3 \
  --health-start-period=10s \
  \
  `# 日志: 限制日志大小` \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  --log-opt labels=secure-app \
  \
  `# 环境变量(非敏感)` \
  -e ENV=production \
  -e LOG_LEVEL=info \
  \
  `# 镜像: 使用固定版本` \
  myregistry.com/myapp:v1.0.0@sha256:abcdef1234567890...

# 验证所有安全配置
echo "=== 验证用户 ==="
docker exec secure-app id

echo "=== 验证只读文件系统 ==="
docker exec secure-app sh -c 'touch /test 2>&1 || echo "只读文件系统已启用"'

echo "=== 验证Capabilities ==="
docker exec secure-app sh -c 'cat /proc/1/status | grep Cap'

echo "=== 验证Seccomp ==="
docker exec secure-app sh -c 'grep Seccomp /proc/1/status'

echo "=== 验证no-new-privileges ==="
docker exec secure-app sh -c 'grep NoNewPrivs /proc/1/status'

echo "=== 验证资源限制 ==="
docker inspect secure-app | jq '.[0].HostConfig | {Memory, NanoCpus, PidsLimit}'

echo "=== 容器状态 ==="
docker stats --no-stream secure-app

docker-compose.yml安全配置模板:

# docker-compose.yml 安全加固模板
version: '3.8'

services:
  app:
    image: myregistry.com/myapp:v1.0.0@sha256:abcdef1234567890...
    container_name: secure-app
    hostname: secure-app
    restart: unless-stopped

    # 用户权限
    user: "1000:1000"
    group_add:
      - "992"

    # 文件系统
    read_only: true
    tmpfs:
      - /tmp:rw,size=64m,mode=1777,noexec,nosuid,nodev
      - /var/cache:rw,size=128m,noexec,nosuid,nodev
      - /var/run:rw,size=32m,noexec,nosuid,nodev
      - /app/logs:rw,size=64m,noexec,nosuid,nodev

    # Capabilities
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

    # 安全选项
    security_opt:
      - no-new-privileges:true
      - seccomp:/etc/docker/seccomp-profiles/restrictive.json
      - apparmor=docker-default
      - label=type:container_t

    # 资源限制
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
          pids: 200
        reservations:
          cpus: '0.5'
          memory: 512M

    # 网络
    networks:
      - secure-net
    dns:
      - 8.8.8.8
    dns_search:
      - internal.example.com
    ports:
      - "127.0.0.1:8080:8080"  # 仅绑定localhost

    # 存储卷
    volumes:
      - app-data:/app/data:rw,Z
      - /etc/ssl/certs/app.crt:/app/certs/app.crt:ro

    # 健康检查
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 10s

    # 日志
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

    # 环境变量(非敏感)
    environment:
      - ENV=production
      - LOG_LEVEL=info

networks:
  secure-net:
    driver: bridge
    internal: false  # 如果设为true,则完全隔离(不能访问外网)

volumes:
  app-data:
    driver: local

第六章 Docker守护进程安全

Docker守护进程(Docker Daemon)是Docker架构的核心组件,负责管理容器的构建、运行和分发。由于Docker Daemon以root权限运行,它本身就是一个高价值攻击目标。如果守护进程被攻破,攻击者可以控制宿主机上的所有容器。本章详细讲解Docker Daemon的安全加固。

6.1 Docker Daemon安全配置

Docker Daemon通过/etc/docker/daemon.json配置文件进行全局配置。合理的配置可以大幅提升Docker的安全性。

# 查看当前Docker Daemon配置
docker info

# 查看当前daemon.json配置
cat /etc/docker/daemon.json

# 修改daemon.json后重启Docker服务
systemctl restart docker

# 验证配置是否生效
docker info | grep -A5 "Security Options"
// 文件: /etc/docker/daemon.json
// Docker Daemon基础安全配置
{
  "icc": false,                    // 禁止容器间默认通信(默认bridge网络)
  "log-level": "info",            // 日志级别(info/debug/warn/error)
  "userland-proxy": false,        // 禁用用户态代理(使用iptables更安全高效)
  "disable-legacy-registry": true, // 禁用旧版registry V1协议
  "live-restore": true,           // 启用实时恢复(Daemon重启不中断容器)
  "userns-remap": "default",      // 启用用户命名空间重映射
  "no-new-privileges": true,      // 全局禁止容器获取新权限
  "cgroup-parent": "/secure-docker", // 设置cgroup父级
  "default-ulimits": {            // 默认ulimit限制
    "nofile": {
      "Name": "nofile",
      "Hard": 65535,
      "Soft": 65535
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 100,
      "Soft": 50
    }
  },
  "default-runtime": "runc",      // 默认容器运行时
  "runtimes": {                   // 配置运行时
    "runc": {
      "path": "runc"
    },
    "kata": {                     // Kata Containers(硬件虚拟化隔离)
      "path": "/usr/bin/kata-runtime"
    }
  }
}

6.2 TLS加密Docker Daemon通信

默认情况下,Docker Daemon的远程API没有认证和加密,任何能访问Daemon端口(2375/2376)的人都可以完全控制Docker。在生产环境中,必须启用TLS加密和双向认证。

# ============================================================
# 步骤1: 创建CA证书
# ============================================================
# 创建CA私钥
openssl genrsa -aes256 -out ca-key.pem 4096

# 创建CA证书(有效期为3650天)
openssl req -new -x509 -days 3650 -key ca-key.pem \
  -sha256 -out ca.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=docker-ca"

# ============================================================
# 步骤2: 创建服务端证书(Docker Daemon使用)
# ============================================================
# 创建服务端私钥
openssl genrsa -out server-key.pem 4096

# 创建服务端CSR
openssl req -subj "/CN=docker-server" \
  -new -key server-key.pem -out server.csr

# 创建服务端扩展配置(包含SAN:Subject Alternative Name)
echo "subjectAltName = DNS:docker-server,IP:192.168.1.100,IP:127.0.0.1" > extfile.cnf
echo "extendedKeyUsage = serverAuth" >> extfile.cnf

# 使用CA签名服务端证书
openssl x509 -req -days 3650 -in server.csr \
  -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
  -out server-cert.pem -extfile extfile.cnf

# ============================================================
# 步骤3: 创建客户端证书(远程客户端使用)
# ============================================================
# 创建客户端私钥
openssl genrsa -out key.pem 4096

# 创建客户端CSR
openssl req -subj "/CN=docker-client" \
  -new -key key.pem -out client.csr

# 创建客户端扩展配置
echo "extendedKeyUsage = clientAuth" > extfile-client.cnf

# 使用CA签名客户端证书
openssl x509 -req -days 3650 -in client.csr \
  -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
  -out cert.pem -extfile extfile-client.cnf

# 清理临时文件
rm -v client.csr server.csr extfile.cnf extfile-client.cnf

# 设置证书文件权限
chmod -v 0400 ca-key.pem key.pem server-key.pem
chmod -v 0444 ca.pem server-cert.pem cert.pem

# ============================================================
# 步骤4: 配置Docker Daemon使用TLS
# ============================================================
# 方法1: 通过systemd配置
mkdir -p /etc/docker/tls
cp ca.pem server-cert.pem server-key.pem /etc/docker/tls/

# 编辑Docker的systemd服务文件
# systemctl edit docker.service
cat > /etc/systemd/system/docker.service.d/override.conf << 'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd \
  --tlsverify \
  --tlscacert=/etc/docker/tls/ca.pem \
  --tlscert=/etc/docker/tls/server-cert.pem \
  --tlskey=/etc/docker/tls/server-key.pem \
  --host=unix:///var/run/docker.sock \
  --host=tcp://0.0.0.0:2376
EOF

# 方法2: 通过daemon.json配置
cat > /etc/docker/daemon.json << 'EOF'
{
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
  "tls": true,
  "tlsverify": true,
  "tlscacert": "/etc/docker/tls/ca.pem",
  "tlscert": "/etc/docker/tls/server-cert.pem",
  "tlskey": "/etc/docker/tls/server-key.pem"
}
EOF

# 重启Docker服务
systemctl daemon-reload
systemctl restart docker

# ============================================================
# 步骤5: 客户端使用TLS连接Docker Daemon
# ============================================================
# 将客户端证书复制到~/.docker目录
mkdir -p ~/.docker
cp ca.pem ~/.docker/ca.pem
cp cert.pem ~/.docker/cert.pem
cp key.pem ~/.docker/key.pem

# 使用TLS连接Docker Daemon
docker --tlsverify \
  --tlscacert=~/.docker/ca.pem \
  --tlscert=~/.docker/cert.pem \
  --tlskey=~/.docker/key.pem \
  -H=tcp://192.168.1.100:2376 \
  version

# 或者通过环境变量设置
export DOCKER_HOST=tcp://192.168.1.100:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/.docker
docker version

TLS端口说明:

端口协议安全性说明
2375TCP不安全未加密的Docker API(生产环境禁止使用)
2376TCP安全TLS加密的Docker API

6.3 限制Docker Socket访问

Docker Socket(/var/run/docker.sock)是Docker Daemon的本地通信接口。如果将这个socket挂载到容器中,容器就可以完全控制Docker Daemon,进而控制宿主机上的所有容器。

# === 极度危险的示例(绝对禁止!) ===
# 将Docker Socket挂载到容器中
docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker:cli
# 在容器内可以执行任何Docker命令,相当于获得了宿主机的root权限!
# 容器逃逸:
docker run -it -v /:/host alpine chroot /host
# 攻击者已完全控制宿主机

# === 安全实践 ===
# 实践1: 限制Docker Socket的文件权限
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker /var/run/docker.sock
# 只有root和docker组成员可以访问

# 实践2: 将需要访问Docker API的用户加入docker组
usermod -aG docker username
# 注意:docker组成员等价于root权限,只授予可信用户

# 实践3: 使用Docker Socket Proxy(如Technativa/docker-socket-proxy)
# 只暴露必要的Docker API端点
docker run -d --name docker-proxy \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -e CONTAINERS=1 \      # 允许查询容器
  -e IMAGES=1 \          # 允许查询镜像
  -e EXEC=0 \            # 禁止执行命令
  -e POST=0 \            # 禁止POST操作(创建/删除)
  -e DELETE=0 \          # 禁止DELETE操作
  -p 2375:2375 \
  tecnativa/docker-socket-proxy

# 其他容器通过proxy访问Docker API
docker run -d --name monitoring \
  -e DOCKER_HOST=tcp://docker-proxy:2375 \
  --link docker-proxy \
  my-monitoring-app

# 实践4: 审计Docker Socket访问
# 在auditd中配置审计规则
echo "-w /var/run/docker.sock -k docker-socket" >> /etc/audit/audit.rules
systemctl restart auditd

# 查看Docker Socket审计日志
ausearch -k docker-socket | tail -20

6.4 Daemon用户命名空间重映射(userns-remap)

用户命名空间重映射(User Namespace Remapping)是Docker最重要的安全特性之一。它将容器内的root用户映射为宿主机上的非特权用户,即使容器逃逸,攻击者也只能获得非特权用户权限。

# 启用用户命名空间重映射
# 方法1: 使用默认映射
cat > /etc/docker/daemon.json << 'EOF'
{
  "userns-remap": "default"
}
EOF

# 方法2: 使用指定用户
# 先创建映射用户
groupadd -g 100000 dockremap
useradd -u 100000 -g 100000 -s /sbin/nologin dockremap

# 配置subuid和subgid
echo "dockremap:100000:65536" >> /etc/subuid
echo "dockremap:100000:65536" >> /etc/subgid

# 配置daemon.json
cat > /etc/docker/daemon.json << 'EOF'
{
  "userns-remap": "dockremap"
}
EOF

# 重启Docker
systemctl restart docker

# 验证用户命名空间重映射
docker run --rm alpine id
# 容器内显示: uid=0(root)
# 但在宿主机上查看该进程的实际UID
docker run -d --name userns-test alpine sleep 3600
HOST_PID=$(docker inspect --format '{{.State.Pid}}' userns-test)
ps -o uid,pid,comm -p $HOST_PID
# UID为100000(而非0)

# 查看UID映射
cat /proc/$HOST_PID/uid_map
# 输出: 0 100000 65536
# 表示容器内UID 0-65535映射到宿主机UID 100000-165535

# 针对特定容器禁用用户命名空间(需要特权操作时)
docker run --rm --userns=host alpine id
# 使用宿主机的用户命名空间(不推荐)

userns-remap的影响:

  • 所有容器自动启用用户命名空间映射
  • 容器内的文件所有权显示为root,但在宿主机上实际属于非特权用户
  • 挂载的volume需要调整权限
  • 某些需要真实root权限的操作可能失败

6.5 禁用不安全的注册中心

Docker默认可以使用HTTP(不加密)连接镜像仓库。在生产环境中,应只允许使用HTTPS连接可信的镜像仓库。

// 文件: /etc/docker/daemon.json
// 禁用不安全的注册中心
{
  "insecure-registries": [],  // 清空不安全注册中心列表
  "disable-legacy-registry": true  // 禁用旧版registry V1
}
# 配置私有镜像仓库认证
# 创建或编辑 ~/.docker/config.json
cat > ~/.docker/config.json << 'EOF'
{
  "auths": {
    "myregistry.com": {
      "auth": "dXNlcm5hbWU6cGFzc3dvcmQ="  // base64(username:password)
    }
  },
  "credsStore": "pass"  // 使用系统密码管理器存储凭据
}
EOF

# 更安全的方式: 使用Docker credential helper
# 安装docker-credential-pass
# 使用pass(Linux密码管理器)存储Docker凭据
echo '{"credsStore":"pass"}' > ~/.docker/config.json

# 配置可信镜像仓库
cat > /etc/docker/daemon.json << 'EOF'
{
  "insecure-registries": [],
  "registry-mirrors": ["https://mirror.gcr.io"],
  "disable-legacy-registry": true
}
EOF

6.6 审计日志配置

审计日志是安全事件追踪和取证的基础。Docker和相关组件都应配置审计日志。

# 配置Linux auditd审计Docker相关文件和目录
cat > /etc/audit/rules.d/docker.rules << 'EOF'
# 审计Docker Daemon
-w /usr/bin/dockerd -k docker-daemon
-w /usr/bin/docker -k docker-cli
-w /usr/bin/containerd -k docker-containerd
-w /usr/bin/runc -k docker-runc

# 审计Docker配置文件
-w /etc/docker/ -k docker-config
-w /etc/docker/daemon.json -k docker-daemon-config

# 审计Docker Socket
-w /var/run/docker.sock -k docker-socket

# 审计Docker数据目录
-w /var/lib/docker/ -k docker-data

# 审计Docker服务文件
-w /usr/lib/systemd/system/docker.service -k docker-service
-w /usr/lib/systemd/system/docker.socket -k docker-socket
EOF

# 重新加载审计规则
augenrules --load

# 查看Docker相关审计日志
ausearch -k docker-daemon | tail -20
ausearch -k docker-socket | tail -20
ausearch -k docker-config | tail -20

# 生成审计报告
aureport -k -i | grep docker
# 配置Docker Daemon日志
cat > /etc/docker/daemon.json << 'EOF'
{
  "log-level": "info",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}
EOF

# 使用syslog日志驱动(集中化日志管理)
cat > /etc/docker/daemon.json << 'EOF'
{
  "log-driver": "syslog",
  "log-opts": {
    "syslog-address": "tcp://192.168.1.200:514",
    "syslog-facility": "daemon",
    "tag": "docker/{{.Name}}/{{.ID}}"
  }
}
EOF

# 使用fluentd日志驱动
cat > /etc/docker/daemon.json << 'EOF'
{
  "log-driver": "fluentd",
  "log-opts": {
    "fluentd-address": "192.168.1.200:24224",
    "tag": "docker.{{.Name}}"
  }
}
EOF

6.7 live-restore配置

live-restore特性允许Docker Daemon重启或升级时容器继续运行而不中断。这对于生产环境的可用性和安全性都很重要。

// 文件: /etc/docker/daemon.json
{
  "live-restore": true
}
# 启用live-restore后
# Docker Daemon重启时容器不会停止
systemctl restart docker
# 容器继续运行,不受影响

# 验证live-restore是否启用
docker info | grep "Live Restore"
# Live Restore Enabled: true

# live-restore的安全意义:
# 1. 安全补丁升级Docker时不会中断业务
# 2. 配置变更后重启Daemon不影响运行中的容器
# 3. 减少因Daemon重启导致的安全窗口

6.8 完整的daemon.json安全配置模板

以下是一个综合了所有安全配置的完整daemon.json模板:

// 文件: /etc/docker/daemon.json
// Docker Daemon安全加固完整配置模板
{
  "icc": false,
  "log-level": "info",
  "userland-proxy": false,
  "disable-legacy-registry": true,
  "live-restore": true,
  "userns-remap": "default",
  "no-new-privileges": true,
  "cgroup-parent": "/secure-docker",

  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65535,
      "Soft": 65535
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 256,
      "Soft": 128
    },
    "core": {
      "Name": "core",
      "Hard": 0,
      "Soft": 0
    }
  },

  "default-runtime": "runc",
  "runtimes": {
    "runc": {
      "path": "runc",
      "runtimeArgs": ["--rootless=false"]
    },
    "kata": {
      "path": "/usr/bin/kata-runtime",
      "runtimeArgs": []
    }
  },

  "default-cgroupns-mode": "private",

  "default-shm-size": "64M",

  "insecure-registries": [],

  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  },

  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ],

  "bridge": "none",
  "iptables": true,
  "ip-forward": true,
  "ip-masq": true,
  "default-address-pools": [
    {"base": "172.20.0.0/16", "size": 24}
  ],

  "dns": ["8.8.8.8", "8.8.4.4"],
  "dns-search": ["internal.example.com"],

  "max-concurrent-downloads": 10,
  "max-concurrent-uploads": 5,

  "shutdown-timeout": 15,
  "containerd": "/run/containerd/containerd.sock",

  "features": {
    "buildkit": true
  },

  "experimental": false,

  "seccomp-profile": "/etc/docker/seccomp/default.json",

  "metrics-addr": "127.0.0.1:9323",
  "experimental": false
}

daemon.json安全配置检查清单:

□ 通信安全
  □ 禁用2375端口(未加密API)
  □ 启用2376端口(TLS加密API)
  □ 配置双向TLS认证
  □ 限制API访问IP

□ 用户安全
  □ 启用userns-remap(用户命名空间重映射)
  □ 启用no-new-privileges
  □ 限制docker组成员

□ 网络安全
  □ 禁用icc(容器间默认通信)
  □ 配置自定义bridge网络
  □ 禁用userland-proxy

□ 存储安全
  □ 使用overlay2存储驱动
  □ 配置容器数据卷加密

□ 日志与审计
  □ 配置日志大小限制
  □ 配置集中化日志收集
  □ 配置auditd审计规则

□ 镜像安全
  □ 禁用不安全注册中心
  □ 禁用旧版registry V1
  □ 配置私有仓库认证

□ 运行时安全
  □ 配置默认seccomp profile
  □ 配置默认ulimit限制
  □ 配置默认cgroup限制
  □ 启用live-restore

第七章 容器网络安全

容器网络安全是容器安全的重要组成部分。默认的Docker网络配置允许容器之间自由通信,这在生产环境中可能带来横向移动攻击的风险。本章将系统讲解容器网络隔离和安全加固。

7.1 网络隔离策略

Docker提供了多种网络驱动,每种驱动有不同的隔离特性。合理选择网络驱动是网络隔离的基础。

# 查看Docker支持的网络驱动
docker network ls
# NETWORK ID     NAME      DRIVER    SCOPE
# a1b2c3d4e5f6   bridge    bridge    local
# b2c3d4e5f6a1   host      host      local
# c3d4e5f6a1b2   none      null      local

# Docker网络驱动类型:
# bridge  - 默认桥接网络(容器通过NAT访问外部)
# host    - 容器直接使用宿主机网络(无隔离,不推荐)
# none    - 无网络(完全隔离)
# overlay - 跨主机容器网络(Swarm/Kubernetes)
# macvlan - 容器拥有独立MAC地址(二层网络)
# ipvlan  - 容器共享MAC地址(三层网络)

# 创建隔离的自定义bridge网络
docker network create --driver bridge \
  --subnet 172.20.0.0/16 \
  --ip-range 172.20.1.0/24 \
  --gateway 172.20.0.1 \
  secure-net

# 查看网络详情
docker network inspect secure-net

# 将容器加入隔离网络
docker run -d --name app1 --network secure-net myapp:v1.0
docker run -d --name app2 --network secure-net myapp:v1.0

# 默认bridge网络中所有容器可以互相通信
# 自定义bridge网络也可以互相通信,但可以通过iptables进一步限制

7.2 限制容器对外通信

限制容器的出站网络访问可以防止数据泄露和恶意外联(如挖矿程序连接矿池)。

# 方法1: 使用none网络(完全无网络)
docker run -d --name isolated-app --network none myapp:v1.0
# 容器没有任何网络接口(除了lo回环)

# 方法2: 使用internal网络(只允许容器间通信,不能访问外网)
docker network create --internal secure-internal
docker run -d --name app --network secure-internal myapp:v1.0
# 容器可以与同网络中的容器通信,但不能访问外部网络

# 方法3: 使用iptables限制容器出站
# 获取容器的IP地址
CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app)
echo "容器IP: $CONTAINER_IP"

# 禁止容器访问特定IP
iptables -I DOCKER-USER -s $CONTAINER_IP -d 10.0.0.0/8 -j DROP

# 禁止容器访问特定端口
iptables -I DOCKER-USER -s $CONTAINER_IP -p tcp --dport 22 -j DROP  # 禁止SSH
iptables -I DOCKER-USER -s $CONTAINER_IP -p tcp --dport 3389 -j DROP # 禁止RDP

# 只允许容器访问特定IP和端口(白名单模式)
iptables -I DOCKER-USER -s $CONTAINER_IP -d 192.168.1.100 -p tcp --dport 5432 -j ACCEPT  # 允许数据库
iptables -I DOCKER-USER -s $CONTAINER_IP -j DROP  # 禁止其他所有出站

# 方法4: 使用Docker的网络配置
docker run -d --name app \
  --network secure-net \
  --dns 8.8.8.8 \
  --dns-search internal.example.com \
  --add-host database:192.168.1.100 \
  myapp:v1.0

7.3 iptables规则加固

Docker默认使用iptables管理容器网络。理解Docker的iptables规则结构对于网络安全加固至关重要。

# 查看Docker的iptables规则
iptables -L -n -v
iptables -t nat -L -n -v

# Docker的iptables链结构:
# DOCKER     - 处理容器端口映射
# DOCKER-ISOLATION-STAGE-1/2 - 网络间隔离
# DOCKER-USER - 用户自定义规则(在Docker规则之前执行)

# 查看DOCKER-USER链(用户自定义规则应在此添加)
iptables -L DOCKER-USER -n -v

# 禁止特定容器访问另一个容器
CONTAINER1_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app1)
CONTAINER2_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app2)
iptables -I DOCKER-USER -s $CONTAINER1_IP -d $CONTAINER2_IP -j DROP

# 禁止容器访问宿主机
iptables -I DOCKER-USER -s $CONTAINER1_IP -d 172.17.0.1 -j DROP

# 限制容器到宿主机的SSH访问
iptables -I DOCKER-USER -s $CONTAINER1_IP -p tcp --dport 22 -j DROP

# 只允许容器访问特定端口
iptables -I DOCKER-USER -s $CONTAINER1_IP -p tcp -m multiport --dports 80,443,5432 -j ACCEPT
iptables -I DOCKER-USER -s $CONTAINER1_IP -j DROP

# 保存iptables规则(持久化)
# Debian/Ubuntu
apt-get install iptables-persistent
netfilter-persistent save

# CentOS/RHEL
service iptables save
# 或
iptables-save > /etc/sysconfig/iptables

7.4 使用自定义网络隔离服务

通过创建多个自定义网络,可以实现服务间的网络隔离。不同网络中的容器默认无法通信。

# 为不同的服务创建隔离的网络
docker network create frontend-net    # 前端网络
docker network create backend-net     # 后端网络
docker network create database-net    # 数据库网络

# 前端容器只加入前端网络
docker run -d --name webapp \
  --network frontend-net \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  webapp:v1.0

# API容器同时加入前端和后端网络(作为中间层)
docker run -d --name api \
  --network frontend-net \
  --network backend-net \
  api:v1.0

# 数据库容器只加入数据库网络
docker run -d --name database \
  --network database-net \
  --cap-drop=ALL \
  postgres:16-alpine

# 后端容器同时加入后端和数据库网络
docker run -d --name backend \
  --network backend-net \
  --network database-net \
  backend:v1.0

# 网络隔离效果:
# webapp → 可以访问 api (同一frontend-net)
# webapp → 不能直接访问 backend, database (不同网络)
# api → 可以访问 webapp, backend (在两个网络中)
# backend → 可以访问 api, database (在两个网络中)
# database → 只能被 backend 访问 (只在database-net中)

# 验证网络隔离
docker exec webapp ping -c1 api      # 成功(同一网络)
docker exec webapp ping -c1 database  # 失败(不同网络)
# docker-compose.yml 多网络隔离示例
version: '3.8'

services:
  webapp:
    image: webapp:v1.0
    networks:
      - frontend
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]

  api:
    image: api:v1.0
    networks:
      - frontend
      - backend
    depends_on: [database]

  backend:
    image: backend:v1.0
    networks:
      - backend
      - database
    depends_on: [database]

  database:
    image: postgres:16-alpine
    networks:
      - database
    cap_drop: [ALL]
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true   # 内部网络,不暴露到外部
  database:
    driver: bridge
    internal: true   # 内部网络,不暴露到外部

secrets:
  db_password:
    file: ./secrets/db_password.txt

7.5 容器间通信加密(TLS/mTLS)

对于跨网络或跨主机的容器通信,应使用TLS/mTLS加密通信,防止中间人攻击和数据窃听。

# 使用Docker Swarm的overlay网络(自动加密)
docker swarm init
docker network create --driver overlay --opt encrypted secure-overlay
# --opt encrypted: 自动加密overlay网络流量

# 手动配置mTLS
# 为每个服务生成证书
# 详见6.2节的TLS证书生成过程

# Nginx配置mTLS(服务间通信加密)
cat > nginx.conf << 'EOF'
server {
    listen 443 ssl;
    server_name api.internal;

    # 服务端证书
    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # 客户端证书验证(mTLS)
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;  # 强制验证客户端证书
    ssl_verify_depth 2;

    # 安全协议
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://backend:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_set_header X-Client-DN $ssl_client_s_dn;
    }
}
EOF

# 运行Nginx容器作为TLS终止代理
docker run -d --name api-gateway \
  --network backend-net \
  -v /etc/nginx/ssl:/etc/nginx/ssl:ro \
  -v /path/to/nginx.conf:/etc/nginx/conf.d/api.conf:ro \
  nginx:1.25-alpine

7.6 端口暴露最小化

端口暴露是容器与外部通信的主要方式。暴露的端口越多,攻击面越大。应遵循最小化原则,只暴露必要的端口。

# 危险:暴露不必要的端口
docker run -d --name app \
  -p 80:80 \
  -p 443:443 \
  -p 8080:8080 \
  -p 9090:9090 \  # 暴露了调试端口
  -p 3000:3000 \  # 暴露了管理界面
  myapp:v1.0

# 安全:只暴露必要端口
docker run -d --name app \
  -p 80:80 \
  -p 443:443 \
  myapp:v1.0

# 更安全:只绑定到localhost(不暴露到外部网络)
docker run -d --name app \
  -p 127.0.0.1:8080:8080 \  # 只绑定localhost
  myapp:v1.0
# 只有宿主机本机可以访问8080端口

# 使用Docker内部网络通信(不暴露端口)
docker run -d --name app \
  --network secure-net \
  --expose 8080 \  # 只在Docker网络内部暴露
  myapp:v1.0
# 容器间可以通过8080端口通信,但外部无法访问

# 限制端口访问源IP(通过iptables)
docker run -d --name app -p 8080:8080 myapp:v1.0
# 只允许特定IP访问8080端口
iptables -I DOCKER -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT
iptables -I DOCKER -p tcp --dport 8080 -j DROP

# 检查容器暴露的端口
docker port app
# 8080/tcp -> 0.0.0.0:8080

# 扫描容器开放端口(安全审计)
docker exec app sh -c 'netstat -tlnp 2>/dev/null || ss -tlnp'

7.7 Docker网络与防火墙(firewalld/ufw)交互

Docker的iptables规则可能与系统防火墙(firewalld/ufw)产生冲突,导致安全规则被绕过。理解它们之间的交互至关重要。

# 问题:Docker会修改iptables的NAT表和FILTER表
# 这可能导致ufw/firewalld的规则被绕过

# ufw与Docker的冲突处理
# 问题现象:ufw禁止了某端口,但Docker映射的端口仍然可以从外部访问
# 原因:Docker在DOCKER链中添加了ACCEPT规则,优先级高于ufw的DROP规则

# 解决方案1: 禁止Docker修改iptables(不推荐,会破坏Docker网络)
# 在daemon.json中设置
cat > /etc/docker/daemon.json << 'EOF'
{
  "iptables": false
}
EOF
# 注意:这会破坏Docker的网络功能,需要手动配置所有网络规则

# 解决方案2: 使用ufw-docker工具管理Docker防火墙规则
# 安装ufw-docker
wget -O /usr/local/bin/ufw-docker \
  https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
chmod +x /usr/local/bin/ufw-docker

# 配置ufw-docker
ufw-docker install
systemctl restart ufw
systemctl restart docker

# 使用ufw-docker控制容器端口访问
ufw-docker allow app 80/tcp       # 允许访问app容器的80端口
ufw-docker allow app 443/tcp from 192.168.1.0/24  # 只允许特定网段

# 解决方案3: 在DOCKER-USER链中添加规则(推荐)
# DOCKER-USER链在Docker规则之前执行,不会被Docker覆盖
# 只允许特定IP访问映射端口
iptables -I DOCKER-USER -p tcp --dport 8080 ! -s 192.168.1.0/24 -j DROP

# firewalld与Docker的交互
# 确保firewalld在Docker之前启动
systemctl enable firewalld
systemctl start firewalld
systemctl restart docker

# 在firewalld中配置Docker区域
firewall-cmd --permanent --zone=trusted --add-interface=docker0
firewall-cmd --permanent --zone=public --add-masquerade
firewall-cmd --reload

7.8 使用网络策略(Network Policy)

在Kubernetes环境中,Network Policy是实现容器网络隔离的标准方式。Docker Swarm也提供了类似的功能。

# Kubernetes Network Policy示例
# 文件: deny-all-ingress.yaml
# 默认拒绝所有入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}  # 选择所有Pod
  policyTypes:
  - Ingress
  # 没有ingress规则 = 拒绝所有入站

---
# 文件: allow-api-to-db.yaml
# 只允许API Pod访问数据库Pod
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    ports:
    - protocol: TCP
      port: 5432

---
# 文件: allow-specific-namespace.yaml
# 只允许特定命名空间访问
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-monitoring
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: webapp
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: monitoring
    ports:
    - protocol: TCP
      port: 9090
# Docker Swarm网络加密和隔离
# 创建加密的overlay网络
docker network create --driver overlay \
  --opt encrypted \
  --attachable \
  secure-overlay

# 查看网络加密状态
docker network inspect secure-overlay | jq '.[0].Options'
# {"encrypted": "true"}

7.9 零信任网络架构与容器

零信任(Zero Trust)是一种安全理念,核心原则是"从不信任,始终验证"。在容器环境中实施零信任架构需要多层安全控制。

零信任容器网络架构:

┌─────────────────────────────────────────────────────────────┐
│                     零信任容器网络                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 身份认证: 每个容器都有唯一身份(SPIFFE/SPIRE)          │
│  2. 最小权限: 每个容器只有最小网络权限                     │
│  3. 微隔离: 容器间默认拒绝,按需允许                        │
│  4. 加密通信: 所有容器间通信使用mTLS                        │
│  5. 持续验证: 持续监控容器行为和身份                        │
│  6. 审计日志: 记录所有网络通信                              │
│                                                             │
│  ┌─────────┐    mTLS    ┌─────────┐    mTLS    ┌─────────┐ │
│  │Container│ ←──────→ │Service  │ ←──────→ │Container│ │
│  │   A     │  加密通信  │  Mesh   │  加密通信  │   B     │ │
│  └─────────┘           │(Istio/  │           └─────────┘ │
│                        │ Linkerd)│                        │
│  ┌─────────┐           └─────────┘           ┌─────────┐ │
│  │Container│          ┌─────────┐            │Container│ │
│  │   C     │ ←──────→ │  Policy │ ←──────→   │   D     │ │
│  └─────────┘  mTLS    │  Engine │   mTLS     └─────────┘ │
│                       └─────────┘                         │
│                                                             │
│  工具:                                                      │
│  - Istio: 服务网格,提供mTLS和流量管理                      │
│  - Linkerd: 轻量级服务网格                                  │
│  - Cilium: 基于eBPF的网络和安全                             │
│  - SPIFFE/SPIRE: 容器身份认证框架                           │
│  - OPA(Open Policy Agent): 策略引擎                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
# 使用Istio实现容器间mTLS(零信任网络)
# 安装Istio
istioctl install --set values.global.meshID=mesh1 \
  --set values.global.network=network1 \
  --set values.global.clusterName=cluster1

# 启用严格mTLS模式
kubectl apply -f - << 'EOF'
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT  # 强制mTLS,不允许非加密通信
EOF

# 定义授权策略(零信任:默认拒绝,按需允许)
kubectl apply -f - << 'EOF'
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: production
spec:
  {}  # 空规则 = 拒绝所有
---
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-api-to-db
  namespace: production
spec:
  selector:
    matchLabels:
      app: database
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/api-service"]
    to:
    - operation:
        methods: ["GET", "POST"]
        ports: ["5432"]
EOF

第八章 密钥与机密管理

密钥与机密(Secrets)管理是容器安全中最容易出错的领域之一。数据库密码、API密钥、TLS证书等敏感信息如果管理不当,可能导致严重的安全事故。本章将系统讲解容器环境下的密钥管理方案。

8.1 容器中密钥管理的挑战

容器环境下的密钥管理面临独特的挑战:

挑战1: 镜像不可变性 —— 镜像一旦构建就不能修改,密钥不能硬编码在镜像中
挑战2: 环境变量泄露   —— 环境变量可以通过docker inspect查看,不安全
挑战3: 多环境管理     —— 同一镜像需要在dev/staging/prod环境中使用不同密钥
挑战4: 密钥轮换       —— 容器生命周期短,密钥轮换需要不影响运行中的容器
挑战5: 审计追踪       —— 需要追踪谁在什么时候访问了什么密钥
挑战6: 密钥分发       —— 如何安全地将密钥分发给容器
挑战7: 密钥存储       —— 密钥在宿主机和容器中的安全存储

8.2 环境变量传递密钥的风险

使用环境变量传递密钥是最常见但也是最不安全的方式之一。

# 危险:通过环境变量传递密码
docker run -d --name app \
  -e DATABASE_PASSWORD=SuperSecret123 \
  -e API_KEY=sk-abc123xyz \
  myapp:v1.0

# 风险1: docker inspect可以查看所有环境变量
docker inspect app | jq '.[0].Config.Env'
# 输出包含明文密码:
# [
#   "DATABASE_PASSWORD=SuperSecret123",
#   "API_KEY=sk-abc123xyz"
# ]

# 风险2: docker history可以查看构建时的环境变量
docker history myapp:v1.0 --no-trunc
# 可能暴露构建时设置的密码

# 风险3: 容器内的进程可以通过/proc查看环境变量
docker exec app cat /proc/1/environ
# 输出包含明文密码

# 风险4: 容器内任何用户都可以查看环境变量
docker exec app env
# DATABASE_PASSWORD=SuperSecret123
# API_KEY=sk-abc123xyz

# 风险5: 日志可能意外记录环境变量
docker logs app 2>&1 | grep -i password

# 风险6: ps命令可能暴露环境变量
docker exec app ps aux
# 某些情况下ps输出会包含环境变量

8.3 Docker Secrets(Swarm模式)

Docker Secrets是Docker Swarm模式提供的密钥管理功能。密钥以加密形式存储和传输,只在容器内存中解密,不会写入磁盘。

# 初始化Docker Swarm(如果尚未初始化)
docker swarm init

# 创建Secret
# 方式1: 从文件创建
echo "SuperSecretPassword123" | docker secret create db_password -
# 创建的Secret名为 db_password

# 方式2: 从文件创建
echo "my-api-key-value" > /tmp/api_key.txt
docker secret create api_key /tmp/api_key.txt
rm /tmp/api_key.txt  # 立即删除临时文件

# 方式3: 从stdin创建(多行密钥)
docker secret create tls_cert - << 'EOF'
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKDxYJ...
-----END CERTIFICATE-----
EOF

# 查看已创建的Secrets
docker secret ls
# ID    NAME          DRIVER    CREATED       UPDATED
# x1y2  db_password             2 minutes ago 2 minutes ago
# a3b4  api_key                 1 minute ago  1 minute ago

# 注意:Docker Secret的值一旦创建就不能查看或修改
# 只能删除后重新创建

# 将Secret分配给服务
docker service create \
  --name app \
  --secret db_password \
  --secret api_key \
  --env DB_PASSWORD_FILE=/run/secrets/db_password \
  --env API_KEY_FILE=/run/secrets/api_key \
  --replicas 3 \
  myapp:v1.0

# Secret在容器内挂载为文件
# 路径: /run/secrets/<secret_name>
# 文件内容就是Secret的值

# 更新Secret(需要删除旧的并创建新的)
docker service update --secret-rm db_password app
docker secret rm db_password
echo "NewSecretPassword456" | docker secret create db_password -
docker service update --secret-add db_password app

# 在docker-compose.yml中使用Secrets
cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  app:
    image: myapp:v1.0
    secrets:
      - db_password
      - api_key
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key
    deploy:
      replicas: 3

  database:
    image: postgres:16-alpine
    secrets:
      - db_password
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt
EOF
# 在应用中读取Docker Secret(Python示例)
# app.py
import os

def get_secret(secret_name):
    """从Docker Secret文件中读取密钥"""
    secret_path = f"/run/secrets/{secret_name}"
    try:
        with open(secret_path, 'r') as f:
            return f.read().strip()
    except FileNotFoundError:
        # 如果Secret文件不存在,回退到环境变量(仅开发环境)
        return os.environ.get(secret_name.upper())

# 使用密钥
db_password = get_secret('db_password')
api_key = get_secret('api_key')

# 密钥不会出现在环境变量中
print(f"DB Password loaded: {'***' if db_password else 'NOT SET'}")
print(f"API Key loaded: {'***' if api_key else 'NOT SET'}")

8.4 使用HashiCorp Vault管理密钥

HashiCorp Vault是目前最流行的开源密钥管理系统,提供动态密钥生成、密钥轮换、审计日志等高级功能。

# 使用Docker部署Vault开发模式(仅用于测试)
docker run -d --name vault \
  -p 8200:8200 \
  -e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' \
  -e 'VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200' \
  vault:1.15

# 进入Vault容器
docker exec -it vault sh

# 设置Vault地址和Token
export VAULT_ADDR='http://0.0.0.0:8200'
export VAULT_TOKEN='myroot'

# 启用密钥引擎(KV版本2)
vault secrets enable -path=secret kv-v2

# 存储密钥
vault kv put secret/app/database \
  username=appuser \
  password=SuperSecret123 \
  host=db.internal

vault kv put secret/app/api \
  key=sk-abc123xyz \
  secret=api-secret-value

# 读取密钥
vault kv get secret/app/database
# ====== Data ======
# Key        Value
# host       db.internal
# password   SuperSecret123
# username   appuser

# 只读取特定字段
vault kv get -field=password secret/app/database
# SuperSecret123

# 创建Vault策略(最小权限)
cat > app-policy.hcl << 'EOF'
path "secret/data/app/database" {
  capabilities = ["read"]
}
path "secret/data/app/api" {
  capabilities = ["read"]
}
EOF

vault policy write app-policy app-policy.hcl

# 为应用创建Token
vault token create -policy=app-policy -period=24h
# 生成一个有限期的Token

# 启用审计日志
vault audit enable file file_path=/vault/audit/audit.log
# 在容器应用中使用Vault(Python示例)
# 需要安装: pip install hvac
import hvac
import os

class VaultSecretManager:
    def __init__(self):
        self.client = hvac.Client(
            url=os.environ.get('VAULT_ADDR', 'http://vault:8200'),
            token=os.environ.get('VAULT_TOKEN')
        )

    def get_secret(self, path, key):
        """从Vault读取密钥"""
        response = self.client.secrets.kv.v2.read_secret_version(
            path=path,
            mount_point='secret'
        )
        return response['data']['data'][key]

    def get_database_credentials(self):
        """获取数据库凭据"""
        response = self.client.secrets.kv.v2.read_secret_version(
            path='app/database',
            mount_point='secret'
        )
        data = response['data']['data']
        return data['username'], data['password'], data['host']

# 使用Vault获取密钥
vault = VaultSecretManager()
username, password, host = vault.get_database_credentials()
# 密钥只在内存中,不会写入磁盘或日志
# 使用Vault Agent Sidecar注入密钥
# Kubernetes配置示例
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  template:
    spec:
      containers:
      # 主应用容器
      - name: app
        image: myapp:v1.0
        volumeMounts:
        - name: vault-secrets
          mountPath: /vault/secrets
          readOnly: true

      # Vault Agent Sidecar(自动获取和刷新密钥)
      - name: vault-agent
        image: vault:1.15
        env:
        - name: VAULT_ADDR
          value: "https://vault.internal:8200"
        - name: VAULT_TOKEN_FILE
          value: /vault/token
        volumeMounts:
        - name: vault-secrets
          mountPath: /vault/secrets

      volumes:
      - name: vault-secrets
        emptyDir:
          medium: Memory  # 使用内存存储密钥(不写入磁盘)

8.5 使用AWS Secrets Manager / Azure Key Vault

云厂商提供的密钥管理服务与企业云环境深度集成,是云原生场景下的首选方案。

# AWS Secrets Manager示例
# 安装AWS CLI
pip install awscli
aws configure

# 创建Secret
aws secretsmanager create-secret \
  --name app/database/credentials \
  --secret-string '{"username":"appuser","password":"SuperSecret123","host":"db.internal"}'

# 读取Secret
aws secretsmanager get-secret-value \
  --secret-id app/database/credentials \
  --query SecretString \
  --output text

# 自动轮换Secret(配置轮换Lambda)
aws secretsmanager rotate-secret \
  --secret-id app/database/credentials \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:rotate-db-password
# 在容器中使用AWS Secrets Manager(Python示例)
# 需要安装: pip install boto3
import boto3
import json
import os

class AWSSecretManager:
    def __init__(self, region='us-east-1'):
        self.client = boto3.client('secretsmanager', region_name=region)

    def get_secret(self, secret_name):
        """从AWS Secrets Manager获取密钥"""
        response = self.client.get_secret_value(SecretId=secret_name)
        if 'SecretString' in response:
            return json.loads(response['SecretString'])
        else:
            return response['SecretBinary']

    def get_database_credentials(self):
        """获取数据库凭据"""
        secret = self.get_secret('app/database/credentials')
        return secret['username'], secret['password'], secret['host']

# 使用
aws_sm = AWSSecretManager()
username, password, host = aws_sm.get_database_credentials()

8.6 Kubernetes Secrets简介

在Kubernetes环境中,Secrets是原生的密钥管理机制。

# 创建Kubernetes Secret
# 方式1: 从字面值创建
kubectl create secret generic db-secret \
  --from-literal=username=appuser \
  --from-literal=password=SuperSecret123 \
  --from-literal=host=db.internal

# 方式2: 从文件创建
kubectl create secret generic tls-secret \
  --from-file=tls.crt=/path/to/cert.pem \
  --from-file=tls.key=/path/to/key.pem

# 方式3: 从YAML文件创建
cat > db-secret.yaml << 'EOF'
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: production
type: Opaque
data:
  username: YXBwdXNlcg==       # base64编码的 appuser
  password: U3VwZXJTZWNyZXQxMjM=  # base64编码的 SuperSecret123
  host: ZGIuaW50ZXJuYWw=       # base64编码的 db.internal
EOF
kubectl apply -f db-secret.yaml

# 查看Secret
kubectl get secrets
kubectl describe secret db-secret

# 在Pod中使用Secret
cat > deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  template:
    spec:
      containers:
      - name: app
        image: myapp:v1.0
        env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password
        # 或以文件形式挂载
        volumeMounts:
        - name: secret-volume
          mountPath: /etc/secrets
          readOnly: true
      volumes:
      - name: secret-volume
        secret:
          secretName: db-secret
EOF

8.7 使用BuildKit secrets构建时密钥管理

第四章已经介绍了BuildKit secrets的基本用法,这里补充更多场景。

# 使用BuildKit secrets的多种场景
# syntax=docker/dockerfile:1.4

# 场景1: 从私有npm仓库安装依赖
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token \
    npm config set //registry.npmjs.org/:_authToken $(cat /run/secrets/npm_token) && \
    npm ci --production && \
    npm config delete //registry.npmjs.org/:_authToken

# 场景2: 从私有Git仓库拉取代码
FROM alpine:3.19 AS builder
RUN apk add --no-cache git openssh
RUN --mount=type=ssh \
    git clone git@github.com:myorg/private-repo.git /app

# 场景3: 从私有Docker Registry拉取基础镜像
# 在BuildKit中使用--mount=type=secret传递registry凭据
FROM alpine:3.19
RUN --mount=type=secret,id=registry_auth \
    cat /run/secrets/registry_auth | docker login myregistry.com -u $(head -1 /run/secrets/registry_auth) -p $(tail -1 /run/secrets/registry_auth) && \
    docker pull myregistry.com/base-image:v1.0

# 场景4: 使用构建时密钥解密配置文件
FROM alpine:3.19
COPY encrypted-config.enc /tmp/
RUN --mount=type=secret,id=decryption_key \
    openssl enc -d -aes-256-cbc \
    -in /tmp/encrypted-config.enc \
    -out /app/config.yaml \
    -pass file:/run/secrets/decryption_key && \
    rm /tmp/encrypted-config.enc

8.8 密钥轮换策略

密钥轮换是安全最佳实践,可以限制密钥泄露后的影响。以下是密钥轮换的策略和实现。

# 策略1: 定期轮换(如每90天)
# 使用Cron job自动轮换
cat > /etc/cron.d/secret-rotation << 'EOF'
# 每90天轮换数据库密码
0 0 1 */3 * root /opt/scripts/rotate-db-password.sh
EOF

# 数据库密码轮换脚本
cat > /opt/scripts/rotate-db-password.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

# 生成新密码
NEW_PASSWORD=$(openssl rand -base64 32)

# 更新Vault中的密钥
export VAULT_ADDR='https://vault.internal:8200'
export VAULT_TOKEN=$(cat /etc/vault/token)
vault kv put secret/app/database password="$NEW_PASSWORD"

# 更新数据库密码
PGPASSWORD=$(vault kv get -field=password secret/app/database/admin) \
  psql -h db.internal -U admin -d postgres \
  -c "ALTER USER appuser PASSWORD '$NEW_PASSWORD'"

# 触发应用重新读取密钥(滚动重启)
kubectl rollout restart deployment/app -n production

# 记录审计日志
logger "Database password rotated for appuser"
SCRIPT
chmod +x /opt/scripts/rotate-db-password.sh

# 策略2: 动态密钥(按需生成,用完即销毁)
# 使用Vault的数据库密钥引擎
vault secrets enable database

# 配置数据库连接
vault write database/config/my-postgresql \
  plugin_name=postgresql-database-plugin \
  allowed_roles="app-role" \
  connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/postgres?sslmode=disable" \
  username="vault-admin" \
  password="vault-admin-password"

# 创建动态密钥角色(每次获取都生成新凭据,默认TTL=1小时)
vault write database/roles/app-role \
  db_name=my-postgresql \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

# 应用获取动态凭据(每次获取都是新的)
vault read database/creds/app-role
# Key            Value
# lease_id       database/creds/app-role/abc123
# lease_duration 1h
# password       s8v-xyz-temp-password  (临时密码)
# username       v-app-role-xyz123       (临时用户)

8.9 密钥泄露检测与响应

# 密钥泄露检测工具

# 1. Trivy Secret扫描
trivy image --scanners secret myapp:v1.0
# 扫描镜像中的硬编码密钥

trivy fs --scanners secret /path/to/source-code
# 扫描源代码中的密钥

# 2. Gitleaks(扫描Git历史中的密钥)
gitleaks detect --source /path/to/repo --report-path leaks.json
# 扫描Git提交历史中的密钥泄露

# 3. GitGuardian
# 商业工具,提供更强大的密钥检测能力

# 4. TruffleHog
trufflehog filesystem --directory /path/to/repo
# 深度扫描文件系统中的密钥

# 密钥泄露响应流程
cat > /opt/scripts/secret-leak-response.sh << 'SCRIPT'
#!/bin/bash
# 密钥泄露应急响应脚本

echo "=== 密钥泄露应急响应 ==="

# 步骤1: 立即撤销泄露的密钥
echo "[步骤1] 撤销泄露的密钥..."
# 在Vault中删除或更新密钥
vault kv put secret/app/database password="$(openssl rand -base64 32)"

# 步骤2: 生成新密钥
echo "[步骤2] 生成新密钥..."
NEW_PASSWORD=$(openssl rand -base64 32)
vault kv put secret/app/database password="$NEW_PASSWORD"

# 步骤3: 更新所有使用该密钥的服务
echo "[步骤3] 滚动重启受影响的服务..."
kubectl rollout restart deployment/app -n production

# 步骤4: 审计泄露影响范围
echo "[步骤4] 审计泄露影响..."
# 检查日志中的异常访问
grep -r "password" /var/log/containers/ | grep -v "PASSWORD_FILE"

# 步骤5: 记录事件
echo "[步骤5] 记录安全事件..."
logger -p security.alert "密钥泄露事件: 数据库密码已轮换,影响范围待评估"

# 步骤6: 通知相关人员
echo "[步骤6] 发送通知..."
# 发送告警通知(Slack/邮件/短信)
curl -X POST -H 'Content-type: application/json' \
  --data '{"text":"SECURITY ALERT: 密钥泄露,已执行轮换"}' \
  https://hooks.slack.com/services/xxx/yyy/zzz

echo "=== 密钥泄露响应完成 ==="
SCRIPT
chmod +x /opt/scripts/secret-leak-response.sh

密钥管理方案对比:

方案适用场景安全等级密钥轮换审计日志动态密钥
环境变量开发环境手动不支持
Docker SecretsSwarm环境手动有限不支持
HashiCorp Vault生产环境自动完整支持
AWS Secrets ManagerAWS云环境自动(Lambda)CloudTrail有限
Azure Key VaultAzure云环境自动Azure Monitor有限
Kubernetes SecretsK8s环境手动/外部有限需External Secrets

第九章 容器安全扫描与合规

容器安全扫描和合规审计是发现安全隐患、满足法规要求的关键手段。本章将深入讲解主流容器安全扫描工具的使用方法,以及如何建立自动化的安全合规流程。

9.1 Trivy深度使用(镜像扫描、文件系统扫描、CI集成)

Trivy是当前最受欢迎的开源容器安全扫描工具,支持镜像漏洞扫描、配置文件扫描、密钥扫描等多种功能。

9.1.1 镜像漏洞扫描深度实战
# 基础镜像扫描
trivy image nginx:1.25-alpine

# 扫描本地镜像(需要先构建)
docker build -t myapp:v1.0 .
trivy image myapp:v1.0

# 按严重级别过滤
trivy image --severity HIGH,CRITICAL myapp:v1.0
# 只显示HIGH和CRITICAL级别的漏洞

# 输出为JSON格式(适合程序处理)
trivy image --format json --output trivy-report.json myapp:v1.0

# 输出为表格格式(适合人工审查)
trivy image --format table --output trivy-report.txt myapp:v1.0

# 输出为SARIF格式(适合GitHub Security Tab)
trivy image --format sarif --output trivy-report.sarif myapp:v1.0

# 输出为HTML报告
trivy image --format template \
  --template "@contrib/html.tpl" \
  --output trivy-report.html myapp:v1.0

# 设置退出码(用于CI/CD流水线)
# 只在发现HIGH或CRITICAL漏洞时返回非零退出码
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:v1.0
echo "退出码: $?"
# 0 = 没有HIGH/CRITICAL漏洞,CI/CD通过
# 1 = 有HIGH/CRITICAL漏洞,CI/CD失败

# 忽略特定漏洞(已知风险但暂不修复)
cat > .trivyignore << 'EOF'
# CVE-2024-0727: OpenSSL漏洞,已在其他层面缓解
CVE-2024-0727
# CVE-2023-1234: 不影响当前应用
CVE-2023-1234
EOF
trivy image --ignorefile .trivyignore myapp:v1.0

# 扫描镜像中的特定操作系统包
trivy image --scanners vuln --vuln-type os myapp:v1.0
# 只扫描操作系统级别的漏洞

# 扫描镜像中的语言依赖漏洞
trivy image --scanners vuln --vuln-type library myapp:v1.0
# 只扫描语言级别的漏洞(pip/npm/maven等)

# 同时扫描漏洞和密钥
trivy image --scanners vuln,secret myapp:v1.0
9.1.2 文件系统和配置文件扫描
# 扫描本地文件系统(用于扫描源代码或配置文件)
trivy fs --severity HIGH,CRITICAL /path/to/project

# 扫描Dockerfile(检查安全配置问题)
trivy config Dockerfile
# 检查项包括:
# - 是否以root用户运行
# - 是否使用latest标签
# - 是否使用ADD而非COPY
# - 是否安装了不必要的包
# - 是否有敏感信息

# 扫描docker-compose.yml
trivy config docker-compose.yml

# 扫描Kubernetes配置文件
trivy config k8s-deployment.yaml
# 检查项包括:
# - 是否使用特权容器
# - 是否挂载了docker.sock
# - 是否使用了过多的capabilities
# - 是否设置了资源限制
# - 是否使用了service account

# 扫描Terraform配置(IaC安全检查)
trivy config --scanners misconfig /path/to/terraform

# 扫描整个目录
trivy config /path/to/project/
9.1.3 Trivy实战案例:完整的安全扫描流程
#!/bin/bash
# 文件: security-scan.sh
# 完整的容器安全扫描脚本

set -euo pipefail

IMAGE_NAME="${1:-myapp:v1.0}"
REPORT_DIR="./security-reports"
DATE=$(date +%Y%m%d_%H%M%S)

echo "============================================"
echo "    容器安全扫描报告"
echo "    镜像: $IMAGE_NAME"
echo "    时间: $(date)"
echo "============================================"
echo ""

# 创建报告目录
mkdir -p "$REPORT_DIR"

# 步骤1: 漏洞扫描
echo "[1/4] 执行漏洞扫描..."
trivy image \
  --format json \
  --output "$REPORT_DIR/vuln-$DATE.json" \
  --severity HIGH,CRITICAL \
  "$IMAGE_NAME"

# 统计漏洞数量
HIGH_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity=="HIGH")] | length' "$REPORT_DIR/vuln-$DATE.json")
CRITICAL_COUNT=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' "$REPORT_DIR/vuln-$DATE.json")
echo "  CRITICAL漏洞: $CRITICAL_COUNT 个"
echo "  HIGH漏洞: $HIGH_COUNT 个"

# 步骤2: 密钥扫描
echo "[2/4] 执行密钥泄露扫描..."
trivy image \
  --format json \
  --output "$REPORT_DIR/secret-$DATE.json" \
  --scanners secret \
  "$IMAGE_NAME"

SECRET_COUNT=$(jq '[.Results[].Secrets[]?] | length' "$REPORT_DIR/secret-$DATE.json")
echo "  发现密钥泄露: $SECRET_COUNT 处"

# 步骤3: SBOM生成
echo "[3/4] 生成SBOM(软件物料清单)..."
syft "$IMAGE_NAME" -o cyclonedx-json > "$REPORT_DIR/sbom-$DATE.json"
PACKAGE_COUNT=$(jq '.components | length' "$REPORT_DIR/sbom-$DATE.json")
echo "  包含组件: $PACKAGE_COUNT 个"

# 步骤4: 安全评估总结
echo "[4/4] 安全评估总结..."
echo ""
echo "============================================"
echo "    安全评估结果"
echo "============================================"
echo "  镜像: $IMAGE_NAME"
echo "  CRITICAL漏洞: $CRITICAL_COUNT"
echo "  HIGH漏洞: $HIGH_COUNT"
echo "  密钥泄露: $SECRET_COUNT"
echo "  组件总数: $PACKAGE_COUNT"
echo "============================================"

# 判断是否通过安全检查
if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$SECRET_COUNT" -gt 0 ]; then
    echo "  结果: [不通过] 存在CRITICAL漏洞或密钥泄露"
    exit 1
elif [ "$HIGH_COUNT" -gt 5 ]; then
    echo "  结果: [不通过] HIGH漏洞超过5个"
    exit 1
else
    echo "  结果: [通过] 安全检查通过"
    exit 0
fi

9.2 Clair镜像漏洞扫描

# 使用Clair的REST API进行扫描
# Clair v4 API

# 提交镜像进行扫描
curl -X POST http://localhost:6060/api/v1/index_report \
  -H "Content-Type: application/json" \
  -d '{
    "manifest": "sha256:abcdef1234567890...",
    "hash": "sha256:abcdef1234567890..."
  }'

# 获取扫描结果
curl http://localhost:6060/api/v1/index_report/sha256:abcdef1234567890...

# Clair与Quay集成(Quay内置Clair)
# 在Quay配置文件中启用Clair扫描
# config.yaml:
# SECURITY_SCANNER: true

9.3 Anchore Engine策略评估

# Anchore策略评估(更灵活的安全检查)
# 策略文件定义了安全规则
cat > security-policy.json << 'EOF'
{
  "id": "strict-security-policy",
  "version": "1",
  "name": "Strict Security Policy",
  "rules": [
    {
      "id": "no-critical-vulns",
      "action": "stop",
      "gate": "vulnerabilities",
      "trigger": "vulnerability",
      "params": [
        {"name": "severity", "value": "Critical"},
        {"name": "package_type", "value": "all"}
      ]
    },
    {
      "id": "no-high-vulns",
      "action": "warn",
      "gate": "vulnerabilities",
      "trigger": "vulnerability",
      "params": [
        {"name": "severity", "value": "High"},
        {"name": "package_type", "value": "all"}
      ]
    },
    {
      "id": "no-root-user",
      "action": "stop",
      "gate": "dockerfile",
      "trigger": "user",
      "params": []
    },
    {
      "id": "no-secret-in-env",
      "action": "stop",
      "gate": "secret_scans",
      "trigger": "content_regex_search",
      "params": [
        {"name": "regex", "value": "(?i)(password|secret|key|token)"}
      ]
    }
  ]
}
EOF

# 添加策略
anchore-cli policy add security-policy.json

# 激活策略
anchore-cli policy activate strict-security-policy

# 评估镜像
anchore-cli evaluate check myapp:v1.0

# 输出示例:
# Image Digest: sha256:abcdef...
# Status: pass
# Last Eval: 2024-01-15T10:30:00Z
# Policy ID: strict-security-policy

9.4 Docker Bench Security(CIS基准检查)

Docker Bench Security是自动化执行CIS Docker Benchmark检查的脚本工具。

# 运行Docker Bench Security
docker run --rm --net host --pid host --userns host \
  --cap-add audit_control \
  -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  --label docker_bench_security \
  docker/docker-bench-security

# 输出解读:
# [PASS] - 检查项通过
# [WARN] - 检查项未通过,需要关注
# [NOTE] - 需要手动确认
# [INFO] - 信息性提示

# 只查看警告项
docker run --rm --net host --pid host --userns host \
  --cap-add audit_control \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  --label docker_bench_security \
  docker/docker-bench-security 2>&1 | grep "\[WARN\]"

# 生成JSON格式报告
docker run --rm --net host --pid host --userns host \
  --cap-add audit_control \
  -v /var/lib:/var/lib:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  -e DOCKER_BENCH_VERSION=2.1.0 \
  --label docker_bench_security \
  docker/docker-bench-security -l json 2>/dev/null > bench-report.json

9.5 kube-bench与kube-hunter

# kube-bench: CIS Kubernetes Benchmark检查工具
# 在Kubernetes集群中运行
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench

# 或在节点上直接运行
docker run --rm \
  -v `pwd`:/host \
  -v /var/lib:/var/lib:ro \
  -v /etc/systemd:/etc/systemd:ro \
  -v /etc/kubernetes:/etc/kubernetes:ro \
  aquasec/kube-bench:latest

# kube-hunter: Kubernetes安全漏洞扫描工具
# 发现集群中的已知漏洞和配置问题
docker run --rm --network host \
  aquasec/kube-hunter --active

# kube-hunter报告示例:
# vulnerabilities:
#   - name: "CAP_NET_RAW Enabled"
#     severity: "low"
#     location: "kube-proxy"
#   - name: "Anonymous Authentication"
#     severity: "high"
#     location: "kubelet"

9.6 Falco运行时安全监控

Falco是CNCF毕业项目,是容器运行时安全监控的事实标准。它通过内核模块或eBPF技术实时监控系统调用,基于规则检测异常行为。

# 使用Docker部署Falco
docker run -d --name falco \
  --privileged \
  -v /var/run/docker.sock:/host/var/run/docker.sock \
  -v /dev:/host/dev \
  -v /proc:/host/proc:ro \
  -v /boot:/host/boot:ro \
  -v /lib/modules:/host/lib/modules:ro \
  -v /usr:/host/usr:ro \
  -v /etc:/host/etc:ro \
  falcosecurity/falco:latest

# 查看Falco实时告警
docker logs -f falco

# Falco告警示例:
# 10:30:45.123456789: Warning Container started with privileged mode
# (container_id=abc123 image=myapp:v1.0 user=root)
#
# 10:31:02.234567890: Error Shell spawned by untrusted binary
# (container_id=def456 shell=/bin/sh parent=myapp)
#
# 10:31:15.345678901: Critical Write below /etc
# (container_id=ghi789 file=/etc/passwd user=appuser)
9.6.1 Falco规则编写
# 文件: /etc/falco/falco_rules.local.yaml
# 自定义Falco规则

# 规则1: 检测容器中执行Shell
- rule: Shell Spawned in Container
  desc: Detect shell execution inside a container
  condition: >
    spawned_process and container and
    proc.name in (bash, sh, zsh, ksh, ash, dash)
  output: >
    Shell spawned in container
    (user=%user.name container_id=%container.id
     container_name=%container.name
     image=%container.image.repository
     shell=%proc.name cmd=%proc.cmdline)
  priority: WARNING
  tags: [container, shell, mitre_execution]

# 规则2: 检测容器写入/etc目录
- rule: Write Below /etc in Container
  desc: Detect writes to /etc directory inside a container
  condition: >
    open_write and container and
    fd.name startswith /etc/
  output: >
    Write below /etc in container
    (user=%user.name container_id=%container.id
     file=%fd.name proc=%proc.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, filesystem, mitre_persistence]

# 规则3: 检测容器中安装新软件包
- rule: Package Installation in Container
  desc: Detect package manager execution inside a container
  condition: >
    spawned_process and container and
    (proc.name in (apt, apt-get, dpkg, yum, dnf, rpm, apk, pip, npm, gem))
  output: >
    Package installation in container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     package_manager=%proc.name cmd=%proc.cmdline)
  priority: WARNING
  tags: [container, package, mitre_persistence]

# 规则4: 检测容器访问Docker Socket
- rule: Docker Socket Access in Container
  desc: Detect access to Docker socket from inside a container
  condition: >
    open_read and container and
    fd.name=/var/run/docker.sock
  output: >
    Docker socket accessed from container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, docker, privilege_escalation]

# 规则5: 检测容器中运行挖矿程序
- rule: Crypto Miner in Container
  desc: Detect cryptocurrency mining software in container
  condition: >
    spawned_process and container and
    (proc.name in (xmrig, minerd, cryptonight, ccminer, ethminer) or
     proc.cmdline contains "stratum+tcp" or
     proc.cmdline contains "stratum+ssl" or
     proc.cmdline contains "miningpool")
  output: >
    Cryptocurrency miner detected in container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, malware, cryptomining]

# 规则6: 检测特权容器启动
- rule: Privileged Container Started
  desc: Detect privileged container start
  condition: >
    container and container.privileged=true
  output: >
    Privileged container started
    (user=%user.name container_id=%container.id
     container_name=%container.name image=%container.image.repository)
  priority: CRITICAL
  tags: [container, privilege_escalation]

# 宏定义(可复用的条件)
- macro: open_write
  condition: (evt.type in (open,openat,openat2,creat) and evt.is_open_write=true)

- macro: spawned_process
  condition: (evt.type=execve and evt.dir=<)

9.7 Sysdig Secure容器安全平台

Sysdig Secure是基于Falco的商业容器安全平台,提供企业级的安全监控和合规能力。

# Sysdig Secure提供以下核心功能:
# 1. 运行时威胁检测(基于Falco规则)
# 2. 合规性审计(CIS Benchmark、PCI-DSS、GDPR)
# 3. 容器取证和事件响应
# 4. 容器漏洞扫描
# 5. 网络安全监控

# 安装Sysdig Agent
curl -s https://download.sysdig.com/stable/install-agent | sudo bash -s -- \
  --access_key YOUR_ACCESS_KEY \
  --tags role:webapp,env:production

# 使用Sysdig CLI查询安全事件
sysdig-cli-scanner --apiurl https://secure.sysdig.com \
  --apikey YOUR_API_KEY \
  scan myapp:v1.0

# 查看运行时安全事件
sysdig-cli event list --severity critical

9.8 合规性报告生成

# 使用Docker Bench生成CIS合规报告
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /etc:/etc:ro \
  -v /var/lib:/var/lib:ro \
  docker/docker-bench-security -l json > cis-report.json 2>/dev/null

# 解析报告
jq '.tests[] | {
  test: .test_desc,
  results: [.results[] | {
    id: .test_number,
    status: .result,
    description: .test_desc
  }]
}' cis-report.json

# 生成合规性摘要
TOTAL=$(jq '[.tests[].results[]] | length' cis-report.json)
PASS=$(jq '[.tests[].results[] | select(.result=="PASS")] | length' cis-report.json)
WARN=$(jq '[.tests[].results[] | select(.result=="WARN")] | length' cis-report.json)
INFO=$(jq '[.tests[].results[] | select(.result=="INFO")] | length' cis-report.json)

echo "=== CIS Docker Benchmark 合规报告 ==="
echo "总检查项: $TOTAL"
echo "通过: $PASS"
echo "警告: $WARN"
echo "信息: $INFO"
echo "合规率: $(echo "scale=1; $PASS * 100 / $TOTAL" | bc)%"

9.9 安全扫描自动化CI/CD流水线

# 完整的容器安全CI/CD流水线(GitLab CI)
# 文件: .gitlab-ci.yml
stages:
  - lint
  - build
  - scan
  - sign
  - sbom
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

# Dockerfile安全检查
dockerfile-lint:
  stage: lint
  image: hadolint/hadolint:latest-alpine
  script:
    - hadolint Dockerfile
  allow_failure: false

# 构建镜像
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $IMAGE .
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $IMAGE
  only:
    - main

# Trivy漏洞扫描
trivy-vuln-scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE
  only:
    - main

# Trivy密钥扫描
trivy-secret-scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --scanners secret --exit-code 1 $IMAGE
  only:
    - main

# CIS基准检查
docker-bench:
  stage: scan
  image: docker/docker-bench-security:latest
  script:
    - docker-bench-security -l json > bench-report.json
    - |
      PASS=$(jq '[.tests[].results[] | select(.result=="PASS")] | length' bench-report.json)
      TOTAL=$(jq '[.tests[].results[]] | length' bench-report.json)
      echo "CIS合规率: $((PASS * 100 / TOTAL))%"
  artifacts:
    paths:
      - bench-report.json

# 生成SBOM
generate-sbom:
  stage: sbom
  image: anchore/syft:latest
  script:
    - syft $IMAGE -o cyclonedx-json > sbom.json
    - syft $IMAGE -o spdx-json > sbom-spdx.json
  artifacts:
    paths:
      - sbom.json
      - sbom-spdx.json

# 镜像签名
sign-image:
  stage: sign
  image: gcr.io/projectsigstore/cosign:latest
  script:
    - echo "$COSIGN_PRIVATE_KEY" > cosign.key
    - cosign sign --key cosign.key --yes $IMAGE
    - cosign attest --predicate sbom.json --type cyclonedx --key cosign.key --yes $IMAGE
  only:
    - main

# 部署(所有安全检查通过后)
deploy:
  stage: deploy
  script:
    # 验证镜像签名
    - cosign verify --key $COSIGN_PUBLIC_KEY $IMAGE
    # 部署到Kubernetes
    - kubectl set image deployment/app app=$IMAGE -n production
    - kubectl rollout status deployment/app -n production
  only:
    - main
  when: manual
  environment:
    name: production

第十章 运行时安全监控与响应

运行时安全监控是容器安全最后一道防线。即使前面所有的安全措施都到位,仍有可能出现未知漏洞或0-day攻击。运行时监控可以及时发现和响应这些安全事件。

10.1 容器运行时安全监控概述

运行时安全监控通过实时分析容器的系统调用、网络行为、文件操作等,检测异常行为和安全事件。

容器运行时安全监控层次:

┌─────────────────────────────────────────────────────┐
│ 第1层: 系统调用监控(Falco/eBPF)                     │
│   监控容器进程的系统调用,检测异常行为                │
├─────────────────────────────────────────────────────┤
│ 第2层: 网络行为监控(Cilium/Calico)                  │
│   监控容器网络流量,检测异常连接                      │
├─────────────────────────────────────────────────────┤
│ 第3层: 文件完整性监控(FIM)                           │
│   监控关键文件的变更,检测篡改                        │
├─────────────────────────────────────────────────────┤
│ 第4层: 进程行为监控                                   │
│   监控容器内进程的创建和执行,检测恶意进程            │
├─────────────────────────────────────────────────────┤
│ 第5层: 资源使用监控                                   │
│   监控CPU/内存/网络的异常使用,检测DoS攻击            │
└─────────────────────────────────────────────────────┘

10.2 Falco规则编写与告警

第九章已经介绍了Falco的基本使用,这里补充更深入的规则编写和告警集成。

# 文件: /etc/falco/falco_rules.local.yaml
# 高级Falco规则

# 检测容器逃逸尝试
- rule: Container Escape Attempt
  desc: Detect potential container escape attempts
  condition: >
    container and (
      evt.type=mount or
      evt.type=umount or
      (evt.type=openat and fd.name startswith /host) or
      (evt.type=connect and fd.sip="169.254.169.254") or
      proc.name in (nsenter, mount, umount)
    )
  output: >
    Potential container escape detected
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline
     evt=%evt.type file=%fd.name)
  priority: CRITICAL
  tags: [container, escape, mitre_privilege_escalation]

# 检测反向Shell
- rule: Reverse Shell Connection
  desc: Detect reverse shell connections
  condition: >
    evt.type=connect and
    fd.typechar=4 and
    proc.name in (bash, sh, zsh, ksh, python, python3, perl, ruby, nc, ncat) and
    not fd.sip in (127.0.0.1, "::1")
  output: >
    Reverse shell detected
    (user=%user.name container_id=%container.id
     proc=%proc.name cmd=%proc.cmdline
     dest_ip=%fd.sip dest_port=%fd.sport)
  priority: CRITICAL
  tags: [container, shell, malware]

# 检测容器内权限提升
- rule: Privilege Escalation in Container
  desc: Detect privilege escalation attempts in containers
  condition: >
    container and
    (
      (evt.type=setuid and evt.dir=< and uid=0) or
      (evt.type=setgid and evt.dir=< and gid=0) or
      (evt.type=prctl and evt.arg.option=PR_SET_SETUID)
    )
  output: >
    Privilege escalation attempt in container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline
     target_uid=%evt.arg.uid)
  priority: HIGH
  tags: [container, privilege_escalation]
# 将Falco告警集成到Slack
# 编辑Falco配置文件
cat > /etc/falco/falco.yaml << 'EOF'
rules_file:
  - /etc/falco/falco_rules.yaml
  - /etc/falco/falco_rules.local.yaml

json_output: true
json_include_output_property: true

# HTTP输出(发送告警到Webhook)
http_output:
  enabled: true
  url: "http://falco-forwarder:2801/"
  user_agent: "falco"
  retry_config:
    max_attempts: 3

# 文件输出
file_output:
  enabled: true
  keep_alive: false
  filename: /var/log/falco/events.log

# 程序输出(调用外部程序处理告警)
program_output:
  enabled: true
  keep_alive: true
  program: "jq '{text: .output} | @base64' | curl -H 'Content-Type: application/json' -d @- https://hooks.slack.com/services/xxx"
EOF

# Falco Sidekick(高级告警转发)
docker run -d --name falco-sidekick \
  -p 2801:2801 \
  -e SLACK_WEBHOOKURL=https://hooks.slack.com/services/xxx \
  -e ELASTICSEARCH_URL=http://elasticsearch:9200 \
  -e PAGERDUTY_ROUTINGKEY=xxx \
  falcosecurity/falcosidekick

10.3 容器行为基线与异常检测

# 使用Falco学习模式建立行为基线
# Falco支持将规则设置为"学习模式",记录正常行为

# 步骤1: 部署Falco并使用学习规则运行一段时间(如1周)
# 学习规则不产生告警,只记录行为
cat > /etc/falco/falco_rules.local.yaml << 'EOF'
- rule: Learn Container Behavior
  desc: Learn normal container behavior
  condition: container
  output: >
    BEHAVIOR user=%user.name container_id=%container.id
    image=%container.image.repository
    proc=%proc.name cmd=%proc.cmdline
    evt=%evt.type file=%fd.name
  priority: DEBUG
  tags: [learning]
EOF

# 步骤2: 收集基线数据
docker logs falco 2>&1 | grep "BEHAVIOR" > container-baseline.log

# 步骤3: 分析基线数据,生成正常行为白名单
cat container-baseline.log | \
  jq -r '.output_fields' | \
  jq -s 'group_by(.proc) | map({proc: .[0].proc, count: length}) | sort_by(-.count)' \
  > process-whitelist.json

# 步骤4: 基于白名单创建异常检测规则
cat > /etc/falco/falco_rules.local.yaml << 'EOF'
- macro: known_process
  condition: (proc.name in (nginx, node, python3, java, redis-server, postgres))

- rule: Unknown Process in Container
  desc: Detect unknown process execution in container
  condition: >
    spawned_process and container and not known_process
  output: >
    Unknown process in container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline)
  priority: HIGH
  tags: [container, anomaly, mitre_execution]
EOF

10.4 容器逃逸检测

# Falco容器逃逸检测规则
# 文件: /etc/falco/falco_rules.local.yaml

# 检测通过挂载宿主机文件系统逃逸
- rule: Mount Host Filesystem from Container
  desc: Detect mount of host filesystem from container
  condition: >
    container and
    evt.type=mount and
    evt.arg.source in (/proc, /sys, /dev, /, /var/lib/docker)
  output: >
    Host filesystem mount from container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     source=%evt.arg.source target=%evt.arg.target
     proc=%proc.name)
  priority: CRITICAL
  tags: [container, escape]

# 检测通过nsenter进入宿主机命名空间
- rule: Namespace Enter from Container
  desc: Detect nsenter usage in container
  condition: >
    container and
    proc.name=nsenter
  output: >
    nsenter executed in container (potential escape)
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape]

# 检测通过Docker Socket逃逸
- rule: Docker API Access from Container
  desc: Detect Docker API access from container
  condition: >
    container and
    ((evt.type=connect and fd.name=/var/run/docker.sock) or
     (evt.type=openat and fd.name=/var/run/docker.sock))
  output: >
    Docker socket access from container (potential escape)
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape]

# 检测CVE-2019-5736 runc逃逸模式
- rule: Potential runc Exploit CVE-2019-5736
  desc: Detect potential CVE-2019-5736 exploitation
  condition: >
    open_write and
    fd.name startswith /proc/self/exe and
    container
  output: >
    Potential CVE-2019-5736 exploitation detected
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape, CVE-2019-5736]

10.5 恶意容器检测

# 恶意容器行为检测规则

# 检测容器中的挖矿程序
- rule: Cryptocurrency Mining Detection
  desc: Detect cryptocurrency mining activity
  condition: >
    container and (
      proc.name in (xmrig, minerd, cpuminer, ethminer, phoenixminer, lolminer) or
      proc.cmdline contains "stratum+tcp" or
      proc.cmdline contains "stratum+ssl" or
      proc.cmdline contains "monero" or
      proc.cmdline contains "ethash" or
      (evt.type=connect and fd.sport in (3333, 4444, 5555, 7777, 14444))
    )
  output: >
    Cryptocurrency mining detected in container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name cmd=%proc.cmdline
     dest_ip=%fd.sip dest_port=%fd.sport)
  priority: CRITICAL
  tags: [container, malware, cryptomining]

# 检测容器中的反弹Shell
- rule: Container Reverse Shell
  desc: Detect reverse shell in container
  condition: >
    container and
    evt.type=connect and
    fd.typechar=4 and
    proc.name in (sh, bash, zsh, ksh, ash) and
    not fd.sip in (127.0.0.1, "::1", "0.0.0.0")
  output: >
    Reverse shell in container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name dest_ip=%fd.sip dest_port=%fd.sport)
  priority: CRITICAL
  tags: [container, malware, reverse_shell]

# 检测容器中的后门
- rule: Suspicious Network Connection from Container
  desc: Detect suspicious outbound connections
  condition: >
    container and
    evt.type=connect and
    fd.typechar=4 and
    not fd.sip in (127.0.0.1, "::1") and
    not fd.sip startswith "10." and
    not fd.sip startswith "172.1" and
    not fd.sip startswith "172.2" and
    not fd.sip startswith "172.3" and
    not fd.sip startswith "192.168."
  output: >
    Suspicious outbound connection from container
    (user=%user.name container_id=%container.id
     image=%container.image.repository
     proc=%proc.name dest_ip=%fd.sip dest_port=%fd.sport)
  priority: WARNING
  tags: [container, network, suspicious]

10.6 安全事件响应流程(IR)

#!/bin/bash
# 文件: container-incident-response.sh
# 容器安全事件响应脚本

set -euo pipefail

INCIDENT_ID=$(date +%Y%m%d_%H%M%S)
CONTAINER_ID="${1:-}"
RESPONSE_DIR="/var/log/incidents/$INCIDENT_ID"

if [ -z "$CONTAINER_ID" ]; then
    echo "用法: $0 <容器ID>"
    exit 1
fi

echo "=== 容器安全事件响应 ==="
echo "事件ID: $INCIDENT_ID"
echo "容器ID: $CONTAINER_ID"
echo ""

mkdir -p "$RESPONSE_DIR"

# 阶段1: 容器取证(冻结证据)
echo "[阶段1] 容器取证..."
# 保存容器状态(不停止容器,保留内存状态)
docker checkpoint create "$CONTAINER_ID" checkpoint-$INCIDENT_ID 2>/dev/null || true

# 保存容器信息
docker inspect "$CONTAINER_ID" > "$RESPONSE_DIR/container-inspect.json"

# 保存容器日志
docker logs "$CONTAINER_ID" > "$RESPONSE_DIR/container-logs.txt" 2>&1

# 保存容器进程列表
docker top "$CONTAINER_ID" > "$RESPONSE_DIR/container-processes.txt" 2>&1

# 保存容器网络连接
docker exec "$CONTAINER_ID" sh -c 'netstat -tlnp 2>/dev/null || ss -tlnp' > "$RESPONSE_DIR/container-network.txt" 2>&1 || true

# 保存容器文件系统快照
docker export "$CONTAINER_ID" | gzip > "$RESPONSE_DIR/container-fs.tar.gz"

# 阶段2: 隔离容器(断开网络)
echo "[阶段2] 隔离容器..."
# 断开容器所有网络
for net in $(docker inspect "$CONTAINER_ID" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'); do
    docker network disconnect "$net" "$CONTAINER_ID" 2>/dev/null || true
done
echo "  容器已断开所有网络"

# 阶段3: 分析事件
echo "[阶段3] 分析事件..."
# 检查Falco告警
grep "$CONTAINER_ID" /var/log/falco/events.log > "$RESPONSE_DIR/falco-alerts.txt" 2>/dev/null || true

# 检查Docker审计日志
ausearch -k docker-socket > "$RESPONSE_DIR/audit-logs.txt" 2>/dev/null || true

# 分析容器镜像
IMAGE=$(docker inspect "$CONTAINER_ID" --format '{{.Config.Image}}')
echo "  镜像: $IMAGE"
trivy image --format json --output "$RESPONSE_DIR/trivy-scan.json" "$IMAGE" 2>/dev/null || true

# 阶段4: 影响评估
echo "[阶段4] 影响评估..."
# 检查同宿主机上的其他容器
docker ps --format '{{.ID}} {{.Names}} {{.Image}}' > "$RESPONSE_DIR/other-containers.txt"

# 检查宿主机是否被影响
ps aux | grep -v grep | grep "$CONTAINER_ID" > "$RESPONSE_DIR/host-processes.txt"

# 阶段5: 响应措施
echo "[阶段5] 响应措施..."
# 选项A: 停止容器(保守做法)
# docker stop "$CONTAINER_ID"

# 选项B: 暂停容器(保留内存状态用于分析)
# docker pause "$CONTAINER_ID"

# 选项C: 删除容器(确认无价值后)
# docker rm -f "$CONTAINER_ID"

echo "  建议措施:"
echo "  1. 分析取证数据: $RESPONSE_DIR/"
echo "  2. 检查是否有容器逃逸迹象"
echo "  3. 评估是否需要重建容器"
echo "  4. 更新安全规则防止再次发生"

# 阶段6: 通知
echo "[阶段6] 通知..."
echo "  发送安全事件通知..."

# 生成事件报告
cat > "$RESPONSE_DIR/incident-report.md" << REPORT
# 容器安全事件报告

## 事件ID: $INCIDENT_ID
## 时间: $(date)
## 容器ID: $CONTAINER_ID
## 镜像: $IMAGE

## 事件描述
[在此填写事件描述]

## 取证数据
- 容器信息: container-inspect.json
- 容器日志: container-logs.txt
- 进程列表: container-processes.txt
- 网络连接: container-network.txt
- 文件系统快照: container-fs.tar.gz
- Falco告警: falco-alerts.txt

## 影响评估
[在此填写影响评估]

## 响应措施
[在此填写已采取的措施]

## 后续行动
- [ ] 分析取证数据
- [ ] 确认根因
- [ ] 更新安全规则
- [ ] 修复漏洞
- [ ] 编写事件总结报告
REPORT

echo ""
echo "=== 事件响应完成 ==="
echo "报告目录: $RESPONSE_DIR/"

10.7 容器取证

# 容器取证工具和技术

# 1. 使用dd捕获容器内存
docker exec <容器ID> dd if=/proc/1/mem of=/tmp/memory.dump 2>/dev/null

# 2. 使用Volatility分析内存转储
# (需要将内存转储从容器中提取出来)
volatility -f memory.dump linux.pslist
volatility -f memory.dump linux.bash
volatility -f memory.dump linux.netstat

# 3. 使用Dive分析镜像层
# 安装Dive
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest <镜像名>

# 4. 提取镜像层文件进行分析
# 将镜像保存为tar文件
docker save myapp:v1.0 -o image.tar

# 解压并分析
mkdir image-analysis
tar xf image.tar -C image-analysis
cd image-analysis
# 每个层是一个tar.gz文件
for layer in */layer.tar; do
    echo "=== 分析层: $layer ==="
    tar tf "$layer" | head -20
done

# 5. 时间线分析
# 创建容器文件系统的时间线
docker run --rm -v /var/lib/docker:/host:ro alpine sh -c '
  find /host/overlay2/<层ID>/diff -type f -exec stat --format="%Y %n" {} \;' | \
  sort -n > timeline.txt

10.8 Docker安全最佳实践完整清单

以下是贯穿全文的Docker安全最佳实践完整清单:

============================================================
           Docker安全最佳实践完整清单
============================================================

一、镜像安全
  □ 使用可信来源的官方或私有仓库镜像
  □ 使用digest而非tag固定镜像版本
  □ 使用最小化基础镜像(scratch/distroless/alpine)
  □ 启用Docker Content Trust或Cosign镜像签名
  □ 定期扫描镜像漏洞(Trivy/Clair/Anchore)
  □ 扫描镜像中的敏感信息(Trivy Secret/Gitleaks)
  □ 不使用latest标签
  □ 定期更新基础镜像
  □ 生成并保存SBOM

二、Dockerfile安全
  □ 创建并使用非root用户
  □ 使用多阶段构建
  □ 使用.dockerignore排除敏感文件
  □ 使用BuildKit secrets管理构建时密钥
  □ 固定所有软件包版本
  □ 使用--no-install-recommends/--no-cache
  □ 使用COPY而非ADD(除非需要解压)
  □ 合并RUN指令减少层数
  □ 清理缓存和临时文件
  □ 设置HEALTHCHECK
  □ 添加OCI标准标签
  □ 暴露非特权端口(>1024)

三、容器运行时安全
  □ 以非root用户运行容器(--user)
  □ 启用只读文件系统(--read-only)
  □ 丢弃所有Capabilities,只添加必要的(--cap-drop=ALL)
  □ 禁止特权模式(绝对不使用--privileged)
  □ 启用no-new-privileges
  □ 配置自定义Seccomp profile
  □ 启用AppArmor/SELinux
  □ 设置资源限制(CPU/内存/IO/PID)
  □ 使用tmpfs挂载临时目录
  □ 限制日志大小

四、Docker守护进程安全
  □ 启用TLS加密Daemon通信(端口2376)
  □ 禁用未加密API(端口2375)
  □ 启用用户命名空间重映射(userns-remap)
  □ 限制Docker Socket访问
  □ 禁用不安全的注册中心
  □ 配置审计日志(auditd)
  □ 启用live-restore
  □ 禁用icc(容器间默认通信)
  □ 配置默认ulimit限制
  □ 使用Rootless Docker(如可能)

五、网络安全
  □ 使用自定义网络隔离服务
  □ 使用internal网络隔离敏感服务
  □ 限制容器出站网络访问
  □ 使用iptables/Network Policy控制容器间通信
  □ 只暴露必要端口
  □ 绑定到localhost(不暴露到外部)
  □ 使用mTLS加密容器间通信
  □ 实施零信任网络架构

六、密钥管理
  □ 不在镜像中硬编码密钥
  □ 不使用环境变量传递密钥
  □ 使用Docker Secrets/Vault/KMS管理密钥
  □ 实施密钥轮换策略
  □ 配置密钥访问审计日志
  □ 使用动态密钥(如可能)

七、安全扫描与合规
  □ 在CI/CD中集成Trivy漏洞扫描
  □ 定期运行Docker Bench Security
  □ 运行kube-bench检查K8s配置
  □ 生成合规性报告(CIS/PCI-DSS)
  □ 使用Falco进行运行时监控
  □ 定期进行渗透测试

八、运行时监控与响应
  □ 部署Falco运行时安全监控
  □ 建立容器行为基线
  □ 配置容器逃逸检测规则
  □ 配置恶意容器检测规则
  □ 建立安全事件响应流程
  □ 定期进行安全演练
  □ 配置告警通知(Slack/邮件/短信)

九、持续安全
  □ 建立安全基线并定期更新
  □ 跟踪安全漏洞通告(CVE)
  □ 及时更新Docker和容器运行时
  □ 及时更新宿主机内核
  □ 定期审查安全配置
  □ 安全左移(在开发阶段引入安全检查)
============================================================

10.9 本章总结与下一期预告

本章系统讲解了容器运行时安全监控与响应的各个方面。从Falco规则编写到容器逃逸检测,从恶意容器检测到安全事件响应流程,从容器取证到完整的安全最佳实践清单,涵盖了运行时安全的全流程。

关键要点回顾:

  1. 运行时监控是最后一道防线。 即使构建和分发阶段的安全措施都到位,仍需要运行时监控来发现0-day攻击和未知威胁。

  2. Falco是运行时监控的核心工具。 通过基于eBPF的系统调用监控,Falco可以实时检测容器中的异常行为。

  3. 行为基线是异常检测的基础。 通过学习容器的正常行为模式,可以更准确地检测异常。

  4. 事件响应流程要预先建立。 安全事件发生时的每一秒都至关重要,预先建立响应流程和取证工具可以大幅缩短响应时间。

  5. 安全是一个持续过程。 容器安全不是一次性的工作,而是需要持续监控、更新和改进的循环过程。

下一期预告:

在Docker专栏的下一篇文章中,我们将深入探讨Docker Compose与多容器应用编排,包括:

  • Docker Compose完整语法详解
  • 多服务应用的编排实战
  • 开发/测试/生产环境配置管理
  • Docker Compose与Swarm的结合使用
  • 从Docker Compose迁移到Kubernetes

总结

本文从Docker安全的底层原理到实战应用,系统全面地讲解了容器安全的所有关键知识。全文涵盖十大章节,超过3万字,内容包括:

底层原理篇(第一、二章): 深入讲解了Docker安全模型(命名空间+cgroups+Capabilities)、容器隔离机制的原理与局限性、Seccomp/AppArmor/SELinux等安全模块的配置、Rootless Docker等。理解这些底层机制是做好容器安全的前提。

镜像安全篇(第三、四章): 从镜像来源安全、签名验证、漏洞扫描、供应链安全(SLSA/SBOM),到Dockerfile安全编写的各项原则(非root用户、最小化镜像、多阶段构建、BuildKit密钥管理等),全面覆盖了镜像构建和分发的安全要求。

运行时安全篇(第五章): 提供了完整的容器运行时安全配置,包括非root运行、只读文件系统、Capabilities限制、特权模式禁止、Seccomp/AppArmor/SELinux配置、资源限制、no-new-privileges等,并给出了完整的docker run安全参数模板。

基础设施安全篇(第六、七章): 详细讲解了Docker守护进程安全加固(TLS加密通信、Socket访问限制、用户命名空间重映射、审计日志等)和容器网络安全(网络隔离、iptables加固、自定义网络、mTLS、零信任架构等)。

密钥管理篇(第八章): 分析了环境变量传递密钥的风险,介绍了Docker Secrets、HashiCorp Vault、AWS Secrets Manager、Kubernetes Secrets等密钥管理方案,以及密钥轮换和泄露响应策略。

扫描合规篇(第九章): 深入讲解了Trivy、Clair、Anchore等扫描工具的使用,Docker Bench Security和kube-bench的CIS基准检查,以及安全扫描自动化CI/CD流水线的搭建。

监控响应篇(第十章): 系统讲解了Falco运行时安全监控、容器行为基线建立、容器逃逸检测、恶意容器检测、安全事件响应流程和容器取证,并给出了Docker安全最佳实践完整清单。

容器安全是一个系统工程,需要在生命周期的每个环节——从构建到分发、从运行到编排——都部署相应的安全控制措施。希望本文能帮助读者建立起完整的容器安全知识体系和实战能力,在生产环境中构建起坚实的容器安全防线。

免责声明: 本文中的安全命令和配置示例仅供学习和参考。在实际生产环境中部署前,请充分测试并根据具体环境调整。部分演示命令(如特权容器逃逸)具有危险性,请勿在生产环境中执行。

更多推荐