Контейнер — не виртуальная машина. Между контейнером и хостом тонкая стена: общее ядро, общие ресурсы, минимальная изоляция по умолчанию. Стандартный 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 --printRead-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 myimageDocker по умолчанию применяет профиль 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: 200Docker из коробки даёт минимальную изоляцию и этого недостаточно.
Базовые меры которые должны быть всегда:
Непривилегированный пользователь (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: обзор последних изменений требований и ответственности». Записаться
