CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Docker 容器入门/课时/容器安全基础
RawBlame
课时目录12 节

Docker 容器入门

  1. 01容器是什么
  2. 02安装与环境
  3. 03Docker CLI 基础
  4. 04镜像与分层
  5. 05编写 Dockerfile
  6. 06构建与优化
  7. 07镜像仓库
  8. 08数据卷与挂载
  9. 09网络
  10. 10Docker Compose
  11. 11容器安全基础
  12. 12综合练习与 K8s 方向

容器安全基础

非 root、最小镜像、密钥不入镜像。

25 分钟

容器安全基础

这一课你将能做到

  1. 把 Spring Boot 镜像改成非 root 用户运行,并用 id 命令验证容器内进程身份
  2. 复现「密码写进 Dockerfile 会从 docker history 泄漏」的安全事故,掌握运行时注入密钥的正确做法
  3. 用 Trivy 扫描镜像漏洞、用资源限制防止容器拖垮宿主机,形成一份可落地的安全清单

为什么要学

前面课程的目标一直是「能跑起来」,本课的目标是「敢放进生产」。容器默认配置有三个隐患,恰好在 Java 后端项目里最常见:

  1. 容器内进程默认以 root 运行——一旦容器被攻破,攻击者在容器内的 root 结合内核漏洞可能影响宿主机。
  2. 开发者习惯把数据库密码写进 Dockerfile 的 ENV——docker history 对任何拿到镜像的人可见,等于密码随镜像公开。
  3. 镜像里的基础系统与依赖带着已知 CVE——不扫描就带病上线。

这三条不需要高深知识就能修,改造成本低、面试常问、生产收益高,是性价比最高的一课。

先建立图像

安全问题的成因链条:

镜像层(构建期定型)                 运行期(每次 run 决定)
┌──────────────────────────┐      ┌──────────────────────────┐
│ Dockerfile ENV 写了密码 ✗  │      │ -p 0.0.0.0:3306 裸奔 ✗    │
│ 以 root 身份运行 ✗         │      │ 无内存/CPU 限制 ✗          │
│ 基础镜像带 CVE ✗           │      │ 密码明文注入环境变量 △     │
└──────────────────────────┘      └──────────────────────────┘
        ↓ 修复手段                        ↓ 修复手段
  多阶段+最小镜像、USER 非 root      端口收敛、资源限额、
  密钥不进层(history 可见)          .env / secrets 注入

优先级排序(本课顺序即优先级):密钥不入镜像 > 非 root > 端口收敛 > 漏洞扫描 > 资源限制。

跟着做

步骤 1:亲眼确认默认是 root

docker run --rm eclipse-temurin:21-jre id
# 输出:uid=0(root) gid=0(root) groups=0(root)

步骤 2:Dockerfile 改为非 root 运行

FROM eclipse-temurin:21-jre
ENV TZ=Asia/Shanghai

# 创建系统用户(-r 不建家目录,uid 10001 固定便于 volume 授权)
RUN useradd -r -u 10001 appuser
USER appuser

WORKDIR /app
COPY --chown=appuser:appuser target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

要点:COPY --chown 让 jar 归属应用用户;Java 监听 8080(>1024 非特权端口)无需 root。重新构建并验证:

docker build -t demo-app:1.1-secure .
docker run --rm demo-app:1.1-secure id
# 输出:uid=10001(appuser) —— 非 root 生效

步骤 3:复现「docker history 泄漏密码」

# 反例(仅实验,不要提交)
FROM eclipse-temurin:21-jre
ENV DB_PASSWORD=SuperSecret123
docker build -t leak-demo . 
docker history leak-demo
# 输出里能看到:ENV DB_PASSWORD=SuperSecret123 —— 密码随镜像公开

同时它还写进了镜像层元数据,即使后续 ENV 覆盖也删不干净。结论:。

镜像里永远不出现明文密钥

步骤 4:密钥的正确注入方式(运行时)

# 方式一:run 时 -e 注入
docker run -d --name secure-app \
  -e SPRING_DATASOURCE_PASSWORD=StrongPass_2024 \
  demo-app:1.1-secure

