6. Безопасность контейнеров

От сброса Capabilities и Seccomp до защиты Supply Chain в Go

В инфраструктурной среде до сих пор циркулирует опасная иллюзия: «Контейнер надежно изолирует сервис, поэтому внутри него можно делать всё что угодно». Как мы детально выяснили в предыдущих лекциях, контейнеры — это не герметичные виртуальные машины. Это обычные процессы операционной системы с урезанной видимостью (Namespaces) и лимитами ресурсов (Cgroups), разделяющие единое монолитное ядро Linux с хостом.

Если атакующий обнаружит уязвимость нулевого дня (0-day) в коде ядра или воспользуется ошибкой конфигурации контейнера, процесс может совершить побег (Container Escape) и получить неограниченный контроль над физическим сервером, базой данных и всеми соседними сервисами. Безопасность контейнеров строится не на надежде на изоляцию, а на принципе глубоко эшелонированной обороны (Defense in Depth).


1. Грех первый: Запуск от Root

В классической модели безопасности Unix идентификатор пользователя UID 0 обладает абсолютной и неограниченной властью над операционной системой.

По умолчанию демон Docker запускает контейнеры с привилегиями root. Даже если процесс помещен в изолированное пространство PID Namespace, для ядра хоста его эффективный идентификатор пользователя (EUID) равен 0.

Если злоумышленник через уязвимость удаленного исполнения кода (RCE) в Go-бэкенде или небезопасный системный вызов захватит контроль над контейнером, он немедленно окажется root-пользователем:

  • Имея доступ к смонтированным томам, он сможет перезаписать критические системные файлы хоста (например, /etc/crontab или /etc/shadow).
  • Он получит возможность взаимодействовать с системным сокетом ядра и атаковать недокументированные системные вызовы.

2. Linux Capabilities: Разделение полномочий суперпользователя

В традиционном Unix привилегии были бинарными: либо процесс обычный пользователь, либо всемогущий root. Начиная с версии ядра 2.2, в Linux появился механизм Linux Capabilities (Привилегии), который разделил права root на десятки независимых атомарных разрешений.

По умолчанию Docker наделяет вновь созданный контейнер набором из ~14 привилегий, включая CAP_NET_RAW, CAP_CHOWN, CAP_MKNOD, CAP_FOWNER.

Однако типичному Go-микросервису, который слушает непривилегированный HTTP-порт (например, 8080) и общается с базой данных, эти привилегии абсолютно не нужны! Наличие CAP_NET_RAW, например, позволяет скомпрометированному контейнеру генерировать сырые Ethernet-фреймы, проводить ARP-спуфинг внутри Docker-сети и перехватывать трафик соседних сервисов.

Безопасный запуск по принципу наименьших привилегий (Least Privilege)

Лучшая практика безопасности — сначала сбросить абсолютно все привилегии, а затем явно выдать только минимально необходимые:

Bash
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE my-go-app

Флаг --cap-drop ALL превращает процесс в абсолютно безобидного обывателя ОС, а --cap-add NET_BIND_SERVICE при необходимости разрешает забиндить стандартный 80 или 443 порт.


3. Фильтрация системных вызовов: Seccomp (Secure Computing Mode)

Ядро современного Linux поддерживает около 350 системных вызовов. Среди них есть мощные и опасные инструменты администрирования: ptrace (отладка и инспекция памяти чужих процессов), reboot (перезагрузка сервера), mount (монтирование блочных устройств) или kexec_load.

Механизм Seccomp (Secure Computing Mode) ядра Linux позволяет сформировать декларативный BPF-фильтр системных вызовов, блокирующий доступ к неиспользуемым интерфейсам ядра.

Рантайм Go невероятно компактен и предсказуем: он использует системные вызовы для сетевого поллера (epoll_create1, epoll_ctl, epoll_wait), ввода-вывода (read, write), управления памятью (mmap, madvise) и планировщика горутин (clone, futex). Ни один корректный Go-бэкенд никогда не вызывает ptrace или reboot!

По умолчанию Docker использует профиль Seccomp, блокирующий около 44 заведомо деструктивных системных вызовов. Однако для высокозащищенных сред создаются жесткие кастомные профили (Whitelists), разрешающие только строго необходимый список вызовов для Go.


4. Специфика Go: Статическая линковка против CGO

