Безопасный Docker на Linux: образы, секреты и цепочка поставки

Безопасный Docker на Linux: образы, секреты и цепочка поставки

Контейнер облегчает доставку приложения, но не создаёт автоматическую границу доверия. Уязвимый базовый образ, секрет в Dockerfile, доступный Docker socket или контейнер с лишними capabilities способны привести к компрометации хоста. Безопасность начинается в репозитории и продолжается до runtime и обновления production.

Современный подход рассматривает контейнер как артефакт цепочки поставки: исходный код, зависимости, builder, registry, подпись, оркестратор и хост должны иметь проверяемое происхождение и минимальные права.

Модель угроз контейнерного проекта

  • вредоносный или заброшенный базовый образ;
  • уязвимые системные и языковые зависимости;
  • секреты в слоях образа или истории Git;
  • подмена образа между сборкой и деплоем;
  • запуск от root и лишние Linux capabilities;
  • монтирование Docker socket;
  • неограниченная сеть между контейнерами;
  • отсутствие лимитов ресурсов и журналирования.

Фиксируйте происхождение базового образа

Используйте официальный или внутренний доверенный registry. В production фиксируйте образ по digest:

docker pull nginx@sha256:...
docker image inspect nginx@sha256:...

Тег удобен человеку, но может указывать на другое содержимое. Digest связывает deployment с конкретным артефактом. При этом pinning не отменяет обновления: процесс должен регулярно создавать новый проверенный digest.

Multi-stage build и минимальный runtime

FROM golang:1.24-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/app ./cmd/app

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

Компилятор и менеджер пакетов остаются в build-stage, а runtime содержит только нужный бинарник. Для Python, PHP и Java выбирайте минимальный, но обслуживаемый образ. «Пустой» образ не всегда лучше: если команда не умеет его диагностировать и обновлять, операционный риск растёт.

Воспроизводимые зависимости

Используйте lock-файлы и проверку контрольных сумм. Не выполняйте curl | sh в Dockerfile и не скачивайте «последнюю» версию без верификации. Сборка должна быть повторяемой, а источник каждого пакета — известным.

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

Сканирование и политика допуска

Сканируйте образ после сборки и перед deployment. Разделяйте найденные CVE по доступности исправления, эксплуатируемости и реальному использованию компонента. Политика может запрещать критические уязвимости с доступным патчем и требовать документированное исключение для остальных.

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

Подпись образов

Подписывайте production-образы после доверенной CI-сборки и проверяйте подпись перед запуском. В deployment используйте digest и правила допуска: разрешены только образы из утверждённого registry, подписанные нужной identity.

Ключ подписи не должен храниться рядом с Dockerfile. Современные схемы могут использовать краткоживущую identity CI вместо постоянного приватного ключа.

Секреты не являются build-аргументами

ARG и ENV могут попасть в историю образа, логи и метаданные. Для доступа к приватным зависимостям используйте BuildKit secrets, а runtime-секреты получайте из secret manager или защищённого механизма оркестратора.

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

После утечки удаление секрета из последнего слоя не помогает: он может остаться в предыдущем слое и кэше. Секрет нужно отозвать, выпустить заново и очистить историю/registry согласно процедуре.

Минимальные права runtime

Контейнер приложения запускайте от непривилегированного UID. Файловую систему делайте read-only, оставляя write-доступ только нужным каталогам. Удаляйте capabilities и запрещайте повышение привилегий:

docker run --read-only   --cap-drop=ALL   --security-opt=no-new-privileges:true   --user 10001:10001   --tmpfs /tmp:rw,noexec,nosuid,size=64m   myapp@sha256:...

Добавляйте отдельную capability только после подтверждения необходимости. Не используйте --privileged для исправления ошибки доступа — он почти стирает границу между контейнером и хостом.

Rootless Docker

Rootless-режим снижает последствия компрометации daemon и контейнера, но имеет ограничения по сети, storage drivers и низким портам. Протестируйте производительность и совместимость. Rootless не отменяет уязвимости ядра и контроль конфигурации.

Почему опасен Docker socket

Монтирование /var/run/docker.sock внутрь контейнера фактически даёт возможность управлять daemon: запускать привилегированные контейнеры, монтировать файловую систему хоста и получать секреты. Если инструменту нужен API Docker, используйте ограниченный proxy или отдельный изолированный worker.

