从基础镜像到运行时:构建更难被攻破的 Java 容器

2026-09-03 33 预计阅读时间: 1 分钟
来源: spring.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

容器安全不只是上线前执行一次漏洞扫描。BellSoft 的 Catherine Edelveis 围绕加固运行时镜像与容器安全展开的讨论,指向了一个更实际的问题:进入生产环境的 Java 容器究竟包含什么、以什么权限运行,以及出现新漏洞后能否快速重建和替换。

真正有效的加固通常不是增加更多安全工具,而是减少镜像中的组件、收紧进程权限,并让构建结果可以重复验证。

“更小”不等于“已加固”

缩小镜像很重要,因为每删除一个不必要的软件包,就减少一部分潜在攻击面。不过,镜像体积只是一个容易观察的指标,并不能单独代表安全性。

一个面向生产环境的运行时镜像至少需要回答这些问题:

  • 是否包含编译器、包管理器、Shell 或调试工具?生产进程不需要的工具通常不应留在最终镜像中。
  • Java 应用是否以非 root 用户运行?
  • 基础镜像和 JRE 是否有明确版本,能否稳定重建?
  • 应用是否只能写入必要目录?
  • 镜像中有哪些操作系统包和 Java 依赖,能否生成 SBOM 并持续扫描?
  • 发现高危漏洞后,是在运行中的容器里“打补丁”,还是重新构建并替换不可变镜像?

因此,选用经过裁剪或加固的运行时镜像只是起点。Dockerfile、Kubernetes 安全上下文、供应链检查和更新机制必须一起工作。

用多阶段构建隔离编译环境

下面是一个可以直接改造的 Java 示例。假设项目使用 Maven,并能通过 mvn package 生成一个可执行 JAR。运行前需要把基础镜像标签替换为团队已经审核并固定的版本;示例中的标签仅用于说明构建结构。

# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /workspace

COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B dependency:go-offline

COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
    mvn -B package -DskipTests

FROM bellsoft/liberica-runtime-container:jre-21-slim-glibc

RUN addgroup --system app && adduser --system --ingroup app app
WORKDIR /app
COPY --from=build --chown=app:app /workspace/target/*.jar app.jar

USER app:app
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]

构建并检查实际运行身份:

docker build -t demo-java:local .
docker run --rm --entrypoint id demo-java:local
docker run --rm -p 8080:8080 demo-java:local

多阶段构建让 Maven、源码和编译缓存停留在构建阶段,最终镜像只接收运行产物。USER 则避免应用默认获得容器内 root 权限。不过,即使容器使用非 root 用户,也不能把容器边界视为绝对隔离;宿主机内核、容器运行时和编排平台仍然需要及时更新。

若应用不依赖字体、Shell、原生诊断工具或动态安装能力,还可以评估更精简的运行时。反过来,如果值班人员需要在生产环境执行诊断命令,不应悄悄把大量工具塞回业务镜像,更合适的做法是使用受控的临时调试容器。

把运行权限继续收紧

镜像中的非 root 用户只是第一层。部署到 Kubernetes 时,还可以禁止提权、删除 Linux capabilities,并将根文件系统设为只读。下面的配置假设应用只需要监听 8080 端口,并将临时文件写入 /tmp

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-java
spec:
  replicas: 2
  selector:
    matchLabels:
      app: demo-java
  template:
    metadata:
      labels:
        app: demo-java
    spec:
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: registry.example.com/demo-java@sha256:REPLACE_WITH_DIGEST
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              memory: 512Mi
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 128Mi

使用 digest 而不是可变标签,可以确保部署的是审核过的确切镜像。readOnlyRootFilesystem 往往会暴露应用的隐式写盘行为,因此应先在测试环境运行集成测试。若框架需要缓存、上传或生成堆转储,应为这些用途分别挂载受限卷,而不是重新开放整个根文件系统。

让扫描结果进入交付链路

漏洞扫描只有在结果能够影响发布时才有价值。可以这样实践:构建镜像后生成 SBOM,再按团队策略检查漏洞。

# 需要预先安装 syft 和 grype
syft demo-java:local -o cyclonedx-json > sbom.cdx.json
grype sbom:sbom.cdx.json --fail-on high

--fail-on high 是示例阈值,不应机械套用。生产策略通常还要区分漏洞是否存在可利用路径、是否已有修复版本,以及基础镜像维护方的安全公告。扫描器也可能产生误报或暂时没有修复方案,因此流水线需要受控的例外机制,并为例外设置负责人和过期时间。

SBOM 也不应只作为构建日志里的临时文件。更稳妥的做法是将它与镜像 digest、源码提交和构建记录关联保存,从而在新 CVE 发布时迅速定位受影响的制品。

落地时检查这六件事

  1. 使用多阶段构建,避免把构建工具和源码带入生产镜像。
  2. 固定基础镜像版本,并在部署清单中使用镜像 digest。
  3. 同时在镜像和编排层强制非 root、禁止提权、删除 capabilities。
  4. 默认使用只读根文件系统,只为明确的写入路径挂载卷。
  5. 生成并保存 SBOM,对镜像和应用依赖持续扫描。
  6. 定期重建镜像,而不是等待线上容器长期积累补丁债务。

加固镜像的核心不是追求一个“绝对安全”的标签,而是建立可审计、可重建、可快速替换的运行时。镜像越简单,权限越明确,更新路径越短,团队在漏洞出现时就越容易做出可靠响应。


相关推荐