Docker 容器安全加固实战:个人站长必做的 9 项安全配置

很多个人站长现在都开始用 Docker 部署网站了,装个 Docker、写个 Dockerfile、docker run 一敲,服务就跑起来了,确实方便。但方便归方便,大多数人直接把容器当成"用完即弃的虚拟机",镜像里跑着 root 用户、端口全部暴露、内存 CPU 不设任何上限,甚至把数据库密码直接写进 Dockerfile。平时没事还好,一旦被扫描器盯上,轻则容器被植入挖矿程序,重则利用挂载漏洞直接拿下整个服务器。

这篇文章不教你怎么部署,只讲安全。我从自己踩过的坑出发,整理出个人站长最容易忽略、但性价比最高的 8 项容器安全配置,每一项都给出可以直接复制的命令和配置,最后再介绍一个一键体检工具,帮你检查自己的服务器还有哪些漏洞。

一、不要让容器以 root 用户运行

Docker 官方镜像默认以 root 身份运行容器内进程。虽然容器里的 root 和宿主机的 root 有隔离,但这种隔离并不是绝对的——一旦 Docker 或内核出现逃逸漏洞,容器内的 root 就等于宿主机的 root。正确的做法是:在 Dockerfile 里创建普通用户,用普通用户跑应用。

# Dockerfile 片段
RUN useradd -r -u 1001 appuser     && mkdir -p /app/data     && chown -R appuser:appuser /app
USER appuser

如果镜像已经构建好了,也可以用运行时参数指定用户,不用重新构建:

docker run --user 1001:1001 -d --name myapp myapp:latest

验证容器内是不是 root 很简单:docker exec myapp whoami,输出不是 root 就对了。注意,如果你挂载了宿主机目录,一定要保证容器用户的 UID 和宿主机目录属主的 UID 一致,否则会出现权限不足的报错,这也是新手最常见的坑。

二、只读根文件系统,让攻击者无处写文件

容器被攻破之后,攻击者第一件事往往是往容器里写后门脚本、下载挖矿程序。如果我们把容器的根文件系统设成只读,这一步就直接被堵死了。用 --read-only 参数启动,容器里除了挂载的卷以外都不能写:

docker run -d --name myapp   --read-only   --tmpfs /tmp   --tmpfs /var/run   myapp:latest

--tmpfs 是把某些必须可写的目录(比如临时目录)挂成内存盘,写入的内容重启即失。如果应用还有其它需要写的地方,单独挂一个数据卷:-v /data/app:/app/data。很多应用在只读模式下第一次启动会报错,这时候用 docker logs 看它到底想写哪个目录,逐个用 tmpfs 或卷解决,耐心一点,这个配置非常值得做。

三、限制资源:防止一个容器拖垮整台服务器

不限制资源的容器就是一个定时炸弹。我见过最典型的事故:某个容器里跑着死循环,内存一路涨,直接把整台服务器的 OOM Killer 触发,MySQL 被杀,网站全部打不开,排查了半天才发现是 Docker 的锅。给容器加上内存、CPU 和进程数限制:

docker run -d --name myapp   --memory 512m   --memory-swap 512m   --cpus 0.5   --pids-limit 128   --restart on-failure   myapp:latest

参数含义:--memory 限制内存上限;--memory-swap 要和内存一样大,否则容器会偷偷用 swap 撑过限制;--cpus 0.5 表示最多用半个 CPU 核心;--pids-limit 限制进程数,防止 fork 炸弹。多容器场景还可以用 docker update 随时调整:docker update --memory 1g myapp

四、裁剪 Linux capabilities:去掉用不上的权限

Linux 的 capabilities 机制把 root 权限拆成了几十个小权限,比如绑定低端口、加载内核模块、修改系统时间等等。Docker 默认给容器带了一长串 capabilities,其中很多(比如 CAP_SYS_ADMIN、CAP_NET_ADMIN)普通应用根本用不到,却成了攻击者提权的阶梯。安全做法是全部丢弃,再按需放行:

docker run -d --name myapp   --cap-drop=ALL   --cap-add=NET_BIND_SERVICE   myapp:latest

--cap-drop=ALL 丢掉所有权限,然后只加回应用真正需要的。Web 应用一般需要绑定 80/443 端口,所以加 NET_BIND_SERVICE;如果应用不需要监听低端口,连这个都可以不加。用 docker inspect myapp 查看 CapAdd/CapDrop 字段可以确认配置是否生效。这一步对 Nginx、PHP 这类 Web 服务几乎零影响,收益却很大。

五、开启 seccomp 和 AppArmor 双重保险

除了 capabilities,Linux 还有 seccomp(安全计算模式)机制,可以限制容器内进程能发起的系统调用。Docker 默认自带一个 seccomp 配置,已经挡住了大部分危险调用,但我们还可以更严格地指定自己的配置:

docker run -d --name myapp   --security-opt seccomp=/etc/docker/seccomp-profile.json   --security-opt apparmor=docker-default   myapp:latest

seccomp 的 profile 文件是一个 JSON,定义了允许/拒绝哪些系统调用,网上有现成的精简模板(比如 Docker 官方示例里的 default.json 去掉一些高风险的调用)。AppArmor 在 Debian/Ubuntu 上默认启用,docker-default 是 Docker 自带的配置。如果你用的是 CentOS,内核里通常是 SELinux,可以加 --security-opt label=disable 之外的建议先查清楚自己的环境,不要盲目套用。