Сеть и публикация портов

Не публикуйте порт, если он нужен только соседнему контейнеру. Создавайте отдельные сети для frontend, backend и данных. База должна быть доступна приложению, но не всему хосту и интернету.

services:
  db:
    image: postgres@sha256:...
    networks: [backend]
  app:
    image: app@sha256:...
    networks: [frontend, backend]
  proxy:
    image: nginx@sha256:...
    ports: ["443:443"]
    networks: [frontend]

Лимиты, healthcheck и журналы

Задайте лимиты CPU, RAM и процессов, чтобы ошибка или атака не исчерпала ресурсы хоста. Healthcheck должен проверять полезную функцию, а не только существование процесса. Журналы отправляйте централизованно и ограничивайте локальную ротацию.

Чеклист CI/CD

  1. проверка исходного кода и Dockerfile;
  2. сборка из фиксированных зависимостей;
  3. создание SBOM;
  4. сканирование образа;
  5. подпись доверенной CI identity;
  6. публикация в закрытый registry;
  7. deployment по digest;
  8. проверка подписи и политики;
  9. наблюдение и регулярное пересканирование.

Hardening Docker-хоста

Контейнеры наследуют безопасность ядра и daemon. Хост должен выполнять одну понятную роль, регулярно обновляться и иметь минимальный набор пакетов. Доступ к группе docker фактически равен административному: её участник может запустить контейнер с монтированием корневой файловой системы.

getent group docker
docker info
docker system df
ss -tulpn

Не публикуйте Docker daemon по TCP без строгой взаимной TLS-аутентификации и сетевого ограничения. Для большинства одиночных серверов удалённый API вообще не нужен.

Безопасный Compose-файл

В Compose явно задавайте пользователя, read-only filesystem, tmpfs, dropped capabilities, лимиты и healthcheck. Не монтируйте весь проект или домашний каталог в production-контейнер. Каждый bind mount расширяет доступ приложения к хосту.

services:
  app:
    image: registry.example/app@sha256:...
    user: "10001:10001"
    read_only: true
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]
    tmpfs:
      - /tmp:size=64m,noexec,nosuid
    pids_limit: 200
    restart: unless-stopped

Обновление без неконтролируемого latest

Обновление должно создавать новый immutable-артефакт, проходить тесты, сканирование и подпись, после чего меняется digest в deployment. Автоматическое подтягивание latest прямо в production удобно, но лишает команду контроля над моментом и содержимым изменения.

Подготовьте rollback на предыдущий digest и учитывайте миграции базы. Откат контейнера не откатывает необратимую схему данных, поэтому миграции проектируются совместимыми с двумя версиями приложения.

Runtime-наблюдение

Собирайте события запуска контейнеров, изменения образов, ошибки healthcheck, OOM, перезапуски и необычные сетевые соединения. Сравнивайте фактический runtime с утверждённым deployment: неожиданный контейнер или интерактивный shell на production требует проверки.

Журнал приложения должен содержать request ID и бизнес-события, но не секреты. Логи Docker ограничивайте ротацией, иначе один шумный контейнер заполнит диск хоста.

Резервное копирование данных

Образ контейнера не является резервной копией. Отдельно защищайте volumes, базы, загруженные пользователями файлы и секреты конфигурации. Проверяйте восстановление на новом хосте с тем же зафиксированным образом. Документируйте RTO/RPO и порядок запуска зависимостей.

FAQ

Нужен ли Kubernetes для безопасного Docker?

Нет. Для небольшого проекта Compose и systemd проще. Безопасность зависит от контроля образов, прав и хоста.

Можно ли использовать latest?

Для локальной разработки допустимо, но production должен быть воспроизводимым — используйте digest или неизменяемый version tag с проверкой.

Контейнер заменяет виртуальную машину?

Нет. Контейнеры обычно разделяют ядро хоста и имеют другую модель изоляции.

Где разместить проект

Для Python, PHP, ботов и pet-проектов мы рекомендуем Beget — SSH, Python, cron и поддержка 24/7 от 150 ₽/мес.

Перейти на Beget →


Нужна помощь с разработкой?

Автоматизация, CRM, Telegram-боты, деплой и техподдержка 24/7.

Телефон: 8 918 532 81 08 · Заказать услугу