Docker Hardening

Docker Hardening — набор практик для уменьшения поверхности атаки контейнеров, сокращения размера образов, ограничения прав процессов и поиска уязвимостей в базовых образах и зависимостях.

Hardening касается не только Dockerfile. Важно защищать Docker daemon, registry, CI/CD, хостовую систему, секреты, сети и параметры запуска контейнера.

Содержание


Multi-stage сборки

Multi-stage build — сборка образа в несколько этапов. На первом этапе выполняется компиляция или установка инструментов, а в итоговый runtime-образ копируются только файлы, нужные для запуска.

Без multi-stage в образ часто попадают компиляторы, менеджеры пакетов, исходники, тестовые зависимости, временные архивы и отладочные инструменты. Это увеличивает размер образа и количество потенциально уязвимых компонентов.

Пример для Go

FROM golang:1.24 AS builder

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

В итоговом образе отсутствуют Go-компилятор и исходный код.

Пример для Node.js

FROM node:22-bookworm-slim AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist

RUN groupadd --system app && useradd --system --gid app app
USER app
CMD ["node", "dist/server.js"]

Исходный код и dev-зависимости не копируются в runtime-слой.

Пример для Python

FROM python:3.13-slim AS builder
WORKDIR /build

COPY requirements.txt .
RUN python -m venv /opt/venv \\
    && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

FROM python:3.13-slim AS runtime
ENV PATH="/opt/venv/bin:$PATH" \\
    PYTHONDONTWRITEBYTECODE=1 \\
    PYTHONUNBUFFERED=1

WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY app.py .
RUN useradd --system --create-home --uid 10001 app
USER app
CMD ["python", "app.py"]

Именованные этапы

FROM node:22 AS build
# сборка приложения

FROM nginx:1.27-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html

Именованные этапы надёжнее ссылок вида 0 и 1, потому что порядок этапов можно изменить без исправления всех ссылок.

Собрать отдельный этап можно так:

docker build --target builder -t app:builder .

Для публикации следует использовать итоговый runtime-этап:

docker build --target runtime -t example/app:1.0 .

Уменьшение размера образов

Маленький образ быстрее скачивается, занимает меньше места и обычно содержит меньше пакетов, которые могут иметь уязвимости. Однако минимальный размер не должен достигаться ценой несовместимости или потери нужных сертификатов и системных библиотек.

Выбор базового образа

Часто используют следующий порядок предпочтений:

специализированный distroless-образ
slim-образ
alpine при подтверждённой совместимости
полный образ дистрибутива — только при необходимости

Примеры:

FROM python:3.13-slim
FROM node:22-bookworm-slim
FROM gcr.io/distroless/static-debian12:nonroot

Alpine использует musl вместо glibc. Некоторые бинарные зависимости и инструменты могут работать иначе, поэтому выбирать Alpine только по размеру не следует.

Установка только нужных пакетов