Архитектура языка Go дарует разработчику уникальные преимущества в области безопасности, недоступные в Python, Node.js или традиционном C++:

  1. Иммунитет к атакам через LD_PRELOAD: В динамически компонуемых приложениях (C/Python) злоумышленник, получивший доступ к файловой системе, может подложить вредоносную динамическую библиотеку libc.so.6 или переопределить переменную LD_PRELOAD. При следующем запуске динамический компоновщик хоста послушно загрузит код атакующего. Go-бинарник, собранный с CGO_ENABLED=0, линкуется полностью статически и использует собственный встроенный рантайм-загрузчик. Он физически игнорирует системные библиотеки и переменные LD_PRELOAD!
  2. Детерминированный профиль Syscalls: В отличие от скриптовых языков, где сторонний пакет может непредсказуемо вызвать произвольный системный интерфейс, статический Go-код чрезвычайно прост для аудита и покрытия жесткими профилями Seccomp.
  3. Образы Scratch и Distroless: Как мы разбирали в лекции 3. Multi stage build, Go-бинарник может запускаться в пустом контейнере без шелла (sh, bash), без утилит curl, wget и пакетных менеджеров. Это блокирует горизонтальное перемещение (Lateral Movement): даже если атакующий эксплуатирует уязвимость в HTTP-хендлере, вызывающем команду через os.Exec, ядро вернет ошибку, так как командной оболочки в контейнере просто не существует.

5. Безопасность цепочки поставок (Supply Chain Security)

Ваш собственный Go-код может быть безупречен и покрыт тестами на 100%. Однако современный микросервис включает сотни сторонних библиотек из открытых репозиториев (например, github.com/gorilla/websocket) и базируется на образах операционных систем (Alpine/Debian). Если в них обнаружится критическая CVE-уязвимость, весь контейнер окажется скомпрометирован.

Атака на цепочку поставок (Supply Chain Attack) — один из главных векторов современных взломов:

  1. Сканирование уязвимостей в CI/CD: В сборочный пайплайн обязательно внедряются специализированные сканеры безопасности — Trivy или Snyk:

    Bash
    trivy image --severity HIGH,CRITICAL my-registry/my-go-app:latest
    

    Сканер анализирует манифесты зависимостей (go.sum) и бинарный файл, сверяя хэши модулей с глобальными базами данных общеизвестных уязвимостей (CVE) и блокируя деплой при обнаружении критических брешей.

  2. Фиксация точных версий базовых образов: Категорически запрещено использовать плавающие теги вроде FROM golang:latest или FROM alpine:latest. В продакшене используются либо точные минорные версии дистрибутивов (FROM golang:1.22.3-alpine3.19), либо неизменяемые криптографические дайджесты SHA256 (FROM alpine@sha256:c5b1261d...).


6. Защита секретов и конфигурации

Никогда не зашивайте пароли к базам данных, приватные ключи RSA и токены API в исходный код и не передавайте их через директиву ENV в Dockerfile:

  • Переменные окружения, объявленные в Dockerfile, сохраняются в метаданных слоев образа и навсегда остаются видны через команду docker inspect любому, у кого есть доступ к registry.

Надежные способы проброса секретов:

  1. Флаг --secret и монтирование в режиме Read-Only Volume: Для передачи учетных данных при сборке используется флаг --secret в BuildKit, а для локального запуска секреты пробрасываются через файлы в защищенном временном хранилище:

    Bash
    docker run -v $(pwd)/db_password.txt:/run/secrets/db_password:ro my-go-app
    
  2. Интеграция с внешними Vault: Использование специализированных хранилищ (HashiCorp Vault, AWS Secrets Manager, Yandex Lockbox), откуда Go-сервис запрашивает секреты в память по защищенному mTLS-каналу в момент старта.

  3. Оркестрация через Kubernetes: В продакшен-кластерах управление конфиденциальными данными делегируется нативным примитивам, о которых мы поговорим в лекции 3. ConfigMap и Secret.

Итог

  1. Запрет на Root: Директива USER appuser обязательна; процесс с UID 0 в разделяемом ядре хоста представляет собой критическую угрозу.
  2. Сброс Capabilities: Комбинация --cap-drop ALL нейтрализует потенциал злоумышленника при захвате контейнера.
  3. Seccomp фильтрация: Блокирует опасные системные вызовы ядра (ptrace, reboot, mount), оставляя только предсказуемый сетевой набор Go.
  4. Статическая чистота Go: Сборка с CGO_ENABLED=0 блокирует атаки подмены динамических библиотек и устраняет уязвимости glibc.
  5. Контроль Supply Chain: Регулярное сканирование образов через Trivy и фиксация дайджестов базовых слоев защищают от скрытых CVE в сторонних зависимостях.

Мы завершили фундаментальное погружение в Docker: от пространств имен и файловых систем до боевой безопасности и сборки микрообразов. Однако запуск единичных контейнеров на отдельных хостах не решает задач автоматического масштабирования, самовосстановления и балансировки между дата-центрами.

Мы переходим к вершине инфраструктурного стека — платформе Kubernetes. В следующей лекции мы детально разберем ее внутреннюю архитектуру: 1. Kubernetes. Архитектура.