MAATRIX / Блог / Безопасность Docker: лучшие практики

Безопасность Docker: лучшие практики

Безопасность Docker: лучшие практики

MAATRIX

По умолчанию Docker настроен на удобство, а не на безопасность: процесс в контейнере запускается от root, сокет демона доступен любому, кто входит в группу docker, базовый образ не проверяется на уязвимости, а файловая система контейнера пишется свободно. Ничего из этого не ломает работу приложения — пока сервер не скомпрометируют. Ниже — практический чек-лист того, что стоит поправить в первую очередь, без теории про модели угроз.

Не запускайте процесс в контейнере от root

Если в Dockerfile нет директивы USER, процесс внутри контейнера работает от root — и это не мелочь. Root в контейнере с примонтированным томом видит и меняет файлы хоста с правами root, даже если сам контейнер изолирован через namespaces. Побег из контейнера (через уязвимость в ядре или неудачную конфигурацию) от root даёт атакующему root на хосте, от непривилегированного пользователя — куда меньше.

Минимальная правка Dockerfile:

FROM node:20-slim

RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
COPY --chown=app:app . .
RUN npm ci --omit=dev

USER app
CMD ["node", "server.js"]

Для образов, где нет своего useradd (например, минималистичные base-образы), пользователя можно задать по UID без создания записи в /etc/passwd:

USER 10001:10001

Если поправить Dockerfile нельзя (чужой образ), root можно ограничить на уровне запуска:

docker run --user 1000:1000 -d myimage
services:
  app:
    image: myimage
    user: "1000:1000"

Проверить, от кого реально работает контейнер, можно так:

docker exec my_container id

Если видите uid=0(root) — контейнер не защищён этим пунктом, даже если в Dockerfile была директива USER, но образ переопределили флагом при запуске.

docker.sock внутри контейнера — это root на хосте

Монтирование /var/run/docker.sock внутрь контейнера (частый паттерн для Portainer, CI-раннеров, watchtower-подобных утилит) даёт этому контейнеру возможность управлять Docker-демоном хоста — создавать новые контейнеры с любыми привилегиями, монтировать любые пути хоста, читать чужие переменные окружения через docker inspect. По факту это равносильно root-доступу к серверу, только через промежуточный API.

# так делать не стоит без крайней необходимости
services:
  ci-runner:
    image: my-ci-runner
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Если контейнеру действительно нужно управлять другими контейнерами (CI, оркестрация), варианты по убыванию риска:

  • Docker socket proxy — прослойка вроде tecnativa/docker-socket-proxy, которая пропускает только разрешённые эндпоинты API (например, только чтение статусов, без создания контейнеров).
  • Rootless Docker — демон работает от непривилегированного пользователя, и утечка сокета не даёт root на хосте.
  • Отдельная нода для сборки — вынести CI-раннеры на отдельный сервер, где компрометация не затронет продакшен-контейнеры.

Если выбора нет и сокет монтировать приходится — хотя бы не давайте контейнеру дополнительно privileged: true и не открывайте наружу порты этого контейнера без аутентификации.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Сканируйте образы на уязвимости до деплоя

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

Docker Scout встроен в современный Docker CLI:

docker scout cves myimage:latest
docker scout quickview myimage:latest

Trivy — отдельная утилита, работает и без демона Docker, удобна для CI:

trivy image --severity HIGH,CRITICAL myimage:latest

В GitHub Actions проверка образа перед публикацией выглядит примерно так:

- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: "myimage:${{ github.sha }}"
    severity: "CRITICAL,HIGH"
    exit-code: "1"

exit-code: "1" останавливает деплой, если найдены критичные уязвимости — так проверка не превращается в отчёт, который никто не читает. На старте не обязательно блокировать релиз по каждой находке: многие CVE в базовых образах не эксплуатируемы в контексте конкретного приложения. Разумный первый шаг — блокировать только CRITICAL, остальное собирать в отчёт и разбирать по расписанию.

Не храните секреты в переменных окружения открытым текстом

environment: в docker-compose и флаг -e — самый частый способ передать пароль в контейнер, и самый ненадёжный: значение видно через docker inspect, попадает в ps aux на момент запуска и оседает в логах мониторинга. Это отдельная большая тема с конкретными механизмами (Docker secrets, bind-mount файлов с правами 600, интеграция с Vault) — подробный разбор с примерами для Swarm и обычного compose есть в статье Docker secrets: управление паролями. Здесь важно одно правило: если секрет виден в выводе docker inspect my_container --format='{{json .Config.Env}}' — он не защищён, вне зависимости от того, насколько сложный пароль вы придумали.

Read-only файловая система там, где приложению не нужно писать

Большинству процессов внутри контейнера не нужно менять файлы образа во время работы — им нужен только каталог для временных файлов, логов или кэша. Запрет записи в остальную файловую систему не даёт эксплойту закрепиться, подменить бинарник или дописать вредоносный код в рантайме.

docker run --read-only --tmpfs /tmp -d myimage

В compose с явным указанием, куда разрешена запись:

services:
  app:
    image: myimage
    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - app_data:/app/data

Каталоги, куда приложению реально нужна запись (логи, кэш, аплоады), оставляйте как обычные volume или bind-mount — read_only: true их не затрагивает. Проверить, что приложение вообще переживёт такой режим, стоит на стейджинге: некоторые фреймворки без предупреждения пишут временные файлы не в /tmp, а куда-то в /app или /var/cache, и это вскроется только логами вида EROFS: read-only file system.

Обновляйте базовые образы регулярно

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

Практика, которая работает без лишней автоматизации:

  • Пересобирайте образы на регулярной основе (еженедельно для внешних сервисов, ежемесячно — для внутренних), даже если код не менялся: docker build --no-cache -t myapp:$(date +%Y%m%d) .
  • Используйте минимальные базовые образы (alpine, slim, distroless) — меньше пакетов, меньше поверхность для CVE.
  • Пиньте базовый образ по digest в критичных сервисах, а обновление делайте осознанным PR, а не случайной подменой тега:
  FROM node:20-slim@sha256:abcd1234...
  • Инструменты вроде Renovate или Dependabot умеют сами открывать PR при выходе новой версии базового образа — это снимает ручной мониторинг, но финальное решение обновлять всё равно должно приниматься осознанно, а не автомёрджем на проде без проверки.

Автоматические обновления запущенных контейнеров (watchtower и аналоги) на проде — спорная практика: они экономят время, но могут выкатить breaking change среди ночи без возможности откатиться быстро. Разумнее держать пересборку образов регулярной, а обновление запущенных сервисов — контролируемым релизом, отдельным от настройки docker-compose для продакшена.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

С чего начать, если ничего из списка ещё не сделано?

С двух пунктов, которые дают больше всего защиты за меньше всего усилий: добавить USER в Dockerfile и убрать монтирование docker.sock там, где оно не обязательно. Остальное можно внедрять постепенно.

Обязательно ли использовать и Docker Scout, и Trivy?

Нет, любого из них достаточно. Docker Scout удобнее, если вы уже в экосистеме Docker Desktop/CLI, Trivy — если нужен инструмент без привязки к демону Docker, например для сканирования в CI на чистом раннере.

--read-only ломает контейнер — что делать?

Значит, приложению нужна запись куда-то ещё, кроме /tmp. Найдите каталог по логу ошибки read-only file system и добавьте под него отдельный volume или tmpfs, не отключая read-only целиком.

Rootless Docker — это то же самое, что запуск контейнера не от root?

Нет, это разные уровни. USER в Dockerfile меняет, от кого работает процесс внутри контейнера. Rootless-режим меняет, от кого работает сам демон Docker на хосте — это защищает от эскалации через сам демон, а не только через приложение внутри контейнера.

Нужно ли делать всё это сразу на одном сервере?

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