# 方式二:环境文件(生产推荐,权限 600)
# secret.env 内容:SPRING_DATASOURCE_PASSWORD=StrongPass_2024
docker run -d --env-file ./secret.env demo-app:1.1-secure

# compose 场景:.env 已在第 10 课实践(不进 git)

注意:-e 注入的变量可通过 docker inspect 被宿主机上有 docker 权限的人看到——所以宿主机本身必须收紧权限(docker 组≈root)。更进一步用 docker secrets / Vault / K8s Secret(进阶方向)。

步骤 5:收敛端口暴露面

# 危险:数据库端口对所有网卡开放(云上=公网可达)
docker run -d -p 3306:3306 mysql:8.0

# 正确:MySQL/Redis 完全不映射端口,只在容器网络内互通(第 9 课 app-net)
# 需要本地调试时,只绑回环:
docker run -d -p 127.0.0.1:3306:3306 mysql:8.0
# 应用端口对外提供时再绑 0.0.0.0(默认)

步骤 6:资源限制防「拖垮宿主机」

docker run -d --name limited-app \
  --memory=512m --memory-swap=512m \
  --cpus=1 \
  -p 8080:8080 demo-app:1.1-secure

docker stats --no-stream limited-app   # 查看实际占用

内存超限的容器会被 OOM kill(docker inspect 里 State.OOMKilled: true)——这本身就是保护:宁可重启单个应用,不能让 JVM 吞掉整台机器。Java 侧配合:JDK 10+ 默认感知容器内存限制;仍建议显式 -XX:MaxRAMPercentage=75.0 而不是写死 -Xmx。

步骤 7:镜像漏洞扫描

# Trivy(业界主流,一条命令扫指定镜像)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image demo-app:1.1-secure

输出按严重级别列出 CVE(CRITICAL/HIGH 优先处理)。配合习惯:基础镜像固定用较新 tag、定期重建镜像(让基础层更新)、CRITICAL 清零后再发布。Docker Desktop 内置的 docker scout 可作为替代。

讲清楚要点

  1. 密钥不入镜像是铁律:ENV、ARG、COPY 的配置文件都会永久留在镜像层里,docker history 与 registry 上的任何使用者都可见。密钥只在运行时进入容器,且来源必须受控(.env/secrets/配置中心)。
  2. 非 root 的边界:它防的是「容器内横向提权 + 配合内核漏洞逃逸」的常见路径,不是银弹。生产进阶方向:rootless 模式运行整个引擎、K8s 里 securityContext 强制约束。
  3. 最小镜像 = 最小攻击面:基础镜像里每个多余的包都是潜在漏洞来源。多阶段构建(第 6 课)同时是性能优化和安全优化。
  4. 端口默认关闭、按需打开:安全组只放行 80/443(及必要的管理口);MySQL/Redis 保持在容器网络内部。-p 127.0.0.1:... 是本地调试的安全姿势。
  5. 资源限制是可用性安全:无限制的容器 = 单点故障放大器。JVM 应用显式设内存上限,避免 full GC 或内存泄漏拖垮宿主机上所有服务。

你可能会懵的地方

  • 容器内 root 等于宿主机 root 吗? 不完全等于——容器有独立的用户命名空间映射、能力(capabilities)被裁剪,root 在容器内权限受限。但历史上多次出现「容器内 root + 内核漏洞 → 逃逸到宿主机」,所以非 root + 最小能力仍是标配。

  • 非 root 运行后写卷报 Permission denied? 卷目录属主与容器内 uid 不一致。两种修法:构建时固定 uid 并用 chown 初始化卷(如 MySQL 容器内是 uid 999);或运行一次性容器 chown -R 10001:10001 /挂载点 对齐属主。

  • 密码用 -e 注入就绝对安全了吗? 相比写进镜像是质的提升,但 docker inspect 和 /proc/1/environ 在容器内可见。分级认知:镜像内 ✗ → 宿主机 .env △ → secrets 管理(文件挂载、配置中心、Vault)✓。按公司安全等级选择。

  • 镜像扫描出一堆 CVE 怎么办? 按 CRITICAL/HIGH 优先修:多数来自基础镜像,升级基础镜像 tag 重新构建即可消除;少数来自应用依赖(jar 里的库漏洞),走 Maven 依赖升级流程。扫描要进 CI,做成发布门禁。

  • 还需要学 rootless Docker / Podman 吗? 知道方向即可:rootless 让引擎本身不以 root 跑,进一步缩小逃逸影响面,多见于强合规环境;Podman 是无守护进程的 OCI 兼容替代品,命令与 docker 几乎一致(alias docker=podman 可无缝过渡)。

