Усиление безопасности Docker: Rootless-режим, Seccomp, AppArmor на VPS
Семь уровней защиты для Docker на VPS. Каждый раздел объясняет угрозу, показывает исправление через CLI и Compose, и проверяет результат.
Стандартная конфигурация Docker жертвует безопасностью ради удобства. Контейнеры работают от root на хосте. Все 14 Linux capabilities остаются активными. Seccomp блокирует только 44 из 300+ системных вызовов. Трафик между контейнерами проходит свободно.
На VPS это важнее, чем на локальной машине разработчика. Вы делите физический сервер с другими арендаторами. Побег из контейнера означает, что атакующий получает root-доступ к ядру, обращённому к гипервизору. Каждый дополнительный уровень защиты уменьшает радиус поражения.
Этот туториал покрывает семь мер по усилению безопасности. Каждый раздел объясняет угрозу, показывает реализацию (флаги docker run и синтаксис Compose) и включает шаг проверки. Все команды протестированы на Ubuntu 24.04 с Docker Engine 29.x.
Требования: VPS с Debian 12 или Ubuntu 24.04 и установленным Docker Engine. SSH-доступ от обычного пользователя с sudo. Если вы ещё не защитили сам хост, начните с нашего руководства по безопасности Linux VPS. По проблемам с файрволом Docker смотрите Docker UFW Firewall Fix.
Эта статья входит в серию .
Как настроить rootless Docker на Ubuntu 24.04 или Debian 12?
Rootless Docker запускает демон и все контейнеры под обычной учётной записью пользователя вместо root. Если атакующий выберется из контейнера, он окажется непривилегированным пользователем на хосте. Никакого root-доступа. Из всех мер в этом руководстве эта даёт наибольший эффект.
Установка rootless Docker
Установите необходимые пакеты. Пакет uidmap предоставляет newuidmap и newgidmap, которые отвечают за маппинг subordinate UID/GID:
sudo apt-get update && sudo apt-get install -y uidmap docker-ce-rootless-extras
Проверьте, что у вашего пользователя есть как минимум 65 536 subordinate UID и GID:
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid
Вы должны увидеть вывод вроде deploy:100000:65536. Если записей нет, добавьте их:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
Остановите системный Docker-демон. Для rootless-режима он не нужен:
sudo systemctl disable --now docker.service docker.socket
Запустите инструмент настройки rootless от обычного пользователя (не от root):
dockerd-rootless-setuptool.sh install
Скрипт выведет переменные окружения, которые нужно задать. Добавьте их в профиль шелла:
echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc
Включите linger, чтобы rootless-демон запускался при загрузке системы, а не только при входе пользователя:
sudo loginctl enable-linger $(whoami)
Проверка rootless-режима
docker context use rootless
docker run --rm hello-world
Убедитесь, что процесс демона запущен от вашего пользователя, а не от root:
ps aux | grep dockerd
В выводе должно быть ваше имя пользователя, а не root. Также подтвердите, что Docker info сообщает о rootless:
docker info --format '{{.SecurityOptions}}'
Вы должны увидеть rootless в списке.
Когда rootless Docker ломает работу
У rootless-режима есть реальные ограничения. Зная их, вы сэкономите часы отладки.
| Ограничение | Причина | Обходной путь |
|---|---|---|
| Нельзя занимать порты ниже 1024 | Непривилегированные пользователи не могут биндить привилегированные порты | Задайте sysctl net.ipv4.ip_unprivileged_port_start=0 или используйте reverse proxy на хосте |
| Ошибки прав при bind mount | Файлы хоста, принадлежащие root, недоступны для remapped UID | Смените владельца на вашего пользователя или используйте named volumes |
| Медленная overlay-файловая система | Rootless использует fuse-overlayfs вместо нативного overlay2 |
Смиритесь с накладными расходами (5-15% для I/O-интенсивных нагрузок) или используйте нативный overlay2 с --privileged (сводит на нет весь смысл) |
Нет --net=host |
Rootless-сеть использует slirp4netns или pasta, а не сетевой стек хоста | Используйте маппинг портов (-p). Для лучшей производительности установите pasta как сетевой драйвер |
ping не работает в контейнерах |
CAP_NET_RAW ограничен |
Установите slirp4netns >= 0.4.0 или используйте pasta |
Заметка о производительности сети: По умолчанию rootless Docker использует slirp4netns для сети, что добавляет NAT-оверхед. Драйвер pasta копирует сетевую конфигурацию хоста в пространство имён контейнера без NAT и обеспечивает лучшую пропускную способность. На Debian 12 и Ubuntu 24.04 установите его:
sudo apt-get install -y passt
Docker автоматически подхватит pasta, если он установлен.
Когда использовать user namespace remapping вместо rootless Docker?
User namespace remapping (userns-remap) мапит UID 0 внутри контейнеров на непривилегированный UID на хосте. В отличие от rootless Docker, сам демон продолжает работать от root. Это значит, что вы сохраняете полную функциональность Docker (привилегированные порты, host networking, нативный overlay2), но при этом предотвращаете ситуацию, когда root контейнера равен root хоста.
Выбирайте userns-remap, когда rootless-режим ломает ваш рабочий процесс, но вам нужна UID-изоляция. Выбирайте rootless, когда можете жить с его ограничениями.
| Фича | Rootless Docker | userns-remap | Стандартный Docker |
|---|---|---|---|
| Демон работает от | Пользователя | Root | Root |
| Root контейнера = root хоста | Нет | Нет | Да |
| Привилегированные порты | Нужен обходной путь | Работает | Работает |
--net=host |
Нет | Да | Да |
| Драйвер хранилища | fuse-overlayfs | overlay2 | overlay2 |
| Сложность настройки | Средняя | Низкая | Нет |
Настройка userns-remap
Создайте пользователя dockremap или используйте сокращение default, которое создаст его автоматически:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"userns-remap": "default"
}
EOF
sudo systemctl restart docker
Docker создаст пользователя dockremap и добавит диапазоны subordinate UID/GID в /etc/subuid и /etc/subgid.
Проверьте, что работает:
sudo ls -ld /var/lib/docker/
Вы должны увидеть новый подкаталог с именем по remapped UID-диапазону, например /var/lib/docker/100000.100000/. Точное число зависит от subordinate UID-диапазона, назначенного пользователю dockremap в /etc/subuid.
Запустите контейнер и проверьте UID процесса на хосте:
docker run -d --name test-userns nginx:alpine
ps aux | grep nginx
Процесс nginx должен показывать высокий UID (соответствующий первому числу из /etc/subuid для dockremap), а не 0.
Очистка:
docker rm -f test-userns
Подводный камень с томами: Файлы bind mount, принадлежащие host root (UID 0), отображаются как nobody внутри контейнера, потому что UID 0 мапится в другой диапазон. Используйте named volumes или chown файлов на remapped UID.
Как сбросить Linux capabilities у Docker-контейнера?
Docker по умолчанию даёт контейнерам 14 Linux capabilities. Каждый capability представляет собой разрешение ядра, которое атакующий может использовать после побега из контейнера или внутри скомпрометированного контейнера. Сброс всех capabilities и добавление обратно только тех, что нужны приложению, сокращает поверхность атаки.
Capabilities Docker по умолчанию
| Capability | Что разрешает | Оставить или убрать? |
|---|---|---|
CAP_CHOWN |
Смена владельца файлов | Убрать, если не нужен |
CAP_DAC_OVERRIDE |
Обход проверок прав чтения/записи | Убрать, если не нужен |
CAP_FOWNER |
Обход проверок прав владельца файла | Убрать, если не нужен |
CAP_FSETID |
Установка setuid/setgid битов | Убрать |
CAP_KILL |
Отправка сигналов любому процессу | Убрать, если не нужен |
CAP_SETGID |
Смена GID процесса | Оставить для большинства приложений |
CAP_SETUID |
Смена UID процесса | Оставить для большинства приложений |
CAP_SETPCAP |
Изменение capabilities процесса | Убрать |
CAP_NET_BIND_SERVICE |
Привязка к портам ниже 1024 | Оставить, если биндите порт 80/443 |
CAP_NET_RAW |
Использование raw sockets (создание пакетов) | Убрать, если не нужен ping/traceroute |
CAP_SYS_CHROOT |
Использование chroot | Убрать |
CAP_MKNOD |
Создание файлов устройств | Убрать |
CAP_AUDIT_WRITE |
Запись в журнал аудита ядра | Убрать, если не нужен |
CAP_SETFCAP |
Установка file capabilities | Убрать |
Сбросить всё, вернуть нужное
Синтаксис CLI:
docker run -d \
--cap-drop ALL \
--cap-add CHOWN \
--cap-add NET_BIND_SERVICE \
--cap-add SETUID \
--cap-add SETGID \
--name hardened-nginx \
nginx:alpine
Nginx нужен CHOWN, потому что его entrypoint-скрипт меняет владельца директорий кеша при запуске. Без него контейнер тут же падает с ошибкой chown: Operation not permitted.
Синтаксис Compose:
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
Проверка сброса capabilities
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep Cap'
Сравните битовую маску CapEff (effective capabilities). Со всеми сброшенными и четырьмя добавленными обратно значение значительно меньше, чем 00000000a80425fb по умолчанию.
Для читаемого вывода установите capsh на хосте и декодируйте hex:
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep CapEff' | awk '{print $2}' | xargs -I{} capsh --decode=0x{}
Вы должны увидеть в выводе только cap_chown,cap_setgid,cap_setuid,cap_net_bind_service.
Очистка:
docker rm -f hardened-nginx
Что предотвращает флаг no-new-privileges?
Флаг no-new-privileges запрещает процессам внутри контейнера получать дополнительные привилегии через setuid или setgid бинарники. Без этого флага скомпрометированный процесс может выполнить setuid-бинарник (вроде su или sudo) и повысить привилегии до root. С установленным флагом ядро откажет в повышении.
Применение к отдельному контейнеру
CLI:
docker run -d --security-opt no-new-privileges:true --name no-priv-test nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- no-new-privileges:true
Применение как настройка демона по умолчанию
Добавьте в daemon.json, чтобы каждый контейнер автоматически получал этот флаг:
{
"no-new-privileges": true
}
Перезапустите Docker после редактирования:
sudo systemctl restart docker
Проверка работы
docker exec no-priv-test grep NoNewPrivs /proc/1/status
Ожидаемый вывод:
NoNewPrivs: 1
Значение 1 означает, что получение новых привилегий заблокировано. Значение 0 означает, что флаг не установлен.
Очистка:
docker rm -f no-priv-test
Как создать кастомный seccomp-профиль для Docker?
Стандартный seccomp-профиль Docker блокирует около 44 системных вызовов из 300+. Кастомный профиль позволяет ограничить контейнеры только теми syscall, которые реально использует ваше приложение. Если атакующий скомпрометирует контейнер, он не сможет эксплуатировать уязвимости ядра через заблокированные syscall.
Определение нужных syscall
Используйте strace на запущенном контейнере, чтобы перехватить syscall во время нормальной работы:
docker run -d --security-opt seccomp=unconfined --name trace-target nginx:alpine
# Install strace on the host
sudo apt-get install -y strace
# Get the container's PID
PID=$(docker inspect --format '{{.State.Pid}}' trace-target)
# Trace syscalls for 30 seconds during normal operation
sudo strace -f -p "$PID" -o /tmp/nginx-syscalls.log -e trace=all &
STRACE_PID=$!
sleep 30
# Send some test traffic to exercise the application
curl -s http://localhost:80 > /dev/null 2>&1 || true
kill "$STRACE_PID" 2>/dev/null
wait "$STRACE_PID" 2>/dev/null
Извлеките уникальные имена syscall:
grep -oP '^\[pid \d+\] \K\w+|^\w+' /tmp/nginx-syscalls.log | sort -u
Это даёт минимальный набор syscall, нужных вашему приложению.
Создание кастомного профиля
Создайте JSON-файл. defaultAction установлен в SCMP_ACT_ERRNO (запрещать всё, что явно не разрешено). Список syscall должен включать и то, что нужно вашему приложению, И то, что нужно рантайму контейнера (runc) во время инициализации. Профиль ниже протестирован с nginx:alpine на Docker Engine 29.x:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4",
"access",
"arch_prctl",
"bind",
"brk",
"capget",
"capset",
"chdir",
"chown",
"clone",
"clone3",
"close",
"close_range",
"connect",
"copy_file_range",
"dup",
"dup2",
"dup3",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"epoll_pwait2",
"epoll_wait",
"eventfd2",
"execve",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fchmod",
"fchmodat",
"fchown",
"fchownat",
"fcntl",
"fork",
"fstat",
"fstatfs",
"futex",
"getcwd",
"getdents",
"getdents64",
"getegid",
"geteuid",
"getgid",
"getpgrp",
"getpid",
"getppid",
"getrandom",
"getrlimit",
"getsockname",
"getsockopt",
"gettid",
"getuid",
"io_destroy",
"io_getevents",
"io_setup",
"io_submit",
"ioctl",
"kill",
"listen",
"lseek",
"lstat",
"madvise",
"memfd_create",
"mkdir",
"mkdirat",
"mmap",
"mount",
"mprotect",
"mremap",
"munmap",
"nanosleep",
"newfstatat",
"open",
"openat",
"pipe",
"pipe2",
"pivot_root",
"poll",
"ppoll",
"prctl",
"pread64",
"prlimit64",
"pwrite64",
"read",
"readlink",
"readlinkat",
"recvfrom",
"recvmsg",
"rename",
"renameat",
"rseq",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_getaffinity",
"sched_yield",
"seccomp",
"sendfile",
"sendmsg",
"sendto",
"set_robust_list",
"set_tid_address",
"setgid",
"setgroups",
"sethostname",
"setitimer",
"setsockopt",
"setuid",
"sigaltstack",
"socket",
"socketpair",
"stat",
"statfs",
"statx",
"symlink",
"symlinkat",
"sysinfo",
"tgkill",
"umask",
"umount2",
"uname",
"unlink",
"unlinkat",
"unshare",
"utimensat",
"wait4",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
Почему так много syscall? Список включает syscall, нужные трём уровням: рантайму контейнера (runc) для настройки namespace (clone3, mount, pivot_root, unshare, seccomp, statx), шеллу Alpine и entrypoint-скриптам (fork, open, pipe, wait4) и самому nginx (accept4, bind, listen, io_setup). Образ на glibc потребовал бы меньше shell-related syscall, но больше внутренних libc-вызовов.
Сохраните как /etc/docker/seccomp-nginx.json.
Применение кастомного профиля
CLI:
docker run -d \
--security-opt seccomp=/etc/docker/seccomp-nginx.json \
--name seccomp-test \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- seccomp=/etc/docker/seccomp-nginx.json
Проверка активности профиля
docker inspect --format '{{.HostConfig.SecurityOpt}}' seccomp-test
Вывод показывает полный JSON seccomp-профиля, применённого к контейнеру. Docker читает файл при создании контейнера и встраивает содержимое профиля.
Проверьте, что ограниченная операция не проходит:
docker exec seccomp-test unshare --mount /bin/sh -c 'echo escaped'
Это должно завершиться с ошибкой "Operation not permitted". У контейнера нет CAP_SYS_ADMIN (не предоставляется по умолчанию), а seccomp-профиль обеспечивает второй уровень защиты, разрешая только syscall, нужные для нормальной работы.
Очистка:
docker rm -f seccomp-test
rm -f /tmp/nginx-syscalls.log
Совет для продакшена: Начните со стандартного профиля Docker и убирайте syscall, а не стройте с нуля. Подход через strace даёт максимально жёсткий профиль, но требует тщательного тестирования. Во время захвата strace задействуйте все пути кода вашего приложения: запуск, обычные запросы, обработку ошибок, корректную остановку (docker stop) и ротацию логов.
Итеративный подход: Если создание профиля через strace кажется рискованным, используйте более безопасный workflow:
- Скопируйте стандартный seccomp-профиль Docker как отправную точку.
- Запустите контейнер с этим скопированным профилем. Он ведёт себя идентично стандартному.
- Убирайте syscall по одной группе за раз (например, все
key*syscall, всеswap*syscall). - Тестируйте контейнер после каждого удаления. Если ломается, верните последний убранный syscall.
- Повторяйте, пока не уберёте всё, что вашему приложению не нужно.
Это медленнее метода strace, но безопаснее для продакшен-контейнеров, где пропущенный syscall может вызвать периодические сбои.
Как написать AppArmor-профиль для Docker-контейнера?
AppArmor ограничивает доступ процесса контейнера к файлам, сетевым ресурсам и capabilities. Docker автоматически применяет стандартный профиль docker-default. Кастомный профиль позволяет дополнительно ограничить контейнеры только теми путями файловой системы и сетевыми операциями, которые им нужны.
Проверка активности AppArmor
sudo aa-status
Вы должны увидеть docker-default в списке загруженных профилей. Если AppArmor не установлен:
sudo apt-get install -y apparmor apparmor-utils
Создание кастомного профиля
Создайте /etc/apparmor.d/containers/docker-nginx:
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/nameservice>
# Capabilities needed by nginx
capability chown,
capability setuid,
capability setgid,
capability net_bind_service,
capability dac_override,
# Network access
network inet tcp,
network inet udp,
network inet6 tcp,
network inet6 udp,
# Shell and entrypoint scripts
/bin/** rix,
/usr/bin/** rix,
/usr/sbin/** rix,
/lib/** mr,
/usr/lib/** mr,
/docker-entrypoint.sh rix,
/docker-entrypoint.d/ r,
/docker-entrypoint.d/** rix,
/dev/null rw,
/dev/stdout rw,
/dev/stderr rw,
# Nginx binary
/usr/sbin/nginx ix,
# Config files (read only)
/etc/ r,
/etc/nginx/ r,
/etc/nginx/** r,
# Web root (read only)
/usr/share/nginx/html/** r,
# Temp and cache directories
/var/cache/nginx/ rw,
/var/cache/nginx/** rw,
/var/log/nginx/ rw,
/var/log/nginx/** rw,
/run/ rw,
/run/** rw,
/tmp/ rw,
/tmp/** rw,
# Proc filesystem (needed for nginx worker management)
/proc/** r,
# Deny sensitive files
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem r,
deny /sys/** w,
}
Профилю нужны декларации capability, потому что AppArmor контролирует использование capabilities независимо от флагов Docker --cap-add. Пути entrypoint-скриптов (/docker-entrypoint.sh, /docker-entrypoint.d/) и бинарники шелла (/bin/**) должны быть явно разрешены, иначе контейнер не запустится. Разрешение rix означает чтение, наследование контекста выполнения и разрешение на запуск.
Загрузка и применение профиля
sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
Проверьте, что профиль загружен:
sudo aa-status | grep docker-nginx
Запустите контейнер с кастомным профилем:
CLI:
docker run -d \
--security-opt apparmor=docker-nginx \
--name apparmor-test \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- apparmor=docker-nginx
Проверка применения AppArmor
docker exec apparmor-test cat /etc/shadow
Это должно вернуть "Permission denied", потому что профиль явно запрещает чтение /etc/shadow.
Проверьте статус AppArmor контейнера:
docker inspect --format '{{.AppArmorProfile}}' apparmor-test
Ожидаемый вывод: docker-nginx.
Очистка:
docker rm -f apparmor-test
Как запустить Docker-контейнер с read-only файловой системой?
Read-only корневая файловая система не даёт атакующим записать малварь, бэкдоры или модифицированные бинарники внутри скомпрометированного контейнера. Контейнер по-прежнему может писать в явно смонтированные tmpfs-тома для временных файлов и данных рантайма.
CLI:
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /run:rw,noexec,nosuid,size=16m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=128m \
-p 8080:80 \
--name readonly-nginx \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
Обратите внимание на флаг noexec у tmpfs-маунтов. Он запрещает выполнение бинарников из временных директорий. Это распространённый приём, который используют атакующие после получения доступа на запись.
Проверка read-only файловой системы
docker exec readonly-nginx touch /testfile
Ожидаемый вывод:
touch: /testfile: Read-only file system
Убедитесь, что tmpfs-маунты работают:
docker exec readonly-nginx touch /tmp/testfile && echo "tmpfs works"
Проверьте, что контейнер обслуживает трафик:
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080
Ожидаемый результат: 200.
Очистка:
docker rm -f readonly-nginx
Как должен выглядеть усиленный Docker daemon.json?
Усиленный daemon.json применяет настройки безопасности по умолчанию ко всем контейнерам на хосте. Отдельные контейнеры могут переопределять некоторые параметры, но конфигурация демона задаёт базовый уровень.
Создайте или отредактируйте /etc/docker/daemon.json:
{
"no-new-privileges": true,
"icc": false,
"live-restore": true,
"userland-proxy": false,
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}"
},
"default-ulimits": {
"nproc": {
"Name": "nproc",
"Hard": 512,
"Soft": 256
},
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 32768
}
},
"storage-driver": "overlay2"
}
Что делает каждая настройка:
no-new-privileges: блокирует setuid/setgid-эскалацию во всех контейнерах по умолчанию.icc: false: отключает межконтейнерное взаимодействие в стандартной bridge-сети. Контейнеры могут связываться друг с другом только через явно опубликованные порты или пользовательские сети. Это ограничивает латеральное перемещение при компрометации одного контейнера.live-restore: сохраняет контейнеры запущенными при перезапуске демона. Предотвращает простой при обновлении Docker.userland-proxy: false: использует iptables для маппинга портов вместо userland proxy-процесса. Лучшая производительность, меньше открытых файловых дескрипторов.log-driver: journald: отправляет логи контейнеров в системный journal, где они централизованно управляются и ротируются.default-ulimits: ограничивает процессы и открытые файлы на контейнер. Защищает от fork bomb и исчерпания файловых дескрипторов.
Примените изменения:
sudo systemctl restart docker
Убедитесь, что демон подхватил конфигурацию:
docker info --format '{{.SecurityOptions}}'
Вы должны увидеть no-new-privileges в списке security options.
Проверьте, что ICC отключён:
docker network inspect bridge --format '{{index .Options "com.docker.network.bridge.enable_icc"}}'
Ожидаемый вывод: false.
Заметка о userns-remap: Если вы выбрали user namespace remapping вместо rootless-режима, добавьте "userns-remap": "default" в эту конфигурацию. Не комбинируйте userns-remap с rootless Docker.
Скрытие версий: Пока редактируете daemon.json, стоит подумать о скрытии версии Docker API. Docker не раскрывает заголовки с версией по умолчанию, как это делает Nginx, но если вы открываете Docker API на TCP-сокет (не делайте так, если нет острой необходимости), защитите его TLS-сертификатами клиента. Открытый Docker API эквивалентен root shell-доступу.
Аудит настроек: Запустите Docker Bench for Security для аудита конфигурации по CIS Docker Benchmark. Клонируйте репозиторий и запустите скрипт напрямую, потому что контейнерный образ поставляется с устаревшим Docker-клиентом, несовместимым с Docker Engine 29.x:
git clone https://github.com/docker/docker-bench-security.git /tmp/docker-bench
cd /tmp/docker-bench && sudo bash docker-bench-security.sh
Изучите вывод на предмет записей WARN. Усиленный daemon.json выше закрывает большинство из них. Записи INFO носят рекомендательный характер, это не ошибки.
Комбинирование нескольких уровней защиты в Docker Compose
Продакшен Compose-файл должен совмещать несколько мер защиты. Вот пример для Nginx-контейнера со всеми семью уровнями:
services:
web:
image: nginx:alpine
read_only: true
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
security_opt:
- no-new-privileges:true
- seccomp=/etc/docker/seccomp-nginx.json
- apparmor=docker-nginx
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
ports:
- "80:80"
pids_limit: 100
deploy:
resources:
limits:
memory: 256M
cpus: '1.0'
Обратите внимание: pids_limit защищает от fork bomb, а deploy.resources.limits ограничивает память и CPU. Это не security-opt флаги, но они предотвращают отказ в обслуживании изнутри контейнера. Подробнее о лимитах ресурсов смотрите .
Проверьте, что все security options активны на запущенном контейнере:
docker compose up -d
docker inspect web --format '{{json .HostConfig.SecurityOpt}}' | python3 -m json.tool
Вы должны увидеть no-new-privileges, путь к seccomp-профилю и имя AppArmor-профиля.
Чеклист усиления безопасности
| Мера | Флаг CLI | Ключ Compose | daemon.json | Предотвращаемая угроза |
|---|---|---|---|---|
| Rootless Docker | N/A (уровень демона) | N/A | N/A (отдельный демон) | Побег из контейнера даёт host root |
| userns-remap | N/A | N/A | "userns-remap": "default" |
Root контейнера = root хоста |
| Сброс capabilities | --cap-drop ALL --cap-add X |
cap_drop: [ALL] |
N/A | Поверхность атаки ядра |
| no-new-privileges | --security-opt no-new-privileges:true |
security_opt: [no-new-privileges:true] |
"no-new-privileges": true |
Setuid/setgid-эскалация |
| Кастомный seccomp | --security-opt seccomp=profile.json |
security_opt: [seccomp:path] |
N/A | Эксплойт ядра через заблокированные syscall |
| AppArmor-профиль | --security-opt apparmor=name |
security_opt: [apparmor:name] |
N/A | Файловый/сетевой доступ за пределами нужд приложения |
| Read-only rootfs | --read-only |
read_only: true |
N/A | Персистентная малварь, подмена бинарников |
| Отключение ICC | N/A | N/A | "icc": false |
Латеральное перемещение между контейнерами |
| Лимит процессов | --pids-limit 100 |
pids_limit: 100 |
"default-pids-limit": 100 |
Fork bomb |
| Отключение userland proxy | N/A | N/A | "userland-proxy": false |
Расход ресурсов, уменьшение поверхности атаки |
Решение проблем
Контейнер не запускается после добавления seccomp-профиля:
В вашем профиле не хватает syscall, нужного приложению. Временно запустите с --security-opt seccomp=unconfined, снимите strace с процесса и добавьте недостающие syscall в профиль.
Проверьте журнал аудита ядра на заблокированные syscall:
sudo journalctl -k | grep -i seccomp
AppArmor блокирует легитимные операции: Переключите профиль в режим жалоб (complain mode), чтобы логировать запреты без применения:
sudo aa-complain /etc/apparmor.d/containers/docker-nginx
Наблюдайте за логами:
sudo journalctl -k | grep apparmor | tail -20
После добавления нужных разрешений переключитесь обратно в enforce mode:
sudo aa-enforce /etc/apparmor.d/containers/docker-nginx
Rootless Docker не может стянуть образы:
Проверьте, что DOCKER_HOST установлен корректно:
echo $DOCKER_HOST
Он должен указывать на /run/user/<UID>/docker.sock. Также убедитесь, что rootless-демон запущен:
systemctl --user status docker
Permission denied при bind mount с userns-remap: Файлы на хосте, принадлежащие root (UID 0), недоступны, потому что UID 0 контейнера мапится на высокий UID хоста. Исправьте, сменив владельца:
# Find the remapped UID
grep dockremap /etc/subuid
# Then chown to that UID
sudo chown -R <remapped-uid>:<remapped-uid> /path/to/bind/mount
Замените <remapped-uid> на первое число из /etc/subuid для пользователя dockremap (обычно 100000).
Логи контейнеров: При любых проблемах с Docker начинайте с логов контейнера и журнала демона Docker:
docker logs <container-name>
sudo journalctl -u docker -f
Для rootless Docker проверьте пользовательский журнал:
journalctl --user -u docker -f
Авторское право 2026 Virtua.Cloud. Все права защищены. Данный контент является оригинальным произведением команды Virtua.Cloud. Воспроизведение, повторная публикация или распространение без письменного разрешения запрещены.
Готовы попробовать?
Разверните свой сервер за секунды. Linux, Windows или FreeBSD.
Смотреть тарифы VPS