Docker 安全最佳实践
容器安全是生产环境部署的关键考量。本章将详细介绍 Docker 的安全机制和最佳实践,帮助你构建安全的容器化应用。
安全概述
Docker 安全涉及四个主要领域:
- 内核安全:Linux 内核的命名空间(Namespaces)和控制组(Cgroups)
- 守护进程安全:Docker Daemon 的攻击面
- 容器配置安全:容器的默认配置和自定义配置
- 内核加固特性:AppArmor、SELinux、Seccomp 等安全模块
容器安全架构
┌─────────────────────────────────────────────────────────┐
│ 应用程序 │
├─────────────────────────────────────────────────────────┤
│ 容器运行时 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Seccomp │ │ AppArmor │ │ Capabilities│ │
│ │ 系统调用过滤│ │ 访问控制 │ │ 能力控制 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Linux 内核安全 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Namespaces │ │ Cgroups │ │ User NS │ │
│ │ 命名空间隔离│ │ 资源限制 │ │ 用户命名空间│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
内核安全机制
命名空间(Namespaces)
命名空间提供了容器的第一层隔离。每个容器拥有独立的:
| 命名空间类型 | 隔离内容 | 说明 |
|---|---|---|
| PID | 进程 ID | 容器内进程独立编号 |
| NET | 网络栈 | 独立网络接口、路由表 |
| MNT | 挂载点 | 独立文件系统视图 |
| IPC | 进程间通信 | 独立消息队列、信号量 |
| UTS | 主机名和域名 | 独立主机名 |
| USER | 用户和用户组 | 独立用户 ID 映射 |
命名空间隔离效果:
# 在容器内查看进程
docker run -it alpine ps aux
# 只能看到容器内的进程
# 在主机上查看
ps aux | grep docker
# 可以看到容器进程,但属于不同的命名空间
网络隔离:
# 每个容器拥有独立的网络栈
docker run -d --name web nginx
docker exec web ip addr
# 容器有独立的 IP 地址和网络接口
控制组(Cgroups)
Cgroups 实现资源限制和审计,防止单个容器耗尽系统资源:
# 限制容器内存
docker run -d --memory="512m" --memory-swap="1g" nginx
# 限制 CPU 使用
docker run -d --cpus="1.5" --cpuset-cpus="0,1" nginx
# 限制 IO
docker run -d --device-read-bps=/dev/sda:10mb nginx
# 查看容器资源使用
docker stats
Cgroups 的安全作用:
- 防止 DoS 攻击:限制单个容器的资源使用
- 公平调度:确保多个容器公平分享资源
- 资源审计:记录资源使用情况
Linux Capabilities
什么是 Capabilities?
传统的 Linux 权限模型是二元的:root(超级用户)或非 root。Capabilities 将 root 权限分解为细粒度的权限单元,实现了更精细的访问控制。
常用的 Capabilities:
| Capability | 说明 | 风险级别 |
|---|---|---|
| CAP_NET_BIND_SERVICE | 绑定 1024 以下端口 | 低 |
| CAP_NET_RAW | 使用原始套接字 | 中 |
| CAP_SYS_ADMIN | 系统管理操作 | 高 |
| CAP_CHOWN | 修改文件所有者 | 低 |
| CAP_DAC_OVERRIDE | 绕过文件权限检查 | 中 |
| CAP_KILL | 发送信号给其他进程 | 中 |
| CAP_SETUID/SETGID | 修改进程 UID/GID | 中 |
Docker 默认 Capabilities
Docker 默认使用白名单方式,只保留必要的 Capabilities:
# 查看容器默认的 capabilities
docker run --rm alpine capsh --print
# 输出示例
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
管理容器 Capabilities
删除不需要的 Capabilities:
# 删除所有 capabilities
docker run --rm --cap-drop=ALL alpine capsh --print
# 删除特定 capability
docker run --rm --cap-drop=NET_RAW alpine capsh --print
# 只保留需要的 capabilities
docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
添加 Capabilities:
# 添加特定 capability
docker run --cap-add=NET_ADMIN alpine
# 添加所有 capabilities(不推荐)
docker run --privileged alpine
最佳实践示例:
# Web 服务器只需要绑定特权端口的能力
docker run -d \
--name web \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-p 80:80 \
nginx
# 需要修改网络配置的容器
docker run -d \
--name network-tool \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
network-tool
非 Root 用户运行容器
为什么使用非 root 用户?
默认情况下,容器内的 root 用户与主机的 root 用户共享 UID 0。如果容器被攻破,攻击者可能获得主机的 root 权限。
在 Dockerfile 中配置非 root 用户
# 创建专用用户和组
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 或指定 UID/GID
RUN groupadd -g 1000 appuser && \
useradd -u 1000 -g appuser -m appuser
# 设置工作目录权限
WORKDIR /app
COPY --chown=appuser:appuser . .
# 切换到非 root 用户
USER appuser
# 启动应用
CMD ["node", "server.js"]
完整示例:
FROM node:18-alpine
# 安装依赖阶段
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# 复制应用代码
COPY --chown=nextjs:nodejs . .
# 切换用户
USER nextjs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# 启动命令
CMD ["node", "server.js"]
运行时指定用户
# 以特定用户运行
docker run -d --user 1000:1000 nginx
# 使用用户名
docker run -d --user appuser:appgroup myapp
# 以只读文件系统运行
docker run -d --read-only --user 1000:1000 myapp
用户命名空间
用户命名空间将容器内的 root 用户映射到主机上的非特权用户,提供额外的安全层。
启用用户命名空间
配置 Docker Daemon:
# /etc/docker/daemon.json
{
"userns-remap": "default"
}
或指定特定用户:
{
"userns-remap": "dockremap:dockremap"
}
创建映射用户:
# 创建用户和组
sudo useradd -r -s /bin/false dockremap
# 配置 subuid 和 subgid
echo "dockremap:100000:65536" | sudo tee -a /etc/subuid
echo "dockremap:100000:65536" | sudo tee -a /etc/subgid
# 重启 Docker
sudo systemctl restart docker
效果:
- 容器内的 root (UID 0) 映射到主机的 dockremap 用户 (UID 100000)
- 容器内的进程在主机上以非特权用户身份运行
- 即使容器被攻破,也无法获得主机 root 权限
Seccomp 安全配置
什么是 Seccomp?
Seccomp(Secure Computing Mode)限制容器可以使用的系统调用,减少内核攻击面。
Docker 默认 Seccomp 配置
Docker 默认应用 Seccomp 配置文件,阻止危险的系统调用:
# 查看默认配置
docker info | grep -i seccomp
# 输出示例
Security Options:
seccomp
Profile: builtin
默认阻止的系统调用:
kexec_load、kexec_file_load:加载新内核init_module、finit_module:加载内核模块acct:启用/禁用进程记账swapon、swapoff:管理交换空间- 等约 44 个危险系统调用
自定义 Seccomp 配置
禁用 Seccomp(不推荐):
docker run --security-opt seccomp=unconfined alpine
使用自定义配置文件:
docker run --security-opt seccomp=/path/to/seccomp.json alpine
示例 Seccomp 配置:
{
"defaultAction": "SCMP_ACT_ALLOW",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["kexec_load"],
"action": "SCMP_ACT_ERRNO"
}
]
}
AppArmor 和 SELinux
AppArmor(Ubuntu/Debian)
AppArmor 通过配置文件限制程序的访问权限:
查看 AppArmor 状态:
# 检查 AppArmor 是否启用
docker info | grep -i apparmor
# 查看加载的配置文件
sudo aa-status
使用自定义 AppArmor 配置:
# 创建配置文件
sudo aa-genprof /path/to/binary
# 运行容器时指定配置
docker run --security-opt apparmor=your-profile myapp
SELinux(CentOS/RHEL)
SELinux 提供强制访问控制:
查看 SELinux 状态:
# 检查 SELinux 是否启用
getenforce
# 查看 Docker SELinux 配置
docker info | grep -i selinux
使用 SELinux 标签:
# 使用 svirt 标签
docker run --security-opt label=level:s0:c100,c200 myapp
# 禁用 SELinux 标签(不推荐)
docker run --security-opt label=disable myapp
守护进程安全
Docker Daemon 攻击面
Docker Daemon 以 root 权限运行,具有以下安全风险:
- TCP 端口暴露:远程 API 访问可能导致权限提升
- 镜像加载:恶意镜像可能包含漏洞
- 特权容器:可以访问主机资源
安全配置
限制 Docker Daemon 访问:
# 只允许特定用户组访问 Docker socket
sudo usermod -aG docker $USER
# 配置 daemon.json
{
"hosts": ["unix:///var/run/docker.sock"],
"tls": false
}
启用 TLS 加密:
# 生成证书
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# 配置 daemon.json
{
"hosts": ["tcp://0.0.0.0:2376", "unix:///var/run/docker.sock"],
"tls": true,
"tlscacert": "/etc/docker/ca.pem",
"tlscert": "/etc/docker/server-cert.pem",
"tlskey": "/etc/docker/server-key.pem",
"tlsverify": true
}
客户端配置:
# 配置环境变量
export DOCKER_HOST=tcp://your-host:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=/path/to/certs
Rootless 模式
Rootless 模式允许非 root 用户运行 Docker Daemon:
安装 Rootless Docker:
# 安装依赖
sudo apt-get install uidmap
# 安装 rootless docker
curl -fsSL https://get.docker.com/rootless | sh
# 配置环境变量
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$UID/docker.sock
# 启动 daemon
dockerd-rootless.sh
优点:
- Daemon 以非 root 用户运行
- 容器进程以非 root 用户运行
- 即使被攻破,也无法获得主机 root 权限
镜像安全
使用可信镜像
镜像来源分类:
| 类型 | 说明 | 安全级别 |
|---|---|---|
| Docker Official Images | Docker 官方维护 | 最高 |
| Verified Publisher | 验证发布者 | 高 |
| Docker-Sponsored OSS | Docker 赞助的开源项目 | 中高 |
| 社区镜像 | 社区贡献 | 需评估 |
验证镜像签名:
# 启用 Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# 拉取签名镜像
docker pull nginx:latest
# 输出签名验证信息
# Tagging nginx:latest@sha256:...
镜像扫描
使用 Docker Scout 扫描漏洞:
# 快速扫描
docker scout quickview nginx:latest
# 详细扫描
docker scout cves nginx:latest
# 查看修复建议
docker scout recommendations nginx:latest
使用 Trivy 扫描:
# 安装 Trivy
brew install trivy # macOS
# 扫描镜像
trivy image nginx:latest
# 输出到文件
trivy image --output results.json nginx:latest
最小化基础镜像
# 不推荐:使用完整镜像
FROM ubuntu:22.04
# 推荐:使用精简镜像
FROM ubuntu:22.04-minimal
# 更推荐:使用 Alpine
FROM alpine:3.18
# 生产环境:使用 distroless
FROM gcr.io/distroless/static-debian12
镜像大小对比:
| 基础镜像 | 大小 | 安全性 |
|---|---|---|
| ubuntu:22.04 | ~77MB | 中 |
| debian:slim | ~74MB | 中 |
| alpine:3.18 | ~5MB | 高 |
| distroless/static | ~2MB | 最高 |
网络安全
网络隔离
# 使用内部网络(无法访问外网)
docker network create --internal internal-net
# 运行容器
docker run -d --network internal-net myapp
# 分离前端和后端网络
docker network create frontend
docker network create --internal backend
docker run -d --name web --network frontend nginx
docker run -d --name api --network frontend --network backend myapi
docker run -d --name db --network backend mysql
端口安全
# 仅绑定到本地接口
docker run -d -p 127.0.0.1:8080:80 nginx
# 使用反向代理暴露服务
# nginx.conf
server {
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
限制网络能力
# 禁止修改网络配置
docker run --cap-drop=NET_ADMIN --cap-drop=NET_RAW myapp
# 使用只读文件系统防止修改
docker run --read-only --tmpfs /tmp myapp
敏感数据管理
Docker Secrets
Docker Secrets 用于安全地管理敏感数据:
在 Swarm 中使用:
# 创建 secret
echo "my_secret_password" | docker secret create db_password -
# 在服务中使用
docker service create \
--name app \
--secret db_password \
-e DB_PASSWORD_FILE=/run/secrets/db_password \
myapp
在 Docker Compose 中使用:
services:
app:
image: myapp
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
避免硬编码敏感信息
不推荐:
# 不要这样做
services:
app:
environment:
- DATABASE_PASSWORD=secret123 # 明文密码
推荐方式:
# 使用环境变量文件
services:
app:
env_file:
- .env # 添加到 .gitignore
# 或使用 Docker Secrets
services:
app:
secrets:
- db_password
安全配置清单
Dockerfile 安全清单
# ✅ 使用最小化基础镜像
FROM alpine:3.18
# ✅ 指定镜像版本和摘要
# FROM alpine:3.18@sha256:...
# ✅ 创建非 root 用户
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
# ✅ 只安装必要的包
RUN apk add --no-cache nodejs npm
# ✅ 设置适当的文件权限
COPY --chown=appuser:appgroup . /app
# ✅ 切换到非 root 用户
USER appuser
# ✅ 声明健康检查
HEALTHCHECK --interval=30s CMD wget -q --spider http://localhost:3000/health
# ✅ 不暴露不必要的端口
EXPOSE 3000
# ✅ 使用 exec 格式
CMD ["node", "server.js"]
运行时安全清单
docker run -d \
--name myapp \
--user 1001:1001 \ # 非 root 用户
--read-only \ # 只读文件系统
--cap-drop=ALL \ # 删除所有能力
--cap-add=NET_BIND_SERVICE \ # 只添加需要的能力
--security-opt=no-new-privileges \ # 禁止提权
--security-opt seccomp=unconfined \ # 使用 seccomp
--memory="512m" \ # 内存限制
--cpus="0.5" \ # CPU 限制
--pids-limit=100 \ # 进程数限制
--tmpfs /tmp \ # 临时目录
myapp:latest
Docker Compose 安全配置
services:
app:
image: myapp:latest
user: "1001:1001"
read_only: true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
安全监控
日志审计
# 启用 Docker 审计日志
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
# 查看容器日志
docker logs myapp
# 使用 journald 日志驱动
docker run -d --log-driver=journald myapp
运行时监控
# 查看容器资源使用
docker stats
# 查看容器进程
docker top myapp
# 查看容器事件
docker events --filter container=myapp
# 使用 Falco 监控异常行为
falco -r /etc/falco/falco_rules.yaml
安全检查命令汇总
# 检查 Docker 版本和配置
docker version
docker info
# 检查容器安全配置
docker inspect --format='{{.HostConfig.SecurityOpt}}' myapp
docker inspect --format='{{.HostConfig.CapAdd}}' myapp
docker inspect --format='{{.HostConfig.CapDrop}}' myapp
# 检查容器用户
docker inspect --format='{{.Config.User}}' myapp
# 检查镜像漏洞
docker scout quickview myapp:latest
# 检查容器进程
docker exec myapp ps aux
# 检查容器网络
docker network inspect bridge
参考资源
小结
本章我们学习了:
- 内核安全机制:命名空间、Cgroups、Capabilities
- 非 root 用户运行:Dockerfile 和运行时配置
- Seccomp 和 AppArmor/SELinux:系统调用过滤和访问控制
- 守护进程安全:TLS 加密、Rootless 模式
- 镜像安全:可信镜像、漏洞扫描
- 网络安全:网络隔离、端口安全
- 敏感数据管理:Docker Secrets
练习
- 创建一个使用非 root 用户的 Dockerfile
- 配置容器只保留必要的 Capabilities
- 使用 Docker Scout 扫描镜像漏洞
- 配置 Docker Content Trust 验证镜像签名
- 设计一个包含前后端分离的安全网络架构