Kubernetes SecurityContext: разбор с примерами
В этой заметке (продолжающей вчерашнюю тему) разбираемся, почему поды без root-доступа - это важно, и подробно рассматриваем параметры Kubernetes SecurityContext, которые позволяют принудительно настраивать запуск от non-root пользователя на уровне пода и контейнера.
Почему non-root поды?
Для начала давайте разберёмся, почему вообще стоит использовать только non-root поды.
Во-первых, это снижает риски безопасности. Запуск контейнеров от non-root пользователей уменьшает потенциальный ущерб от эксплуатации уязвимостей, поскольку процессы внутри контейнера не имеют root-доступа к хост-системе.
Кроме того, это соответствует принципу минимальных привилегий: контейнеры работают только с теми правами, которые действительно необходимы для выполнения их задач.
И наконец - соответствие требованиям. Во многих организациях и отраслях существуют политики и стандарты (например, PCI DSS), которые требуют запускать контейнеры от non-root пользователей, чтобы удовлетворять требованиям безопасности и регуляторов.
UID по умолчанию, назначаемый подам
По умолчанию Kubernetes назначает контейнерам в поде UID 0 (root), если вы явно не указали другого пользователя.
Вот пример, который показывает user ID контейнера в поде без какой-либо настройки безопасности — ни на уровне образа, ни на уровне пода.
$ kubectl exec -it flask-app -- sh # whoami root
Kubernetes поддерживает настройки на уровне всего кластера, которые позволяют принудительно запрещать запуск подов от root. Это можно реализовать с помощью PodSecurity Standards (PSS) или Pod Security Admission (PSA).
SecurityContext в Kubernetes
Чтобы запускать поды от non-root пользователей, для начала нужно разобраться с параметром SecurityContext.
SecurityContext поддерживает параметры runAsUser и runAsGroup, с помощью которых можно указать пользователя и группу, от имени которых должен работать контейнер.
При этом, если вы не хотите задавать конкретного пользователя, но всё же хотите принудительно запретить root, можно использовать параметр runAsNonRoot: true.
Например:

SecurityContext на уровне пода и контейнера
SecurityContext можно задавать как на уровне пода, так и на уровне контейнера.
Уровень пода:
Такой securityContext применяется ко всем контейнерам внутри пода. По сути, это настройки по умолчанию для каждого контейнера в этом поде.
Например:

Уровень контейнера:
Этот securityContext применяется к конкретному контейнеру. Он даёт больше гибкости, если у отдельных контейнеров есть разные требования. Настройки на уровне контейнера переопределяют параметры, заданные на уровне пода, но только для этого контейнера.

Образ контейнера с non-root пользователем
Теперь представим, что вы создали образ контейнера с non-root пользователем с UID 1001.
Что произойдёт, если в поде не указывать securityContext?
Контейнер запустится от пользователя, заданного при сборке образа. Например, если образ был настроен на запуск от пользователя appuser, то контейнер внутри пода будет работать именно от appuser.
Чтобы это проверить, я задеплоил non-root образ devopscube/flask-app-non-root:1.0, собранный для запуска от appuser, без какого-либо security context.
Результат подтверждает, что контейнер действительно работает от appuser.
kubectl exec -it flask-app-non-root -- sh $ whoami appuser
Итого, общая рекомендация такая:
- Если образ корректно настроен с non-root пользователем - не указывайте
runAsUser. - Если по требованиям (например, комплаенс) нужно принудительно задать конкретный UID независимо от образа - используйте
runAsNonRootилиrunAsUser, в зависимости от сценария.
если в namespace включён Pod Admission и разрешены только non-root поды, необходимо указать runAsNonRoot: true. В этом случае контейнер автоматически будет использовать user ID, заданный в образе контейнера.Образ контейнера с root-пользователем
Теперь рассмотрим ситуацию, когда вы используете образ контейнера, рассчитанный на запуск от root (что по умолчанию встречается во многих образах).
Обычно это происходит, если в Dockerfile не задана инструкция USER или образ по умолчанию предполагает запуск от root.
В таком случае, если в securityContext включить runAsNonRoot: true, под завершится с ошибкой.
Ошибка возникает потому, что Kubernetes настроен на жёсткое соблюдение политики runAsNonRoot, но контейнер пытается запуститься от root, при том что в самом образе non-root пользователь не определён.
Смягчить это можно, добавив runAsUser с конкретным UID.
Но что если приложению внутри образа действительно нужны root-привилегии? Возьмём пример с обычным образом nginx и таким манифестом:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
containers:
- name: app-container
image: nginx
imagePullPolicy: Always
ports:
- containerPort: 5000
Если задеплоить этот манифест, под уйдёт в состояние CrashLoopBackOff.
$ kubectl get po NAME READY STATUS RESTARTS AGE nginx 0/1 CrashLoopBackOff 4 (10s ago) 47s
Причина в том, что стандартный образ nginx изначально рассчитан на запуск от root, чтобы иметь возможность привязываться к привилегированным портам, таким как 80 (порты ниже 1024 требуют root-прав). После старта процесс nginx может сбросить привилегии до менее привилегированного пользователя, но стартовать он всё равно должен от root.
Поэтому, если вы хотите запускать nginx как non-root пользователя, нужно собрать кастомный образ nginx, рассчитанный на работу без root. Для подробностей можно посмотреть на Nginx Unprivileged Image.
Итог: non-root под будет работать без ошибок только в том случае, если процесс внутри контейнера не требует root-привилегий.
Если же он пытается изменять системные настройки, работать с файлами, принадлежащими root, или привязываться к привилегированным портам (например, < 1024), это приведёт к ошибкам.
Заключение
SecurityContext - это довольно обширная тема с большим набором возможностей.
Например:
- Capabilities: позволяет добавлять или удалять Linux-capabilities. Удаление лишних возможностей уменьшает поверхность атаки контейнера.
- seccompProfile: управляет тем, какие системные вызовы может использовать контейнер. Ограниченный профиль блокирует опасные или ненужные system call’ы.
- AppArmor: модуль безопасности ядра Linux, реализующий Mandatory Access Control (MAC). Его можно подключать к подам.
- allowPrivilegeEscalation: определяет, может ли процесс получить больше привилегий, чем его родительский процесс.
- readOnlyRootFilesystem: при значении
trueкорневая файловая система контейнера становится доступной только для чтения, что снижает риск изменения системных файлов злоумышленником.
Информация: все эти механизмы безопасности в Kubernetes задаются на уровне пода через его security context, но в итоге они транслируются в настройки контейнерного рантайма.
Именно контейнерный рантайм (например, containerd или CRI-O) отвечает за фактическую реализацию и применение этих мер безопасности.