Безопасность контейнеров и Kubernetes

Контейнер — это не «лёгкая виртуалка», а процесс на вашем ядре. Если он запущен от root и привилегирован, выход из него — выход на хост.

Изоляция контейнера держится на механизмах ядра Linux: namespaces (своя файловая система, сеть, PID) и cgroups (лимиты ресурсов). Это изоляция процессов, а не полноценная граница, как у виртуальной машины, — и при плохой настройке она протекает.

Docker и Kubernetes сделали деплой простым, но «работает» и «безопасно настроено» — разные вещи. Защитнику важно понимать четыре типовых слабых места: контейнер от root, привилегированный режим, открытый наружу API оркестратора и секреты, зашитые прямо в образ. Каждое из них — это не экзотика, а настройка по умолчанию или «временный костыль», который доезжает до прода.

Зачем это знать защитнику

Контейнерная инфраструктура часто — это «мягкое подбрюшье»: периметр защищён, а внутри кластера поды бегают от root и видят чужие секреты. Понимая, как из плохо настроенного контейнера расширяют доступ, вы выстраиваете оборону: non-root, минимальные capability, RBAC, сканирование образов. Всё ниже — про свои кластеры и лабораторию (DVWA, локальная ВМ, TryHackMe). Атаковать чужую инфраструктуру нельзя (ст. 272/274 УК РФ).

Запуск от root

По умолчанию процесс в контейнере — root (UID 0). Если в приложении найдут уязвимость (например, исполнение команд), атакующий получает root внутри контейнера. Само по себе это ещё не root на хосте, но это плацдарм: дальше он ищет любую щель в изоляции. А если контейнер ещё и примонтировал хостовый путь или сокет — выход на хост становится тривиальным.

Как защититься: non-root

Запускайте процесс от непривилегированного пользователя. В Dockerfile создайте пользователя и переключитесь на него:

FROM node:20-slim
RUN useradd --create-home --shell /bin/bash app
WORKDIR /home/app
COPY --chown=app:app . .
RUN npm ci --omit=dev
USER app                 # дальше всё работает от непривилегированного app
CMD ["node", "server.js"]

В Kubernetes продублируйте это в securityContext, чтобы под не мог стартовать от root:

securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]

Привилегированные контейнеры

Флаг --privileged (или privileged: true в поде) снимает почти всю изоляцию: контейнер получает все capability ядра и доступ к устройствам хоста. Из такого контейнера выйти на хост — дело техники. Привилегированный режим нужен крайне редко (некоторые системные агенты), и почти всегда это признак мисконфига.

Особый случай — Docker-сокет

Монтирование /var/run/docker.sock внутрь контейнера равносильно выдаче root на хосте: через сокет можно запустить новый контейнер, примонтировавший весь хост. Это частый «удобный» приём в CI — и частая дыра.

ОпасноПочему
--privilegedвсе capability + устройства хоста → выход на хост
монтаж docker.sockконтроль над демоном Docker = root на хосте
монтаж / или /etcчтение/правка файлов хоста из контейнера

Защита: не используйте privileged, выдавайте отдельные capability через --cap-add точечно, не монтируйте хостовые сокеты и системные пути. В Kubernetes ограничьте это политиками Pod Security Standards (уровень restricted) и admission-контролем.

Открытый API оркестратора

Незащищённый Docker API (порт 2375 без TLS) или Kubernetes API/kubelet, доступный из интернета без аутентификации, — это прямой пульт управления инфраструктурой. Боты постоянно сканируют интернет на такие порты, находят их и запускают свои контейнеры (часто — майнеры).

Как защититься

  • API не выставляют в интернет: только внутренняя сеть/VPN, доступ по mTLS.
  • В Kubernetes включён RBAC; анонимный доступ к API и kubelet запрещён.
  • Сетевые политики (NetworkPolicy) ограничивают, кто к кому может ходить внутри кластера.

Секреты в образе

Зашитые в образ пароли, токены и ключи — частая ошибка. Опасный нюанс: даже если вы удалили секрет в следующем слое, он остаётся в истории слоёв образа, и его видно через docker history или распаковку слоёв. Образ из публичного реестра с секретом внутри — это утечка для всех.

# Образ помнит всё: секрет из удалённого слоя всё равно в истории
docker history --no-trunc my-app:latest
# Сканирование образа на уязвимости и секреты (свой образ, в CI)
trivy image my-app:latest

Защита: секреты передают в рантайме (переменные окружения из менеджера секретов, Kubernetes Secrets, смонтированные файлы), используют multi-stage build и .dockerignore, чтобы лишнее не попало в образ, и сканируют образы в CI (trivy, grype).

Как это работает под капотом

Контейнер — это процесс хоста, «обёрнутый» в namespaces и cgroups. Безопасность держится на том, что процесс непривилегирован (non-root), лишён опасных capability, не имеет каналов к хосту (сокеты, монтажи) и ограничен политиками. Любая «дыра» из разобранных выше — это либо лишние права процесса (root, privileged), либо лишний канал наружу (открытый API, хостовый сокет), либо данные, которым не место в образе. Оборона = убрать лишние права и лишние каналы.

Как защититься

  • Образы и поды — non-root: USER в Dockerfile, runAsNonRoot и drop: [ALL] capability в securityContext.
  • Никаких privileged и смонтированных docker.sock/системных путей; capability добавляются точечно.
  • API оркестратора закрыт от интернета, доступ по mTLS, включён RBAC и NetworkPolicy.
  • Секреты — только в рантайме (менеджер секретов / Kubernetes Secrets), не в образе; multi-stage build.
  • Сканируйте образы в CI (trivy/grype) и применяйте Pod Security Standards уровня restricted через admission-контроль.

Итоги

  • Контейнер — это изолированный процесс ядра, а не виртуалка; изоляция протекает при плохой настройке.
  • Четыре типовых риска: root, privileged/сокет хоста, открытый API, секреты в образе.
  • Базовая оборона: non-root, drop всех capability, никаких привилегий и хостовых монтажей.
  • RBAC, NetworkPolicy и закрытый API защищают оркестратор; сканирование образов ловит уязвимости и секреты.
  • Эксперименты — только в своей лаборатории; чужая инфраструктура под защитой закона (ст. 272/274 УК РФ).
Проверьте себя
1. Почему запуск контейнера в режиме --privileged опасен?
AКонтейнер начинает потреблять слишком много памяти
BКонтейнер получает почти все capability ядра и доступ к устройствам хоста, что упрощает выход на хост
CОбраз перестаёт собираться в CI
DЭто автоматически открывает порт 2375 в интернет
2. Что из перечисленного — корректная базовая защита пода в Kubernetes?
ArunAsNonRoot: true и drop всех capability в securityContext
BМонтировать /var/run/docker.sock для удобства отладки
CЗашить пароль базы прямо в Dockerfile
DВыставить Kubernetes API в интернет без аутентификации