Безопасность подов: securityContext
Контейнер по умолчанию может работать от root и захватить ноду — если вы это не запретили.
securityContext — секция манифеста, которая ужесточает права контейнера и пода на уровне ядра Linux: под каким пользователем запускаться, какие привилегии иметь, можно ли писать в корневую файловую систему.
Контейнер — это не виртуальная машина, а процесс на ноде, изолированный namespaces и cgroups. Если этот процесс работает от root и сбежит из контейнера (через уязвимость в рантайме или примонтированный сокет), он станет root на самой ноде. Поэтому первая линия обороны пода — снять с него лишние привилегии заранее, в декларации.
Зачем это на практике
Большинство приложений (веб-сервер, API, воркер) не нужны права суперпользователя: им достаточно слушать порт > 1024 и читать свой код. Но базовые образы часто запускают процесс от root «на всякий случай». Атакующий, получивший RCE в таком контейнере, сразу имеет root внутри и широкую поверхность для побега. Один блок securityContext закрывает большинство таких сценариев — это дёшево и обязательно для прода.
Важно понимать модель угроз. Изоляция контейнера держится на функциях ядра Linux — namespaces (отдельные представления процессов, сети, точек монтирования) и cgroups (ограничение ресурсов). Это не граница виртуальной машины: ядро у контейнера и ноды общее. Поэтому уязвимость в рантайме, монтирование сокета Docker внутрь пода или привилегированный режим способны превратить компрометацию одного контейнера в компрометацию всей ноды, а через неё — соседних подов. Снятие лишних привилегий не делает побег невозможным, но многократно сужает то, что атакующий сможет сделать, даже добравшись до контейнера.
securityContext: ключевые поля
Контекст можно задать на уровне пода (spec.securityContext, применяется ко всем контейнерам) и на уровне контейнера (containers[].securityContext, переопределяет под). Самые важные поля:
| Поле | Эффект |
runAsNonRoot: true | kubelet не запустит контейнер, если внутри он стартует от UID 0 |
runAsUser: 1000 | принудительно запускает процесс под этим UID |
readOnlyRootFilesystem: true | корневая ФС только для чтения — запись лишь в явные тома |
allowPrivilegeEscalation: false | процесс не сможет повысить привилегии (setuid, sudo) |
privileged: false | запрет привилегированного режима (доступ ко всем устройствам ноды) |
capabilities.drop: ["ALL"] | снять все Linux capabilities, добавить обратно лишь нужные |
Эталонный «жёсткий» под
Соберём контейнер, у которого почти ничего нельзя, кроме его прямой работы:
apiVersion: v1
kind: Pod
metadata:
name: hardened-web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.27
ports:
- containerPort: 8080
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: cache
mountPath: /tmp
volumes:
- name: cache
emptyDir: {}
Обратите внимание: образ — nginx-unprivileged, слушающий порт 8080, а не привилегированный 80. При readOnlyRootFilesystem: true nginx не сможет писать во временные файлы, поэтому мы примонтировали emptyDir в /tmp. Это типичный приём: корень закрыт, а под запись выделены отдельные тома.
Linux capabilities
Capabilities — это «нарезка» прав root на отдельные способности (открыть порт < 1024 — NET_BIND_SERVICE, менять владельца файлов — CHOWN и т.д.). По умолчанию контейнеру выдаётся целый набор. Безопасный паттерн — снять ALL и вернуть точечно только необходимое:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"] # только если реально нужен порт < 1024
Pod Security Standards и Pod Security Admission
Прописывать securityContext в каждом манифесте надёжно, но люди забывают. Поэтому в Kubernetes есть встроенный механизм, который проверяет поды на входе. Он опирается на три Pod Security Standards (PSS) — готовые профили требований:
| Профиль | Смысл |
privileged | без ограничений (для системных компонентов) |
baseline | запрещает явно опасное: privileged, hostNetwork, hostPID |
restricted | жёсткий профиль: требует runAsNonRoot, drop ALL, запрет escalation |
Применяются они через Pod Security Admission — встроенный контроллер, который включается простыми метками на namespace. Метка задаёт уровень и режим (enforce — блокировать, warn — предупреждать, audit — писать в журнал):
kubectl label namespace prod \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted
Теперь любой под в prod, не проходящий профиль restricted (например, с privileged: true), будет отклонён на этапе создания. Удобно сначала повесить warn/audit, посмотреть на нарушения, и только потом включать enforce.
Как это работает под капотом
За соблюдением профиля следит admission-стадия API-сервера — тот же этап, что валидирует и мутирует объекты после авторизации RBAC. Pod Security Admission читает метки namespace, сравнивает спецификацию пода с требованиями выбранного PSS и при нарушении в режиме enforce возвращает ошибку, не записывая объект в etcd. Сам же securityContext применяет уже kubelet: он передаёт рантайму UID, набор capabilities и флаги, а ядро Linux навешивает их через user namespaces, cgroups и seccomp.
Частые ошибки
- privileged: true ради одной мелочи. Привилегированный контейнер видит все устройства ноды — это почти отказ от изоляции.
- runAsNonRoot без подходящего образа. Если образ хардкодит запись в
/var/runот root, под просто не стартует — нужен unprivileged-образ или примонтированные тома. - readOnlyRootFilesystem без emptyDir. Приложение падает на первой попытке записать лог или temp-файл; выделяйте запись отдельными томами.
- hostNetwork/hostPID «для отладки». Они ломают сетевую и процессную изоляцию пода; в проде их быть не должно.
- Метка PSA только enforce сразу на работающий namespace. Можно мгновенно заблокировать деплои; начинайте с warn/audit.
Итоги
- Контейнер — процесс на ноде; root внутри легко превращается в root на ноде, поэтому привилегии снимают заранее.
runAsNonRoot,readOnlyRootFilesystem,allowPrivilegeEscalation: falseиdrop: ["ALL"]— обязательный минимум для прода.- Capabilities снимают целиком и добавляют точечно только нужные.
- Pod Security Standards (privileged/baseline/restricted) задают готовые профили, а Pod Security Admission применяет их метками на namespace.
- Включайте профиль через warn/audit, и только потом enforce, чтобы не сломать существующие деплои.