Gymterview
middle

Как использовать multistage builds для повышения безопасности?

Multistage build – это техника построения Docker-образов, при которой Dockerfile содержит несколько стадий сборки, и в финальный образ копируются только необходимые артефакты, исключая компилятор, исходный код, тесты и build-утилиты.

Аналогия: multistage build – как кухня ресторана. Гостю (production) подают только готовое блюдо, а все ножи, разделочные доски и отходы (JDK, Maven, исходники) остаются на кухне и не попадают в зал.

Проблема без multistage build

Пример
# ПЛОХО: всё в одном образе
FROM eclipse-temurin:21-jdk
WORKDIR /app
COPY . .
RUN ./mvnw clean package
ENTRYPOINT ["java", "-jar", "target/app.jar"]
# Результат: образ ~800 MB, содержит JDK, Maven, исходники, тесты, .git

Этот образ содержит JDK с компилятором, Maven wrapper, все исходные файлы, тестовые зависимости, .git директорию – всё это увеличивает поверхность атаки и даёт атакующему инструменты для дальнейшего проникновения.

Правильный multistage build для Java

Multistage build с layered Spring Boot jar
# ============ Этап 1: Сборка ============
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /build

# Кэширование зависимостей Maven
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN ./mvnw dependency:go-offline -B

# Копирование исходного кода и сборка
COPY src ./src
RUN ./mvnw clean package -DskipTests -B && \
    # Извлечение Spring Boot layered jar для лучшего кэширования
    java -Djarmode=layertools -jar target/*.jar extract --destination /extracted

# ============ Этап 2: Финальный образ ============
FROM eclipse-temurin:21-jre-alpine

RUN addgroup -S banking && adduser -S banking -G banking

WORKDIR /app

# Копирование только скомпилированного приложения (layered)
COPY --from=builder --chown=banking:banking /extracted/dependencies/ ./
COPY --from=builder --chown=banking:banking /extracted/spring-boot-loader/ ./
COPY --from=builder --chown=banking:banking /extracted/snapshot-dependencies/ ./
COPY --from=builder --chown=banking:banking /extracted/application/ ./

USER banking

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Multistage build с кастомным JRE (jlink)

Трёхстадийная сборка с jlink
# Этап 1: Сборка приложения
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
COPY . .
RUN ./mvnw clean package -DskipTests

# Этап 2: Определение нужных Java-модулей и создание кастомного JRE
FROM eclipse-temurin:21-jdk-alpine AS jre-builder
COPY --from=builder /build/target/*.jar /app/app.jar
RUN jar xf /app/app.jar && \
    jdeps \
        --ignore-missing-deps \
        --print-module-deps \
        --multi-release 21 \
        --class-path 'BOOT-INF/lib/*' \
        /app/app.jar > /modules.txt && \
    jlink \
        --add-modules $(cat /modules.txt) \
        --strip-debug \
        --no-man-pages \
        --no-header-files \
        --compress=zip-9 \
        --output /custom-jre

# Этап 3: Минимальный финальный образ
FROM alpine:3.20
COPY --from=jre-builder /custom-jre /opt/java
COPY --from=builder /build/target/*.jar /app/app.jar

RUN addgroup -S app && adduser -S app -G app
USER app

ENV JAVA_HOME=/opt/java
ENV PATH="${JAVA_HOME}/bin:${PATH}"

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Что удаляется из финального образа

Компонент Угроза Удалён в multistage?
JDK (javac, jdb) Компиляция вредоносного кода на месте Да
Maven/Gradle Скачивание вредоносных зависимостей извне Да
Исходный код Реверс-инжиниринг бизнес-логики Да
Тестовый код и данные Утечка тестовых учётных данных Да
Build-утилиты (git, curl) Инструменты для развития атаки Да
.git директория История изменений, возможные секреты в коммитах Да

Файл .dockerignore

Не забывайте о .dockerignore – он предотвращает копирование ненужных файлов в контекст сборки:

Пример
.git
.gitignore
*.md
docker-compose*.yml
.env
.idea
*.iml
target
!target/*.jar

Вывод

Multistage build – обязательная практика для production-образов. Она уменьшает размер образа в 5-10 раз, удаляет из финального образа все build-инструменты и исходный код, что радикально сокращает поверхность атаки.

На собеседовании: покажите понимание двух- или трёхстадийной сборки, объясните, почему каждый удалённый компонент – это закрытая дверь для атакующего. Бонус – упомяните layered jars Spring Boot и jlink для создания кастомного JRE.