Контейнер — не виртуальная машина. Между контейнером и хостом тонкая стена: общее ядро, общие ресурсы, минимальная изоляция по умолчанию. Стандартный 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 нужны:

  1. Запустить с --cap-drop=ALL

  2. Если падает — смотреть логи и dmesg

  3. Добавлять по одной 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"
    }
  ]
}

Генерация профиля под приложение:

  1. Запустить с профилем в режиме логирования (не блокировки)

  2. Собрать какие syscalls использует приложение

  3. Создать 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: обзор последних изменений требований и ответственности». Записаться