Безопасность контейнеров и 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 УК РФ).