RUN apt-get update \\
    && apt-get install -y --no-install-recommends ca-certificates \\
    && rm -rf /var/lib/apt/lists/*

Для Alpine:

RUN apk add --no-cache ca-certificates

Очистку кэша выполняйте в той же инструкции, что и установку. Удаление файла в следующем слое не убирает его из предыдущего слоя.

Использование кэша

Сначала копируйте файлы зависимостей, затем исходный код:

COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Изменение исходников не потребует повторной установки зависимостей.

Не храните секреты в слоях

Плохой вариант:

ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken=$NPM_TOKEN

Секрет может остаться в истории сборки или metadata образа. Для BuildKit используйте секретное монтирование:

# syntax=docker/dockerfile:1.7
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
docker buildx build \\
  --secret id=npmrc,src="$HOME/.npmrc" \\
  -t example/app:1.0 .

Фиксация версий

Для воспроизводимости фиксируйте версии базовых образов и зависимостей:

FROM python:3.13.2-slim-bookworm

Тег latest может неожиданно изменить содержимое образа. Для строгого контроля используют digest:

FROM python:3.13.2-slim-bookworm@sha256:<digest>

Digest нужно обновлять по контролируемому процессу, иначе можно долго использовать старый образ с уже исправленными уязвимостями.


Непривилегированный пользователь

По умолчанию процесс часто запускается от root. При уязвимости приложения или ошибке конфигурации это может увеличить последствия инцидента. Приложение следует запускать от отдельного пользователя с минимальными правами.

Debian или Ubuntu

RUN groupadd --system --gid 10001 app \\
    && useradd --system --uid 10001 --gid 10001 --create-home app

WORKDIR /app
COPY --chown=app:app . .
USER 10001:10001
CMD ["./server"]

Alpine

RUN addgroup -S -g 10001 app \\
    && adduser -S -D -u 10001 -G app app

WORKDIR /app
COPY --chown=app:app . .
USER app:app

Процесс должен иметь право записи только в необходимые каталоги:

RUN mkdir -p /app/data && chown -R app:app /app/data
USER app:app

Проверка:

docker run --rm example/app:1.0 id
docker run --rm example/app:1.0 whoami

Если вывод содержит uid=0(root), проверьте, действительно ли приложению нужен root.

Read-only filesystem

Если приложение не должно изменять файлы образа, используйте read-only root filesystem:

docker run --rm \\
  --read-only \\
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \\
  example/app:1.0

tmpfs добавляется только для каталогов, которым действительно нужна временная запись.


Сканирование уязвимостей с помощью Trivy

Trivy — инструмент для сканирования контейнерных образов, файловых систем, Git-репозиториев и конфигураций. Он может находить уязвимости пакетов ОС и языковых библиотек, секреты и некоторые проблемы конфигурации.

Перед использованием проверьте установку:

trivy --version

Образ

trivy image example/app:1.0

Проверка только высоких и критических проблем:

trivy image --severity HIGH,CRITICAL example/app:1.0

Сканирование для CI с ненулевым кодом завершения:

trivy image \\
  --severity HIGH,CRITICAL \\
  --ignore-unfixed \\
  --exit-code 1 \\
  example/app:1.0

Здесь --ignore-unfixed исключает проблемы, для которых ещё нет исправления, а --exit-code 1 позволяет остановить pipeline.

Severity — инструмент приоритизации, а не доказательство эксплуатируемости в конкретном приложении. Результаты нужно анализировать с учётом реального пути выполнения и конфигурации.

JSON и SBOM

trivy image \\
  --format json \\
  --output trivy-image.json \\
  example/app:1.0

SBOM — перечень компонентов образа:

trivy image \\
  --format cyclonedx \\
  --output sbom.cdx.json \\
  example/app:1.0

Конфигурация и секреты

trivy config .
trivy fs --scanners secret .

Если секрет уже попал в Git, образ или registry, его следует считать раскрытым: удалить файл недостаточно, ключ нужно отозвать и заменить.

Исключения Trivy должны быть узкими, документированными и иметь владельца. Не следует глобально подавлять весь класс уязвимостей.


.dockerignore

.dockerignore определяет, какие файлы не отправляются в build context. Он уменьшает объём контекста и снижает риск случайно передать в сборку секреты и локальные данные.

Без него в context могут попасть .git, .env, приватные ключи, каталоги зависимостей, кэш, логи, тестовые результаты и большие архивы.

Пример

.git
.gitignore
.dockerignore
.env
.env.*
!.env.example

node_modules
npm-debug.log*
dist
coverage

__pycache__
*.py[cod]
.pytest_cache
.venv
venv

*.log
*.tmp
*.swp
.DS_Store

secrets/
keys/
*.pem
*.key

Не исключайте файлы, необходимые для сборки. Размер контекста можно наблюдать при подробном выводе:

docker build --progress=plain -t example/app:1.0 .

.dockerignore не удаляет секрет из истории Git. Если ключ был закоммичен, его необходимо отозвать и выпустить новый.


Best practices безопасности контейнеров

Ограничивайте привилегии

Не используйте --privileged без серьёзного обоснования:

docker run --privileged example/app:1.0

Убирайте ненужные Linux capabilities:

docker run --rm \\
  --cap-drop=ALL \\
  example/app:1.0

Добавляйте отдельную capability только при подтверждённой необходимости:

docker run --rm \\
  --cap-drop=ALL \\
  --cap-add=NET_BIND_SERVICE \\
  example/app:1.0

Запрещайте повышение привилегий:

docker run --rm \\
  --security-opt=no-new-privileges:true \\
  example/app:1.0

Не давайте доступ к Docker socket

docker run -v /var/run/docker.sock:/var/run/docker.sock example/app:1.0

Такой mount может дать контейнеру возможность управлять Docker daemon и создавать контейнеры с широкими правами. Используйте его только после отдельного анализа угроз.

Ограничивайте ресурсы

docker run --rm \\
  --memory=512m \\
  --cpus=1.0 \\
  --pids-limit=100 \\
  example/app:1.0

Ограничения снижают риск отказа в обслуживании из-за бесконтрольного потребления памяти, CPU или процессов.

Изолируйте сеть и порты

Не используйте --network host без необходимости. Не публикуйте наружу базы данных и внутренние сервисы, которым достаточно общей Docker-сети.

services:
  api:
    networks:
      - backend
  db:
    networks:
      - backend

networks:
  backend:
    driver: bridge

Если порт нужен только локальному reverse proxy, привяжите его к loopback-интерфейсу:

docker run -p 127.0.0.1:8080:8080 example/app:1.0

Защищайте mounts

Если контейнеру не нужна запись на хост, используйте :ro:

docker run --rm \\
  -v "$PWD/config:/app/config:ro" \\
  example/app:1.0

Не монтируйте системные каталоги хоста, Docker data-root и чувствительные сокеты без необходимости.

Секреты

Не помещайте пароли и токены в Dockerfile, образ, публичный .env, ARG или обычные ENV, если секрет не должен находиться в metadata. Используйте секреты CI/CD, секрет-хранилище платформы, Docker secrets в подходящем режиме или BuildKit secrets.

Healthcheck и логи

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

Healthcheck должен быть лёгким и не раскрывать чувствительную информацию. Если в минимальном образе нет wget, используйте встроенную проверку приложения.

Не записывайте в логи пароли, токены, cookies, приватные ключи и заголовки авторизации. Настройте ротацию логов, чтобы контейнеры не заполнили диск хоста.

Образы и registry

Используйте доверенный registry, ограничивайте доступ и по возможности проверяйте подписи, provenance и attestations. Для production фиксируйте digest проверенного образа.

Регулярно обновляйте базовые образы и зависимости: результат сканирования отражает состояние базы уязвимостей только на момент проверки.


Безопасный Dockerfile

# syntax=docker/dockerfile:1.7

FROM python:3.13-slim AS builder
ENV VIRTUAL_ENV=/opt/venv
ENV PATH="${VIRTUAL_ENV}/bin:$PATH"
WORKDIR /build

RUN python -m venv "$VIRTUAL_ENV"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.13-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \\
    PYTHONUNBUFFERED=1 \\
    VIRTUAL_ENV=/opt/venv \\
    PATH="/opt/venv/bin:$PATH"

WORKDIR /app
RUN groupadd --system --gid 10001 app \\
    && useradd --system --uid 10001 --gid 10001 --create-home app \\
    && mkdir -p /app/data \\
    && chown -R app:app /app

COPY --from=builder /opt/venv /opt/venv
COPY --chown=app:app app.py ./app.py
USER 10001:10001
EXPOSE 8080

CMD ["python", "app.py"]

Пример запуска с ограничениями:

docker build --pull -t example/app:1.0 .

docker run --rm \\
  --read-only \\
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \\
  --cap-drop=ALL \\
  --security-opt=no-new-privileges:true \\
  --memory=512m \\
  --cpus=1.0 \\
  --pids-limit=100 \\
  -p 127.0.0.1:8080:8080 \\
  example/app:1.0

Пример проверки в CI

set -euo pipefail

IMAGE="example/app:${GIT_COMMIT}"

docker build --pull -t "$IMAGE" .
trivy config --exit-code 1 --severity HIGH,CRITICAL .
trivy image \\
  --severity HIGH,CRITICAL \\
  --ignore-unfixed \\
  --exit-code 1 \\
  "$IMAGE"
docker run --rm "$IMAGE" id

Pipeline должен дополнительно проверять секреты, зависимости, Dockerfile, SBOM, подпись образа и правила публикации в registry.


Чек-лист

Dockerfile

Запуск

CI/CD


Краткая шпаргалка

# Multi-stage сборка
docker build --target runtime -t example/app:1.0 .

# Слои образа
docker history example/app:1.0

# Проверка пользователя
docker run --rm example/app:1.0 id

# Сканирование образа
trivy image example/app:1.0
trivy image --severity HIGH,CRITICAL example/app:1.0

# Проверка конфигурации и секретов
trivy config .
trivy fs --scanners secret .

# SBOM
trivy image --format cyclonedx --output sbom.json example/app:1.0

# Безопасный запуск
docker run --rm \\
  --read-only \\
  --cap-drop=ALL \\
  --security-opt=no-new-privileges:true \\
  --memory=512m \\
  --cpus=1 \\
  --pids-limit=100 \\
  example/app:1.0

Итог

Docker Hardening строится на сочетании мер. Multi-stage сборка и минимальный runtime-образ уменьшают количество компонентов. Непривилегированный пользователь, read-only filesystem, ограничения ресурсов и удаление capabilities ограничивают последствия компрометации. .dockerignore предотвращает передачу лишних файлов в контекст, а Trivy помогает находить уязвимости и секреты до публикации образа.

Главный принцип — минимально необходимые права и компоненты. Контейнер должен содержать только то, что нужно приложению для работы, и запускаться с минимально возможными разрешениями.