securityContext: запуск Pod без root
Поля securityContext, которые превращают обычный контейнер в non-root процесс с урезанными правами и readonly-корнем.
Что такое securityContext
Это блок настроек безопасности процесса в контейнере. Живёт на двух уровнях:
spec.securityContextприменяется ко всем контейнерам Pod. Pod - наименьшая единица запуска в Kubernetes, внутри неё работает один или несколько контейнеров.spec.containers[i].securityContextотносится к конкретному контейнеру и переопределяет Pod-уровень.
Не каждое поле есть на обоих уровнях. На Pod-уровне: runAsUser,
runAsGroup, runAsNonRoot, fsGroup, seccompProfile. На
контейнерном уровне дополнительно: capabilities,
allowPrivilegeEscalation, readOnlyRootFilesystem, privileged.
Ниже разобрано каждое поле по одному, а в конце собран готовый жёсткий шаблон из них всех.
| Поле | Что запрещает | Что видно, если приложению это нужно |
|---|---|---|
runAsNonRoot: true | старт от UID 0 | CreateContainerConfigError |
capabilities.drop: ["ALL"] | дефолтный набор root-привилегий | ping не работает без NET_RAW |
allowPrivilegeEscalation: false | setuid-бинари | sudo, passwd, mount падают |
readOnlyRootFilesystem: true | запись в /, /etc, /usr, /var | Read-only file system |
seccompProfile: RuntimeDefault | около 60 опасных системных вызовов | обычно ничего, фильтр незаметен |
Запретить root: runAsNonRoot + runAsUser
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true. Kubelet проверяет UID процесса при старте
контейнера. Kubelet - агент Kubernetes на узле, который запускает
контейнеры. Если UID = 0, Pod падает в CreateContainerConfigError: container has runAsNonRoot and image will run as root. Защита от
дурака: кто-то накатил образ, в котором зашит USER 0, и контейнер
просто не стартует.
runAsUser: 1000. Kubelet форсит UID процесса. Работает, только если
образ не требует root для запуска. Многие официальные образы
(nginx, redis) требуют root для bind на 80 порт или setuid. Для
non-root запуска нужны специальные сборки (nginxinc/nginx-unprivileged,
bitnami/redis).
Альтернатива: указать USER в Dockerfile при сборке образа.
Контейнер под runAsUser: 1000 может получить permission denied на
файлах из образа: они принадлежат root, и UID 1000 их не читает.
Помогает USER в Dockerfile или fsGroup для смонтированных volume.
Сбросить capabilities
Linux capabilities делят root-привилегии на куски. CAP_NET_BIND_SERVICE
разрешает bind на порты <1024, CAP_NET_RAW даёт ping, CAP_SYS_ADMIN
даёт почти всё (его иногда называют "новый root").
По умолчанию контейнер получает дефолтный набор:
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_NET_BIND_SERVICE,
CAP_SETGID, CAP_SETUID, ещё пара. Веб-приложению из них ничего не
нужно. Сбрасывают все:
containers:
- name: app
securityContext:
capabilities:
drop:
- ALL
Если конкретный cap нужен (например NET_BIND_SERVICE для bind на 80
порт), его добавляют через add:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
allowPrivilegeEscalation: false
Флаг ядра no_new_privs. Запрещает процессу получить больше
привилегий, чем у родителя. Главное практическое последствие:
не работают setuid-бинари. sudo, ping, passwd, mount внутри
контейнера будут падать. Это правильно: в боевом контейнере им делать
нечего. Большинство дистрибутивов под Kubernetes (distroless, alpine
без setuid) уже не имеют suid-битов.
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem
Монтирует корневую FS контейнера в readonly. Любая запись в
/, /etc, /usr, /var падает с Read-only file system. Для
приложений, которые пишут логи, кэш или PID-файлы, это сразу ломает
запуск.
Решение: подмонтировать emptyDir на каталоги, куда нужна запись.
containers:
- name: app
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
/tmp нужен почти всегда (даже busybox shell пишет туда историю).
Дополнительно частые точки: /var/run, /var/log, /var/cache/nginx
для nginx.
seccompProfile
seccomp фильтрует системные вызовы. До v1.25 контейнеры по умолчанию
бежали без seccomp-фильтра, что давало доступ к экзотике
(add_key, keyctl, bpf), которой настоящие приложения не
пользуются. С v1.25 в качестве best practice ставится:
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
RuntimeDefault это фильтр, который заблокирует около 60 опасных
системных вызовов. Большинство приложений работает без вопросов.
Альтернативы: Unconfined (без фильтра) и Localhost (свой профиль из
файла).
Одна деталь про умолчания. Возможность включить RuntimeDefault для
всего узла разом дошла до GA в 1.27, но это опция kubelet
seccompDefault, и включают её вручную. Пока администратор её не
поставил, контейнер без seccompProfile в спеке бежит как
Unconfined. Поэтому поле пишут в манифесте, а не надеются на кластер.
Готовый «жёсткий» шаблон
apiVersion: v1
kind: Pod
metadata:
name: secured
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Это близкий к Pod Security Standards restricted минимум, который
дальше принимается в admission-контроллерах.
Что часто ломается
- Pod зависает в
CreateContainerConfigErrorсrunAsNonRoot. Образ запускается от root, нужен другой образ илиrunAsUserс ненулевым UID. readOnlyRootFilesystemпоставили, контейнер сразу падает. Приложение пишет на корень. Найти куда (strace,df, документация образа) и подмонтировать emptyDir.drop: [ALL]поставили, ping не работает изнутри. Это нормально,pingхочетNET_RAW. Если он не нужен, оставляют как есть.
Список полей и их описания всегда под рукой в самом kubectl:
kubectl explain pod.spec.securityContext
kubectl explain pod.spec.containers.securityContext --recursive | head -40
Домашка
Теорию закрепляет задание на своём кластере: 1 шаг с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.
./run.sh start k8s-security-context # развернуть стартовое состояние
./run.sh verify k8s-security-context # проверить текущий шаг
Коротко
securityContextесть на Pod-уровне и на контейнерном; контейнерный переопределяет Pod-уровень.runAsNonRoot: trueне даст стартовать контейнеру с UID 0,runAsUserфорсит конкретный UID.capabilities.drop: ["ALL"]убирает дефолтный набор root-привилегий, нужное возвращают черезadd.allowPrivilegeEscalation: falseвключаетno_new_privsи ломает setuid-бинари внутри контейнера.readOnlyRootFilesystem: trueтребует emptyDir хотя бы на/tmp.seccompProfile: RuntimeDefaultпишут в манифесте: без него контейнер чаще всего бежит какUnconfined.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс