Контейнер — не виртуальная машина. Между контейнером и хостом тонкая стена: общее ядро, общие ресурсы, минимальная изоляция по умолчанию. Стандартный docker run запускает процесс с root правами внутри контейнера и доступом к большинству системных вызовов.
Большинство команд оставляют дефолтные настройки, потому что «и так работает». Пока не приходят пентестеры или не случается инцидент. Разберём конкретные настройки, которые реально повышают безопасность, с примерами и объяснением зачем это нужно.
Проблема root в контейнере
По дефолту процесс в контейнере работает от root. Внутри контейнера это root с uid 0. На хосте это тоже uid 0.
Если злоумышленник выбрался из контейнера, он получает root на хосте. Даже без escape: root в контейнере может монтировать файловые системы, загружать модули ядра (при наличии capabilities), манипулировать сетью.
Решение 1: USER в Dockerfile
FROM python:3.11-slim # Создаём непривилегированного пользователя RUN groupadd -r appgroup && useradd -r -g appgroup appuser # Копируем файлы COPY --chown=appuser:appgroup . /app WORKDIR /app # Переключаемся на пользователя USER appuser CMD ["python", "app.py"]
Процесс работает от appuser с непривилегированным uid. Даже при escape злоумышленник получает непривилегированного пользователя.
Решение 2: user namespace remapping
Настройка на уровне демона Docker. Uid 0 в контейнере маппится на непривилегированный uid на хосте.
// /etc/docker/daemon.json { "userns-remap": "default" }
systemctl restart docker
После этого root в контейнере = непривилегированный пользователь на хосте. Даже если Dockerfile не указывает USER.
Проверка:
# В контейнере id # uid=0(root) gid=0(root) # На хосте, смотрим процесс контейнера ps aux | grep <процесс> # uid=100000 — ремаппинг работает
Capabilities: гранулярные права вместо root
Linux capabilities — разбиение root-привилегий на отдельные права. Вместо бинарного «root или нет» — набор конкретных разрешений.
По дефолту Docker даёт контейнеру набор capabilities:
CAP_CHOWN — смена владельца файлов
CAP_DAC_OVERRIDE — игнорирование прав доступа
CAP_FSETID — сохранение setuid при модификации
CAP_FOWNER — игнорирование владельца файла
CAP_NET_RAW — raw сокеты (нужно для ping)
CAP_SETGID, CAP_SETUID — смена uid/gid
и другие
Большинству приложений это не нужно.
Убираем все capabilities:
docker run --cap-drop=ALL myimage
Добавляем только необходимые:
# Приложению нужен ping docker run --cap-drop=ALL --cap-add=NET_RAW myimage # Приложению нужно слушать privileged порт < 1024 docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage
В docker-compose:
services: app: image: myimage cap_drop: - ALL cap_add: - NET_BIND_SERVICE
Как узнать какие capabilities нужны:
Запустить с --cap-drop=ALL
Если падает — смотреть логи и dmesg
Добавлять по одной capability пока не заработает
Или использовать инструменты аудита:
# capsh показывает текущие capabilities docker run --rm --cap-drop=ALL myimage capsh --print
Read-only filesystem
Если приложение не пишет на диск — запретите запись.
docker run --read-only myimage
Приложению нужны временные файлы? Монтируем tmpfs:
docker run --read-only --tmpfs /tmp:rw,noexec,nosuid myimage
В docker-compose:
services: app: image: myimage read_only: true tmpfs: - /tmp:rw,noexec,nosuid - /var/run:rw,noexec,nosuid
Это предотвращает запись малвари в файловую систему, модификацию исполняемых файлов, персистентность после рестарта контейнера.
No-new-privileges: запрет эскалации
Флаг запрещает процессу получать новые привилегии через setuid, setgid, capabilities.
docker run --security-opt=no-new-privileges:true myimage
Даже если внутри контейнера есть setuid бинарник, его выполнение не даст повышенных прав.
services: app: image: myimage security_opt: - no-new-privileges:true
Это должно быть включено всегда. Нет легитимных причин для приложения поднимать привилегии в runtime.
Seccomp: фильтрация системных вызовов
Seccomp ограничивает какие системные вызовы может делать процесс.
Docker по умолчанию применяет профиль, блокирующий опасные syscalls. Но можно ужесточить.
# Применить кастомный профиль docker run --security-opt seccomp=/path/to/profile.json myimage
Пример профиля, разрешающего только базовые syscalls:
{ "defaultAction": "SCMP_ACT_ERRNO", "architectures": ["SCMP_ARCH_X86_64"], "syscalls": [ { "names": ["read", "write", "open", "close", "stat", "fstat", "mmap", "mprotect", "munmap", "brk", "rt_sigaction", "rt_sigprocmask", "ioctl", "access", "pipe", "select", "sched_yield", "mremap", "msync", "mincore", "madvise", "dup", "dup2", "nanosleep", "getpid", "socket", "connect", "accept", "sendto", "recvfrom", "bind", "listen", "getsockname", "getpeername", "socketpair", "setsockopt", "getsockopt", "clone", "fork", "execve", "exit", "wait4", "kill", "uname", "fcntl", "flock", "fsync", "fdatasync", "getcwd", "chdir", "rename", "mkdir", "rmdir", "creat", "link", "unlink", "readlink", "chmod", "chown", "lchown", "umask", "gettimeofday", "getrlimit", "getrusage", "sysinfo", "times", "getuid", "getgid", "geteuid", "getegid", "setpgid", "getppid", "getpgrp", "setsid", "setreuid", "setregid", "getgroups", "setgroups", "setresuid", "getresuid", "setresgid", "getresgid", "sigaltstack", "statfs", "fstatfs", "arch_prctl", "set_tid_address", "exit_group"], "action": "SCMP_ACT_ALLOW" } ] }
Генерация профиля под приложение:
Запустить с профилем в режиме логирования (не блокировки)
Собрать какие syscalls использует приложение
Создать whitelist
Инструменты: OCI seccomp generator, sysdig для трассировки.
AppArmor: дополнительный слой
AppArmor — система мандатного контроля доступа. Профили ограничивают что может делать программа: какие файлы читать, какие сети использовать.
docker run --security-opt apparmor=docker-nginx myimage
Docker по умолчанию применяет профиль docker-default. Можно создать строже.
Пример профиля для веб-приложения:
#include <tunables/global> profile docker-webapp flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base> # Сеть только TCP network tcp, # Чтение конфигов /etc/passwd r, /etc/group r, /app/** r, # Запись только в tmp /tmp/** rw, # Запуск только своего бинарника /app/server ix, # Запрет на всё остальное deny /proc/** rwklx, deny /sys/** rwklx, }
Ограничение ресурсов
Без лимитов контейнер может съесть все ресурсы хоста — DoS.
docker run --memory=512m --cpus=1.0 myimage
Детальнее:
services: app: image: myimage deploy: resources: limits: cpus: '1.0' memory: 512M reservations: cpus: '0.25' memory: 128M # Лимит PID — защита от fork bomb pids_limit: 100 # Лимит файловых дескрипторов ulimits: nofile: soft: 1024 hard: 2048
Сеть: изоляция и ограничения
По дефолту все контейнеры в одной сети Docker видят друг друга.
Изолированные сети:
services: frontend: networks: - frontend-net backend: networks: - frontend-net - backend-net database: networks: - backend-net networks: frontend-net: backend-net: internal: true # нет доступа наружу
Database доступен только backend, не виден frontend и не имеет выхода в интернет.
Запрет ICC (inter-container communication):
// /etc/docker/daemon.json { "icc": false }
Кон��ейнеры не могут общаться друг с другом, только через явно опубликованные порты.
Сканирование образов
Образы могут содержать уязвимости в базовых пакетах, устаревшие зависимости, захардкоженные секреты.
Trivy — бесплатный сканер:
trivy image myimage:latest
Вывод показывает CVE, severity, есть ли фикс.
Интеграция в CI:
# GitLab CI scan: stage: test image: aquasec/trivy script: - trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Билд падает если есть очень критичные уязвимости.
Сканирование на секреты:
# Trufflehog, gitleaks для поиска секретов в образе docker save myimage | trufflehog docker --json
Запускаем адекватно
Минимальный набор для production:
services: app: image: myregistry/myimage:1.0.0 user: "1000:1000" # непривилегированный пользователь read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true deploy: resources: limits: cpus: '2' memory: 1G pids_limit: 200 networks: - app-net
Расширенный набор с AppArmor и seccomp:
services: app: image: myregistry/myimage:1.0.0 user: "1000:1000" read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true - apparmor=docker-custom - seccomp=/etc/docker/seccomp/app.json deploy: resources: limits: cpus: '2' memory: 1G pids_limit: 200
Docker из коробки даёт минимальную изоляцию и этого недостаточно.
Базовые меры которые должны быть всегда:
Непривилегированный пользователь (USER в Dockerfile или docker run --user)
drop ALL capabilities, добавлять только нужные
no-new-privileges: true
Лимиты ресурсов
Изолированные сети
Плюсом можно подумать про:
Read-only filesystem
User namespace remapping
Кастомные seccomp профили
AppArmor профили
Сканирование образов в CI
Каждая мера идет как дополнительный слой. Даже если один обойдут, остальные работают.

Харднинг контейнеров быстро упирается в более широкий вопрос: как устроить ИБ системно, а не набором флагов. На курсе OTUS «Информационная безопасность. Professional» разбирают архитектуру комплексной кибербезопасности предприятия: эшелонирование, инструменты и процессы. Плюс — работа с рисками и аргументация инвестиций на языке бизнеса.
Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:
27 января в 19:00. «Docker hardening + IaC security: типовые ошибки и чек-лист безопасной инфраструктуры». Записаться
3 февраля в 20:00. «Экономика и архитектура комплексной кибербезопасности компании». Записаться
16 февраля в 20:00. «Безопасность КИИ в 2026: обзор последних изменений требований и ответственности». Записаться