六、镜像安全:从源头减少攻击面

容器安全的根基在镜像。几个实用的习惯:

  • 尽量用官方镜像,并固定 tag,比如 nginx:1.25.3-alpine,不要用 latest——latest 会变,你无法确定线上跑的到底是哪个版本。
  • 优先选 alpine 等精简基础镜像,体积小,里面的组件少,攻击面自然小。
  • 构建后扫描漏洞:装一个 trivy,trivy image myapp:latest 几秒钟就能扫出镜像里各组件的中高危 CVE,比手动看安全公告省事太多。
  • 写 .dockerignore,把 .git、.env、备份文件排除在构建上下文之外,防止敏感文件被打进镜像。
  • 别往镜像里装调试工具,尤其是 netcat、telnet 这类网络工具——它们平时用不上,却可能成为攻击者反弹 shell 的跳板。调试完就删,或者干脆用 exec 临时进容器装。
  • 使用多阶段构建,构建工具和运行环境分离,最终镜像里只保留运行所需的最小文件集。

七、敏感信息永远不要写进镜像

这是最要命的一条。很多人图省事,把数据库密码、API Key 直接写进 Dockerfile 的 ENV 指令里,或者把带密码的配置文件 COPY 进镜像。问题在于:Docker 镜像是分层存储的,即使你后面删掉了那个文件、重新构建,docker history 依然能看到历史层里的内容,等于把密码直接发给了每一个能拉取镜像的人。正确做法:

# 运行时通过环境变量或文件注入,不进镜像
docker run -d --name myapp   --env-file /etc/myapp/env.prod   -v /etc/myapp/config.ini:/app/config.ini:ro   myapp:latest

--env-file 从宿主机文件读取环境变量,:ro 让挂载的配置文件只读。这样密码只存在于宿主机上,镜像本身是干净的,可以放心推到仓库。

八、网络隔离:只暴露必要端口

默认情况下 Docker 的 bridge 网络已经做了容器间隔离,但很多人的问题出在端口映射上。常见错误是 -p 3306:3306 把数据库端口直接暴露到公网,等于把 MySQL 大门敞开给全世界的扫描器。正确做法:

# 只监听本机,由 Nginx 反向代理转发
docker run -d --name myapp -p 127.0.0.1:8080:80 myapp:latest

# 容器间通信用自定义网络,不暴露端口
docker network create app-net
docker run -d --name mysql --network app-net mysql:8
docker run -d --name myapp --network app-net -p 127.0.0.1:8080:80 myapp:latest

数据库这类只给内部用的容器,干脆不要做端口映射,让其它容器通过自定义网络里的服务名(比如 mysql:3306)直接访问。Web 容器绑定 127.0.0.1 只在本机监听,外面用 Nginx 反代,再配合防火墙只放行 80/443,端口暴露面就非常小了。另外,除非你真的需要,别用 --network host,它会让容器共享宿主机的网络栈,完全失去网络层面的隔离。

九、千万别把 Docker Socket 挂进容器

这是很多人会踩的致命坑,必须单独拿出来说:有些容器化监控、管理类工具(比如 Portainer、某些 CI 工具)为了"方便管理",会要求把宿主机的 /var/run/docker.sock 挂载进容器。这等于把宿主机的 Docker 控制权完全交给了容器——只要容器被攻破,攻击者通过这个 socket 就能创建任意容器,挂载宿主机的根目录进去,直接拿到整台服务器的 root 权限,比任何漏洞利用都直接。判断方法很简单,看启动命令或 docker inspect 里有没有 -v /var/run/docker.sock:/var/run/docker.sock 这样的挂载,有的话务必去掉。真的需要远程管理 Docker,优先用配置了 TLS 证书的远程 API,或者至少用带访问控制的工具,而不是把 socket 裸挂进去。

十、一键体检:docker-bench-security

配置做了这么多,怎么知道自己还有哪些没做到位?Docker 官方有一个安全审计工具 docker-bench-security,基于 CIS 基准自动检查几十个项目,一键运行:

docker run -it --net host --pid host --cap-add audit_control   -v /var/lib:/var/lib -v /var/run/docker.sock:/var/run/docker.sock   -v /usr/lib/systemd/system:/usr/lib/systemd/system   -v /etc:/etc --label docker_bench_security   docker/docker-bench-security

输出会逐项给出 PASS 或 WARN,并附上修复建议。第一次跑出来几十条 WARN 是正常的,不用焦虑,优先修那些标了 [HIGH] 风险的项,比如权限设置、未限制的资源、暴露的 socket 等。建议每季度跑一次,服务器配置变更后也跑一次。

总结

容器安全没有银弹,但上面这 9 项配置加起来花不了半小时,却能把 90% 的常见攻击路径堵死:root 用户、可写文件系统、无资源上限、多余权限、漏洞镜像、明文密码、端口裸奔、socket 越权。对个人站长来说,安全的意义不是防住国家队,而是让脚本小子和扫描器知难而退。你的服务器上跑着几年的心血,花半小时给它上个保险,非常划算。如果你还不知道自己的服务器安不安全,先跑一遍 docker-bench-security,从输出里挑 HIGH 风险的项开始修吧。

Last modification:August 10th, 2026 at 08:33 am

Leave a Comment