Docker安全加固:VPS上的Rootless模式、Seccomp和AppArmor
VPS上Docker的七层加固措施。每节说明威胁来源,给出CLI和Compose语法的修复方案,并验证其效果。
Docker的默认配置以安全换便利。容器以主机root身份运行。全部14个Linux capabilities(内核权限)保持激活。Seccomp仅拦截300多个syscall中的约44个。容器间流量不受限制。
在VPS上,这比在本地开发机上更重要。你与其他租户共享物理主机。一次容器逃逸意味着攻击者以root身份落在面向hypervisor的内核上。你添加的每一层加固都能缩小爆炸半径。
本教程涵盖七项加固措施。每节说明它防范的威胁,展示实现方法(docker run参数和Compose语法),并包含验证步骤。我们在运行Docker Engine 29.x的Ubuntu 24.04上测试了所有命令。
前提条件: 一台运行Debian 12或Ubuntu 24.04且已安装Docker Engine的VPS。以非root sudo用户进行SSH访问。如果你还没有加固主机本身,请先阅读我们的Linux VPS安全指南。关于Docker防火墙问题,请参阅Docker UFW防火墙修复。
本文是系列的一部分。
如何在Ubuntu 24.04或Debian 12上设置rootless Docker?
Rootless Docker在普通用户账户下运行daemon和所有容器,而非root。如果攻击者逃出容器,他们会以非特权用户身份落在主机上,没有root权限。在本指南的所有措施中,这一项影响最大。
安装rootless Docker
安装所需软件包。uidmap包提供newuidmap和newgidmap,用于处理subordinate UID/GID映射:
sudo apt-get update && sudo apt-get install -y uidmap docker-ce-rootless-extras
验证你的用户至少有65,536个subordinate UID和GID:
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid
你应该看到类似deploy:100000:65536的输出。如果条目缺失,添加它们:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
停止系统级Docker daemon。rootless模式不需要它:
sudo systemctl disable --now docker.service docker.socket
以普通用户身份(不是root)运行rootless设置工具:
dockerd-rootless-setuptool.sh install
脚本会输出你需要设置的环境变量。将它们添加到你的shell配置文件:
echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc
启用linger,使rootless daemon在开机时启动,而不仅在你登录时:
sudo loginctl enable-linger $(whoami)
验证rootless模式
docker context use rootless
docker run --rm hello-world
检查daemon进程以你的用户身份运行,而非root:
ps aux | grep dockerd
输出应显示你的用户名,而非root。同时确认Docker info报告rootless:
docker info --format '{{.SecurityOptions}}'
你应该在列表中看到rootless。
rootless Docker的局限性
Rootless模式有实际限制。了解它们可以避免数小时的调试。
| 限制 | 原因 | 解决方法 |
|---|---|---|
| 无法绑定1024以下端口 | 非root用户无法绑定特权端口 | 设置sysctl net.ipv4.ip_unprivileged_port_start=0或在主机上使用反向代理 |
| Bind mount权限错误 | 主机上root拥有的文件对重映射的UID不可访问 | 将所有权更改为你的用户,或使用named volumes |
| 较慢的overlay文件系统 | Rootless使用fuse-overlayfs而非原生overlay2 |
接受开销(I/O密集型工作负载约5-15%),或使用带--privileged的原生overlay2(违背初衷) |
不支持--net=host |
Rootless网络使用slirp4netns或pasta,而非主机网络栈 | 使用端口映射(-p)。要获得更好的性能,安装pasta作为网络驱动 |
容器内ping失败 |
CAP_NET_RAW受限 |
安装slirp4netns >= 0.4.0或使用pasta |
网络性能说明: 默认情况下,rootless Docker使用slirp4netns进行网络处理,会增加NAT开销。pasta驱动将主机网络配置复制到容器namespace中,无需NAT,吞吐量更好。在Debian 12和Ubuntu 24.04上,使用以下命令安装:
sudo apt-get install -y passt
如果已安装pasta,Docker会自动使用它。
何时应该使用user namespace remapping而非rootless Docker?
User namespace remapping(userns-remap)将容器内的UID 0映射到主机上的非特权UID。与rootless Docker不同,daemon本身仍以root运行。这意味着你保留完整的Docker功能(特权端口、主机网络、原生overlay2),同时仍防止容器root等于主机root。
当rootless模式破坏你的工作负载但你仍需要UID隔离时,选择userns-remap。当你能接受其限制时,选择rootless。
| 特性 | Rootless Docker | userns-remap | 标准Docker |
|---|---|---|---|
| Daemon运行身份 | 用户 | Root | Root |
| 容器root = 主机root | 否 | 否 | 是 |
| 特权端口 | 需要变通方法 | 可用 | 可用 |
--net=host |
否 | 是 | 是 |
| 存储驱动 | fuse-overlayfs | overlay2 | overlay2 |
| 设置复杂度 | 中等 | 低 | 无 |
配置userns-remap
创建dockremap用户或使用default快捷方式自动创建:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"userns-remap": "default"
}
EOF
sudo systemctl restart docker
Docker创建dockremap用户并将subordinate UID/GID范围添加到/etc/subuid和/etc/subgid。
验证它是否正常工作:
sudo ls -ld /var/lib/docker/
你应该看到一个以重映射UID范围命名的新子目录,例如/var/lib/docker/100000.100000/。具体数字取决于/etc/subuid中分配给dockremap用户的subordinate UID范围。
运行一个容器并检查主机上的进程UID:
docker run -d --name test-userns nginx:alpine
ps aux | grep nginx
nginx进程应显示一个高UID(与/etc/subuid中dockremap的第一个数字匹配),而非0。
清理:
docker rm -f test-userns
Volume所有权注意事项: 主机root(UID 0)拥有的bind-mounted文件在容器内显示为nobody,因为UID 0映射到了不同的范围。使用named volumes或将文件chown到重映射的UID。
如何从Docker容器中削减Linux capabilities?
Docker默认授予容器14个Linux capabilities。每个capability是一个内核权限,攻击者在容器逃逸或容器被入侵后可以滥用它。削减所有capabilities并仅添加回应用程序实际需要的,可以缩小攻击面。
Docker默认capabilities
| Capability | 允许的操作 | 保留还是削减? |
|---|---|---|
CAP_CHOWN |
更改文件所有权 | 非必需则削减 |
CAP_DAC_OVERRIDE |
绕过文件读写权限检查 | 非必需则削减 |
CAP_FOWNER |
绕过文件所有者的权限检查 | 非必需则削减 |
CAP_FSETID |
设置setuid/setgid位 | 削减 |
CAP_KILL |
向任意进程发送信号 | 非必需则削减 |
CAP_SETGID |
更改进程GID | 大多数应用保留 |
CAP_SETUID |
更改进程UID | 大多数应用保留 |
CAP_SETPCAP |
修改进程capabilities | 削减 |
CAP_NET_BIND_SERVICE |
绑定1024以下端口 | 如绑定80/443端口则保留 |
CAP_NET_RAW |
使用原始套接字(构造数据包) | 非必需ping/traceroute则削减 |
CAP_SYS_CHROOT |
使用chroot | 削减 |
CAP_MKNOD |
创建设备文件 | 削减 |
CAP_AUDIT_WRITE |
写入内核审计日志 | 非必需则削减 |
CAP_SETFCAP |
设置文件capabilities | 削减 |
全部削减,按需添加
CLI语法:
docker run -d \
--cap-drop ALL \
--cap-add CHOWN \
--cap-add NET_BIND_SERVICE \
--cap-add SETUID \
--cap-add SETGID \
--name hardened-nginx \
nginx:alpine
Nginx需要CHOWN,因为其entrypoint脚本在启动时更改缓存目录的所有权。没有它,容器会立即以chown: Operation not permitted错误退出。
Compose语法:
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
验证capabilities已被削减
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep Cap'
将CapEff(有效capabilities)位掩码进行比较。全部削减并添加回四个后,该值远低于默认的00000000a80425fb。
要获得可读输出,在主机上安装capsh并解码十六进制值:
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep CapEff' | awk '{print $2}' | xargs -I{} capsh --decode=0x{}
你应该在输出中仅看到cap_chown,cap_setgid,cap_setuid,cap_net_bind_service。
清理:
docker rm -f hardened-nginx
no-new-privileges标志防止什么?
no-new-privileges标志阻止容器内的进程通过setuid或setgid二进制文件获取额外权限。没有此标志,被入侵的进程可以执行setuid二进制文件(如su或sudo)并提升为root。设置此标志后,内核拒绝权限提升。
按容器应用
CLI:
docker run -d --security-opt no-new-privileges:true --name no-priv-test nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- no-new-privileges:true
作为daemon默认设置应用
将其添加到daemon.json,使每个容器自动获得此标志:
{
"no-new-privileges": true
}
编辑后重启Docker:
sudo systemctl restart docker
验证它是否生效
docker exec no-priv-test grep NoNewPrivs /proc/1/status
预期输出:
NoNewPrivs: 1
值为1表示无法获取新权限。值为0表示标志未设置。
清理:
docker rm -f no-priv-test
如何为Docker创建自定义seccomp配置文件?
Docker的默认seccomp配置文件在300多个syscall中仅拦截约44个。自定义配置文件让你将容器限制为仅应用程序实际使用的syscall。如果攻击者入侵了容器,他们无法通过被拦截的syscall利用内核漏洞。
发现应用程序需要哪些syscall
在运行的容器上使用strace捕获正常操作期间的syscall:
docker run -d --security-opt seccomp=unconfined --name trace-target nginx:alpine
# Install strace on the host
sudo apt-get install -y strace
# Get the container's PID
PID=$(docker inspect --format '{{.State.Pid}}' trace-target)
# Trace syscalls for 30 seconds during normal operation
sudo strace -f -p "$PID" -o /tmp/nginx-syscalls.log -e trace=all &
STRACE_PID=$!
sleep 30
# Send some test traffic to exercise the application
curl -s http://localhost:80 > /dev/null 2>&1 || true
kill "$STRACE_PID" 2>/dev/null
wait "$STRACE_PID" 2>/dev/null
提取唯一的syscall名称:
grep -oP '^\[pid \d+\] \K\w+|^\w+' /tmp/nginx-syscalls.log | sort -u
这给你应用程序所需的最小syscall集合。
构建自定义配置文件
创建一个JSON文件。defaultAction是SCMP_ACT_ERRNO(拒绝所有未明确允许的操作)。syscall列表必须包括应用程序需要的和容器运行时(runc)在初始化期间需要的。以下配置文件已在Docker Engine 29.x上的nginx:alpine中测试通过:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4",
"access",
"arch_prctl",
"bind",
"brk",
"capget",
"capset",
"chdir",
"chown",
"clone",
"clone3",
"close",
"close_range",
"connect",
"copy_file_range",
"dup",
"dup2",
"dup3",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"epoll_pwait2",
"epoll_wait",
"eventfd2",
"execve",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fchmod",
"fchmodat",
"fchown",
"fchownat",
"fcntl",
"fork",
"fstat",
"fstatfs",
"futex",
"getcwd",
"getdents",
"getdents64",
"getegid",
"geteuid",
"getgid",
"getpgrp",
"getpid",
"getppid",
"getrandom",
"getrlimit",
"getsockname",
"getsockopt",
"gettid",
"getuid",
"io_destroy",
"io_getevents",
"io_setup",
"io_submit",
"ioctl",
"kill",
"listen",
"lseek",
"lstat",
"madvise",
"memfd_create",
"mkdir",
"mkdirat",
"mmap",
"mount",
"mprotect",
"mremap",
"munmap",
"nanosleep",
"newfstatat",
"open",
"openat",
"pipe",
"pipe2",
"pivot_root",
"poll",
"ppoll",
"prctl",
"pread64",
"prlimit64",
"pwrite64",
"read",
"readlink",
"readlinkat",
"recvfrom",
"recvmsg",
"rename",
"renameat",
"rseq",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_getaffinity",
"sched_yield",
"seccomp",
"sendfile",
"sendmsg",
"sendto",
"set_robust_list",
"set_tid_address",
"setgid",
"setgroups",
"sethostname",
"setitimer",
"setsockopt",
"setuid",
"sigaltstack",
"socket",
"socketpair",
"stat",
"statfs",
"statx",
"symlink",
"symlinkat",
"sysinfo",
"tgkill",
"umask",
"umount2",
"uname",
"unlink",
"unlinkat",
"unshare",
"utimensat",
"wait4",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
为什么这么多syscall? 列表包含三层所需的syscall:容器运行时(runc)用于namespace设置的(clone3、mount、pivot_root、unshare、seccomp、statx),Alpine shell和entrypoint脚本的(fork、open、pipe、wait4),以及nginx本身的(accept4、bind、listen、io_setup)。基于glibc的镜像需要更少的shell相关syscall,但更多libc内部的。
将此文件保存为/etc/docker/seccomp-nginx.json。
应用自定义配置文件
CLI:
docker run -d \
--security-opt seccomp=/etc/docker/seccomp-nginx.json \
--name seccomp-test \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- seccomp=/etc/docker/seccomp-nginx.json
验证配置文件是否生效
docker inspect --format '{{.HostConfig.SecurityOpt}}' seccomp-test
输出显示应用于容器的完整seccomp配置文件JSON。Docker在创建容器时读取文件并嵌入配置文件内容。
测试受限操作是否失败:
docker exec seccomp-test unshare --mount /bin/sh -c 'echo escaped'
这应该失败并显示"Operation not permitted"。容器缺少CAP_SYS_ADMIN(默认未授予),seccomp配置文件提供了第二层防御,仅允许正常操作所需的syscall。
清理:
docker rm -f seccomp-test
rm -f /tmp/nginx-syscalls.log
生产提示: 从Docker的默认配置文件开始并删除syscall,而非从零开始构建。strace方法能给你最紧凑的配置文件,但需要充分测试。在strace捕获期间,要覆盖应用程序使用的每条代码路径:启动、正常请求、错误处理、优雅关闭(docker stop)和日志轮转。
迭代方法: 如果从strace构建感觉风险较大,使用这个更安全的工作流:
- 复制Docker的默认seccomp配置文件作为起点。
- 使用复制的配置文件运行容器。其行为与默认配置完全相同。
- 每次删除一组syscall(例如所有
key*syscall、所有swap*syscall)。 - 每次删除后测试容器。如果出错,将最后删除的syscall添加回来。
- 重复直到你削减了应用程序不需要的所有内容。
这比strace方法慢,但对生产容器更安全,因为遗漏一个syscall可能导致间歇性故障。
如何为Docker容器编写AppArmor配置文件?
AppArmor限制容器进程可以访问的文件、网络资源和capabilities。Docker默认自动应用docker-default配置文件。自定义配置文件让你进一步将容器限制为仅所需的文件系统路径和网络操作。
检查AppArmor是否已激活
sudo aa-status
你应该在已加载配置文件列表中看到docker-default。如果AppArmor未安装:
sudo apt-get install -y apparmor apparmor-utils
编写自定义配置文件
创建/etc/apparmor.d/containers/docker-nginx:
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/nameservice>
# Capabilities needed by nginx
capability chown,
capability setuid,
capability setgid,
capability net_bind_service,
capability dac_override,
# Network access
network inet tcp,
network inet udp,
network inet6 tcp,
network inet6 udp,
# Shell and entrypoint scripts
/bin/** rix,
/usr/bin/** rix,
/usr/sbin/** rix,
/lib/** mr,
/usr/lib/** mr,
/docker-entrypoint.sh rix,
/docker-entrypoint.d/ r,
/docker-entrypoint.d/** rix,
/dev/null rw,
/dev/stdout rw,
/dev/stderr rw,
# Nginx binary
/usr/sbin/nginx ix,
# Config files (read only)
/etc/ r,
/etc/nginx/ r,
/etc/nginx/** r,
# Web root (read only)
/usr/share/nginx/html/** r,
# Temp and cache directories
/var/cache/nginx/ rw,
/var/cache/nginx/** rw,
/var/log/nginx/ rw,
/var/log/nginx/** rw,
/run/ rw,
/run/** rw,
/tmp/ rw,
/tmp/** rw,
# Proc filesystem (needed for nginx worker management)
/proc/** r,
# Deny sensitive files
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem r,
deny /sys/** w,
}
配置文件需要capability声明,因为AppArmor独立于Docker的--cap-add标志来控制capability的使用。entrypoint脚本路径(/docker-entrypoint.sh、/docker-entrypoint.d/)和shell二进制文件(/bin/**)必须明确允许,否则容器无法启动。rix权限表示读取、继承执行上下文并允许执行。
加载并应用配置文件
sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
验证是否已加载:
sudo aa-status | grep docker-nginx
使用自定义配置文件运行容器:
CLI:
docker run -d \
--security-opt apparmor=docker-nginx \
--name apparmor-test \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- apparmor=docker-nginx
验证AppArmor强制执行
docker exec apparmor-test cat /etc/shadow
这应该返回"Permission denied",因为配置文件明确拒绝读取/etc/shadow。
检查容器的AppArmor状态:
docker inspect --format '{{.AppArmorProfile}}' apparmor-test
预期输出:docker-nginx。
清理:
docker rm -f apparmor-test
如何使用只读文件系统运行Docker容器?
只读根文件系统阻止攻击者在被入侵的容器中写入恶意软件、后门或修改过的二进制文件。容器仍然可以写入明确挂载的tmpfs卷来存放临时文件和运行时数据。
CLI:
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /run:rw,noexec,nosuid,size=16m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=128m \
-p 8080:80 \
--name readonly-nginx \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
注意tmpfs挂载上的noexec标志。这阻止从临时目录执行二进制文件,这是攻击者获得写入权限后常用的技术。
验证文件系统是否为只读
docker exec readonly-nginx touch /testfile
预期输出:
touch: /testfile: Read-only file system
确认tmpfs挂载正常工作:
docker exec readonly-nginx touch /tmp/testfile && echo "tmpfs works"
验证容器正在提供流量:
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080
预期:200。
清理:
docker rm -f readonly-nginx
加固后的Docker daemon.json应该是什么样的?
加固后的daemon.json将安全默认值应用于主机上的每个容器。单个容器仍可覆盖某些设置,但daemon配置设定了基线。
创建或编辑/etc/docker/daemon.json:
{
"no-new-privileges": true,
"icc": false,
"live-restore": true,
"userland-proxy": false,
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}"
},
"default-ulimits": {
"nproc": {
"Name": "nproc",
"Hard": 512,
"Soft": 256
},
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 32768
}
},
"storage-driver": "overlay2"
}
每项设置的作用:
no-new-privileges:默认阻止所有容器中的setuid/setgid提权。icc: false:禁用默认bridge网络上的容器间通信。容器只能通过明确发布的端口或用户自定义网络互相访问。这限制了一个容器被入侵后的横向移动。live-restore:在daemon重启期间保持容器运行。防止Docker升级期间的停机。userland-proxy: false:使用iptables进行端口映射,而非userland proxy进程。性能更好,打开的文件描述符更少。log-driver: journald:将容器日志发送到系统journal,集中管理和轮转。default-ulimits:限制每个容器的进程数和打开文件数。防止fork bomb和文件描述符耗尽。
应用更改:
sudo systemctl restart docker
验证daemon是否采用了配置:
docker info --format '{{.SecurityOptions}}'
你应该在安全选项列表中看到no-new-privileges。
检查ICC是否已禁用:
docker network inspect bridge --format '{{index .Options "com.docker.network.bridge.enable_icc"}}'
预期输出:false。
关于userns-remap的说明: 如果你选择了user namespace remapping而非rootless模式,请将"userns-remap": "default"添加到此配置中。不要将userns-remap与rootless Docker组合使用。
版本隐藏: 编辑daemon.json时,也考虑隐藏Docker API版本信息。Docker默认不像Nginx那样暴露版本头,但如果你在TCP socket上运行Docker API(除非绝对必要,否则不要这样做),请使用TLS客户端证书保护它。暴露的Docker API等同于root shell访问。
审计你的设置: 运行Docker Bench for Security来根据CIS Docker Benchmark审计你的配置。克隆仓库并直接运行脚本,因为容器镜像附带的过时Docker客户端与Docker Engine 29.x不兼容:
git clone https://github.com/docker/docker-bench-security.git /tmp/docker-bench
cd /tmp/docker-bench && sudo bash docker-bench-security.sh
检查输出中的WARN条目。上面加固后的daemon.json解决了大部分问题。标记为INFO的项目是建议,不是故障。
在Docker Compose中组合多层加固
生产Compose文件应叠加多项加固措施。以下是一个Nginx容器应用全部七层加固的示例:
services:
web:
image: nginx:alpine
read_only: true
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
security_opt:
- no-new-privileges:true
- seccomp=/etc/docker/seccomp-nginx.json
- apparmor=docker-nginx
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
ports:
- "80:80"
pids_limit: 100
deploy:
resources:
limits:
memory: 256M
cpus: '1.0'
注意pids_limit防止fork bomb,deploy.resources.limits限制内存和CPU。这些不是security-opt标志,但它们防止来自容器内部的拒绝服务攻击。更多关于资源限制的内容,请参阅。
验证运行容器上的所有安全选项是否生效:
docker compose up -d
docker inspect web --format '{{json .HostConfig.SecurityOpt}}' | python3 -m json.tool
你应该看到no-new-privileges、你的seccomp配置文件路径和AppArmor配置文件名称被列出。
生产加固清单
| 措施 | CLI标志 | Compose键 | daemon.json | 防范的威胁 |
|---|---|---|---|---|
| Rootless Docker | N/A(daemon级别) | N/A | N/A(独立daemon) | 容器逃逸获取主机root |
| userns-remap | N/A | N/A | "userns-remap": "default" |
容器root = 主机root |
| 削减capabilities | --cap-drop ALL --cap-add X |
cap_drop: [ALL] |
N/A | 内核攻击面 |
| no-new-privileges | --security-opt no-new-privileges:true |
security_opt: [no-new-privileges:true] |
"no-new-privileges": true |
Setuid/setgid提权 |
| 自定义seccomp | --security-opt seccomp=profile.json |
security_opt: [seccomp:path] |
N/A | 通过被拦截syscall的内核漏洞利用 |
| AppArmor配置文件 | --security-opt apparmor=name |
security_opt: [apparmor:name] |
N/A | 超出应用需求的文件/网络访问 |
| 只读rootfs | --read-only |
read_only: true |
N/A | 持久化恶意软件、二进制文件篡改 |
| 禁用ICC | N/A | N/A | "icc": false |
容器间横向移动 |
| 限制进程数 | --pids-limit 100 |
pids_limit: 100 |
"default-pids-limit": 100 |
Fork bomb |
| 禁用userland proxy | N/A | N/A | "userland-proxy": false |
资源浪费,缩小攻击面 |
故障排查
添加seccomp配置文件后容器无法启动:
你的配置文件缺少应用程序需要的syscall。临时使用--security-opt seccomp=unconfined运行,strace进程,并将缺少的syscall添加到配置文件中。
检查内核审计日志中被拦截的syscall:
sudo journalctl -k | grep -i seccomp
AppArmor阻止合法操作: 将配置文件设为complain模式,记录拒绝但不执行:
sudo aa-complain /etc/apparmor.d/containers/docker-nginx
查看日志:
sudo journalctl -k | grep apparmor | tail -20
添加所需权限后,切换回enforce模式:
sudo aa-enforce /etc/apparmor.d/containers/docker-nginx
Rootless Docker无法拉取镜像:
检查DOCKER_HOST是否设置正确:
echo $DOCKER_HOST
它应该指向/run/user/<UID>/docker.sock。同时验证rootless daemon是否在运行:
systemctl --user status docker
使用userns-remap时bind mount权限被拒绝: 主机上root(UID 0)拥有的文件不可访问,因为容器的UID 0映射到了高位主机UID。通过更改所有权修复:
# Find the remapped UID
grep dockremap /etc/subuid
# Then chown to that UID
sudo chown -R <remapped-uid>:<remapped-uid> /path/to/bind/mount
将<remapped-uid>替换为/etc/subuid中dockremap用户的第一个数字(通常是100000)。
容器日志: 对于任何Docker相关问题,从容器日志和Docker daemon journal开始:
docker logs <container-name>
sudo journalctl -u docker -f
对于rootless Docker,检查用户级journal:
journalctl --user -u docker -f
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。