Усиление безопасности Docker: Rootless-режим, Seccomp, AppArmor на VPS

13 мин чтения·Matthieu·vpscontainersapparmorseccomprootless-dockersecuritydocker|

Семь уровней защиты для 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:

  1. Скопируйте стандартный seccomp-профиль Docker как отправную точку.
  2. Запустите контейнер с этим скопированным профилем. Он ведёт себя идентично стандартному.
  3. Убирайте syscall по одной группе за раз (например, все key* syscall, все swap* syscall).
  4. Тестируйте контейнер после каждого удаления. Если ломается, верните последний убранный syscall.
  5. Повторяйте, пока не уберёте всё, что вашему приложению не нужно.

Это медленнее метода 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