错法对照

错法问题正解
Dockerfile 里 ENV DB_PASSWORD=xxxdocker history 与镜像层永久可见运行时 -e / --env-file / secrets
容器默认 root 运行提权与逃逸风险放大useradd + USER + COPY --chown
-p 3306:3306 直接暴露数据库公网爆破,教科书级事故不映射;调试绑 127.0.0.1
JVM 容器不设内存限制OOM 拖垮宿主机全部服务--memory + MaxRAMPercentage
基础镜像用 latest 永不更新CVE 随时间持续累积固定版本 + 定期重建 + CI 扫描
只扫一次镜像新 CVE 持续披露扫描进流水线,发布门禁

卡点急救

  1. 非 root 后容器启动即退:docker logs 看是否有 Permission denied。检查 jar 与工作目录的属主(COPY --chown)、写日志目录是否挂载给了正确 uid。
  2. Trivy 拉取失败或扫描超时:国内网络问题,可换 registry 镜像源拉 aquasec/trivy,或改用 Docker Desktop 的 docker scout。
  3. --env-file 不生效:文件里不能有空格分隔的行内赋值格式错误(必须 KEY=value),且不会做 shell 展开;用 docker exec 容器 printenv 验证注入结果。
  4. 容器 OOMKilled 反复重启:docker inspect 确认 State.OOMKilled: true;上调 --memory 或下调 JVM MaxRAMPercentage,并排查应用内存泄漏(docker stats 观察)。
  5. 误提交了带密码的镜像到仓库:立即换密码(改密码优于删镜像)、删除镜像 tag、清理 git/CI 日志中的明文,再用运行时注入方式重新发布。

面试常问

  • 说一说你的容器安全实践清单? 按层回答:镜像层(多阶段最小镜像、固定版本基础镜像、非 root、无明文密钥、CI 漏洞扫描);运行层(端口最小暴露、资源限额、restart 策略);宿主层(docker 组收紧、内核与 Docker 版本及时更新、日志审计)。

  • 为什么容器内不应该用 root? 纵深防御:非 root 缩小容器被攻破后的横向移动能力,降低配合内核漏洞逃逸的风险。实现是 Dockerfile 的 USER 指令;K8s 侧对应 securityContext 的 runAsNonRoot。

  • 如何在 Dockerfile 之外安全地传递数据库密码? 运行时注入:--env-file(宿主机文件,权限 600、不入 git)、compose 的 .env、docker secrets、配置中心/密钥管理服务。原则:密钥不进镜像层、不进 git、进容器仅运行时。

  • 镜像漏洞扫描的原理与落地方式? 扫描器解析镜像层里的系统包与应用依赖清单,与 CVE 数据库比对。落地:Trivy/scout 进 CI,CRITICAL 阻断发布,基础镜像定期升级重建。

自测三问

  1. 写出把 Spring Boot 镜像改为 uid 10001 非 root 运行的 Dockerfile 片段,并说明 COPY --chown 的作用。
  2. 为什么 docker history 能看到密码?哪些 Dockerfile 写法会让密钥进入镜像层?
  3. 给一个内存 2G 的宿主机上跑的 JVM 容器,设计内存限制方案(容器 + JVM 两侧)。

小结一句话

容器安全的四件套——密钥运行时注入、非 root 运行、端口最小暴露、资源限额加漏洞扫描——把「能跑的镜像」变成「敢上生产的镜像」。

延伸

  • Docker 安全官方文档
  • Trivy 官方仓库
  • 最后一课预告:综合练习——把 11 课所学串成一条完整的生产部署流水线,并绘制 K8s 进阶路线图。
加载本节练习…
← 上一课Docker Compose下一课 →综合练习与 K8s 方向