ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker 容器安全加固实战:非 root、只读根文件系统、drop capabilities 与最小攻击面

Docker 容器安全加固实战:非 root、只读根文件系统、drop capabilities 与最小攻击面 Docker 容器安全加固实战:非 root、只读根文件系统、drop capabilities 与最小攻击面大部分 Dockerfile 默认以 root 跑进程——一旦应用被 RCE(远程代码执行),攻击者拿到的是容器里的 root,再叠加一个容器逃逸漏洞,就能威胁到宿主机。容器安全的核心思路很朴素:假设进程一定会被攻破,那就让它被攻破后什么也干不了。这篇把几个立刻能落地、性价比最高的加固手段串起来:换非 root 用户、只读根文件系统、砍掉多余的 Linux capabilities、禁止提权。全是几行配置的事,却能把攻击面砍掉一大半。第一刀:别用 root 跑进程默认容器里USER是 root。验证一下:dockerrun--rmalpineid# uid0(root) gid0(root) groups0(root)在 Dockerfile 里建一个普通用户并切过去。注意顺序:装依赖等需要 root 权限的操作放前面,USER指令放在最后,这样运行时进程才是非 root:FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # 建一个固定 uid 的非 root 用户(指定 uid 便于宿主机侧对齐文件权限) RUN groupadd -g 10001 appuser \ useradd -u 10001 -g 10001 -m -s /usr/sbin/nologin appuser # 关键:切换到非 root,后续进程都以 appuser 身份运行 USER 10001:10001 CMD [python, main.py]用数字 uid(USER 10001)而不是用户名,好处是即便镜像里/etc/passwd被改,Kubernetes 的runAsNonRoot也能正确识别。运行时也能强制不许用 root 起:# 即使镜像里写了 USER root,这行也会让容器拒绝启动dockerrun--user10001:10001 myapp第二刀:只读根文件系统进程被攻破后,攻击者第一件事往往是往磁盘写马、改二进制、留后门。给容器一个只读根文件系统,让它根本写不进去:dockerrun --read-only myapp但很多程序要写临时文件、日志、pid 文件,一上来会崩。正确做法不是放弃只读,而是只给需要写的目录挂上可写的 tmpfs,其余全只读:dockerrun\--read-only\--tmpfs/tmp:rw,noexec,nosuid,size64m\--tmpfs/run:rw,noexec,nosuid,size8m\myappnoexec尤其关键:它让写进/tmp的东西不能被执行。攻击者就算把 payload 写进/tmp,也运行不起来。在 Docker Compose 里等价写法:services:app:image:myappread_only:truetmpfs:-/tmp:rw,noexec,nosuid,size64muser:10001:10001排查思路:开只读后如果容器崩了,docker logs会看到Read-only file system报错,顺着报错路径把那个目录加进 tmpfs(或挂一个具名 volume)即可,一个个补,别直接退回可写。第三刀:砍掉用不到的 capabilitiesLinux capabilities 把「root 的超能力」拆成了几十个细粒度权限(改系统时间、绑定低端口、加载内核模块……)。Docker 默认就给了容器一串 capabilities,但绝大多数 Web 应用一个都用不到。正确姿势是先全砍光,再按需加回来:dockerrun\--cap-dropALL\--cap-addNET_BIND_SERVICE\myapp--cap-dropALL先清空所有 capabilities,--cap-addNET_BIND_SERVICE只在「进程确实要监听 80/443 这类 1024 以下端口」时加回来。如果你的服务监听的是 8080 这种高端口,那连这个都不用加,直接--cap-dropALL就行。顺带禁止提权——阻止进程通过 setuid 二进制拿到更高权限:dockerrun\--cap-dropALL\--security-optno-new-privileges\myappno-new-privileges会让容器里任何 setuid 程序都无法提权,把「从普通用户偷偷变 root」这条路彻底堵死。第四刀:镜像本身要小、要干净攻击面也包括镜像里那些用不到的东西——一个装了curl、bash、编译器的镜像,给了攻击者一堆现成工具。两个方向:用多阶段构建,只把产物拷进最终镜像,构建工具全不带:# 构建阶段:有完整工具链 FROM golang:1.22 AS build WORKDIR /src COPY . . RUN CGO_ENABLED0 go build -o /app/server . # 运行阶段:distroless,连 shell 都没有 FROM gcr.io/distroless/static:nonroot COPY --frombuild /app/server /server USER nonroot:nonroot ENTRYPOINT [/server]distroless镜像不含包管理器、不含 shell,攻击者进来连sh都没有,docker exec也进不去交互式终端。它自带nonroot用户,直接USER nonroot即可。再用 trivy 扫一遍镜像里依赖的已知漏洞,纳入 CI:# 扫出 HIGH/CRITICAL 级别漏洞就让流水线失败trivy image--severityHIGH,CRITICAL --exit-code1myapp:latest一份「全加固」的运行配置把上面几刀合起来,一个典型的加固运行命令长这样:dockerrun-d\--user10001:10001\--read-only\--tmpfs/tmp:rw,noexec,nosuid,size64m\--cap-dropALL\--security-optno-new-privileges\--pids-limit200\--memory512m\myapp额外两个--pids-limit(限制进程数,防 fork 炸弹)和--memory(内存上限,防单容器吃光宿主机),属于「限制爆炸半径」的兜底,顺手加上。小结非 root:Dockerfile 里建普通用户、USER 数字uid放最后;运行时--user强制。只读根文件系统:--read-only 给需要写的目录挂--tmpfs(带noexec),让攻击者写不进也执行不了。capabilities:--cap-dropALL先全砍,只按需--cap-add;配--security-optno-new-privileges禁提权。最小镜像:多阶段构建 distroless(无 shell 无包管理器),trivy 扫漏洞纳入 CI。兜底:--pids-limit、--memory限制爆炸半径。一句话记忆:容器安全的心法是「假设它一定会被攻破」——那就让被攻破的进程不是 root、写不了盘、没有超能力、也没有趁手的工具。
返回列表