跳到主要内容

Docker 安全最佳实践

容器安全是生产环境部署的关键考量。本章将详细介绍 Docker 的安全机制和最佳实践,帮助你构建安全的容器化应用。

安全概述

Docker 安全涉及四个主要领域:

  1. 内核安全:Linux 内核的命名空间(Namespaces)和控制组(Cgroups)
  2. 守护进程安全:Docker Daemon 的攻击面
  3. 容器配置安全:容器的默认配置和自定义配置
  4. 内核加固特性: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_loadkexec_file_load:加载新内核
  • init_modulefinit_module:加载内核模块
  • acct:启用/禁用进程记账
  • swaponswapoff:管理交换空间
  • 等约 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 权限运行,具有以下安全风险:

  1. TCP 端口暴露:远程 API 访问可能导致权限提升
  2. 镜像加载:恶意镜像可能包含漏洞
  3. 特权容器:可以访问主机资源

安全配置

限制 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 ImagesDocker 官方维护最高
Verified Publisher验证发布者
Docker-Sponsored OSSDocker 赞助的开源项目中高
社区镜像社区贡献需评估

验证镜像签名

# 启用 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

参考资源

小结

本章我们学习了:

  1. 内核安全机制:命名空间、Cgroups、Capabilities
  2. 非 root 用户运行:Dockerfile 和运行时配置
  3. Seccomp 和 AppArmor/SELinux:系统调用过滤和访问控制
  4. 守护进程安全:TLS 加密、Rootless 模式
  5. 镜像安全:可信镜像、漏洞扫描
  6. 网络安全:网络隔离、端口安全
  7. 敏感数据管理:Docker Secrets

练习

  1. 创建一个使用非 root 用户的 Dockerfile
  2. 配置容器只保留必要的 Capabilities
  3. 使用 Docker Scout 扫描镜像漏洞
  4. 配置 Docker Content Trust 验证镜像签名
  5. 设计一个包含前后端分离的安全网络架构