Docker Hardening
Docker Hardening — набор практик для уменьшения поверхности атаки контейнеров, сокращения размера образов, ограничения прав процессов и поиска уязвимостей в базовых образах и зависимостях.
Hardening касается не только Dockerfile. Важно защищать Docker daemon, registry, CI/CD, хостовую систему, секреты, сети и параметры запуска контейнера.
Содержание
- Multi-stage сборки
- Уменьшение размера образов
- Непривилегированный пользователь
- Сканирование уязвимостей с помощью Trivy
- .dockerignore
- Best practices безопасности контейнеров
- Безопасный Dockerfile
- Чек-лист
- Краткая шпаргалка
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:nonrootAlpine использует 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 cidocker 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.0tmpfs добавляется только для каталогов, которым действительно нужна временная запись.
Сканирование уязвимостей с помощью 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.0SBOM — перечень компонентов образа:
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 1Healthcheck должен быть лёгким и не раскрывать чувствительную информацию. Если в минимальном образе нет 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" idPipeline должен дополнительно проверять секреты, зависимости, Dockerfile, SBOM, подпись образа и правила публикации в registry.
Чек-лист
Dockerfile
- Выбран подходящий минимальный базовый образ.
- Версии базового образа и зависимостей зафиксированы.
- Используется multi-stage, если есть компиляция или dev-зависимости.
- В runtime нет исходников, тестов и инструментов сборки.
- Кэш очищается в том же слое.
- Секреты не попадают в
ARG,ENV, слои и metadata. - Задан непривилегированный пользователь.
- Права на каталоги ограничены.
- Используется exec-форма
CMDилиENTRYPOINT.
Запуск
- Не используется
--privileged. - Убраны лишние capabilities.
- Включён
no-new-privileges. - Заданы лимиты памяти, CPU и процессов.
- Root filesystem read-only, если это возможно.
- Bind mounts имеют
:ro, когда запись не нужна. - Не монтируется Docker socket без обоснования.
- Не используется host network без необходимости.
- Не публикуются лишние порты.
- Секреты передаются безопасным механизмом.
CI/CD
- Выполняется
trivy config. - Выполняется
trivy image. - Есть политика для
HIGHиCRITICAL. - Исключения документированы и ограничены.
- Публикуется SBOM.
- Registry защищён контролем доступа.
- Для production фиксируется digest образа.
Краткая шпаргалка
# 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 помогает находить уязвимости и секреты до публикации образа.
Главный принцип — минимально необходимые права и компоненты. Контейнер должен содержать только то, что нужно приложению для работы, и запускаться с минимально возможными разрешениями.